Confusing behavior from computed properties with async Ember models - ember.js

I ran into some strange behavior trying to build computed properties into my Ember Data model. Here's what my .coffee file looks like:
LineItemModel = DS.Model.extend
quantity: attr 'number'
length: attr 'number'
product: DS.belongsTo 'product', async: true
priceAdjust: attr 'number', defaultValue: 0
weight: (->
return this.get('product.weight') * length
).property('product')
# Get MLF price
price: (->
# Adjust if percentage is specified
adjust = (mlf, adjustment) ->
return mlf unless adjustment
mlf += Math.ceil(mlf * adjustment / 100)
prices = this.get('product').get 'prices'
level = this.get('quote').get 'level'
price = prices.filterBy 'level', level
return if price[0] then adjust( price[0], this.get('priceAdjust') ) else 0
).property('product', 'product.#each.prices', 'quote.level', 'priceAdjust')
My problem is that in my weight method this.get('product.weight') always returns undefined. I've done some testing and anything other than this.get('product.id') returns undefined. To make thing even more confusing, this works
Ember.computed.alias('product.weight')
and this from with the price method
# Get MLF price
price: (->
console.log this.get('product.weight')
...
This is the product model for reference:
ProductModel = DS.Model.extend
...
weight: attr 'number'
prices: DS.hasMany 'price', async: true
I'm not sure if I did something wrong or if this is a bug. This is a cli app v0.1.15

So as far as I can tell, the problem is related to promises as whether or not they're resolved. Here's what I came up with so far:
weight: Ember.computed.alias 'product.weight'
Computed property easily takes care of handling the promise relationship business
# Lbs / Ft
lbs: (->
return this.get('weight') * this.get('length')
).property('weight', 'length')
Now I know I have some value for weight and I can do my calculation with it. I'm still not sure why I didn't seem to need this in the price method but this seems like the "Ember" way to do things. I'd love some feedback if this can be improved.

Related

Amplify AppSync: custom sorting and filtering with pagination

I'm trying to write a schema so that I can query models filtered by multiple keys, sorted by a custom key and paginated.
an example of my model:
type Article {
id: ID!
category: String!
area: String!
publishOn: AWSDate!
}
And an example of the query I would like to do is: retrieve all the Articles which are part of both a given category AND area, returned in descending order by publishOn in chunks of 10 items each (to implement pagination server-side, and have a lightweight UI).
The response should include also the nextToken attribute that can be used to load the "next" page of the filtered articles list.
I have multiple problems with what I can do with the automatically generated schema and can't find a way to implement manually a solution that works for all what I want to do. I try and make a list of what goes wrong:
Filtering
Let's say I want to query 10 articles that belong to the category "Holiday":
listArticles(filter: {category: {eq: "Holiday} }, limit: 10)
I won't get the first 10 articles that match that category, but instead, it seems that AppSync selects the first 10 items in the table, and then it filters these 10 items by the filter criteria.
In other words, it seems that the sequence in which filtering and sorting are applied is the opposite of what expected. Expected: firstly filter the table by the filter critaria, then return the first 10 items of the filtered result sets.
Sorting
I couldn't find a way to add sorting with AppSync, so I added searchable:
type Article (
#searchable
) {
id: ID!
category: String!
area: String!
publishOn: AWSDate!
}
Now if I sort by date, that key will be used as nextToken and brake the pagination. This is a known issue: https://github.com/aws-amplify/amplify-cli/issues/4434
Do you have any good tip on how to find a workaround to these bugs? I dag into the documentation and in couple of issue, but didn't come up with a solution that works well...
Thanks in advance,
Matteo
Filtering
You will need a Global Secondary Index in DynamoDB to achieve such a behaviour. You can create them with the #key annotation. I your case I would create a composite key consisting of the category for the partition key and area and publishOn as the sort key(s).
type Article
#model
#key(fields: ["id"])
#key(name: "byCategory", fields: ["category", "publishOn"])
#key(name: "byCategoryArea", fields: ["category", "area", "publishOn"])
{
id: ID!
category: String!
area: String!
publishOn: AWSDate!
}
Sorting
Sorting is done by the sortDirection property which is either DESC or ASC and can only be done on the sort key.
The #searchable directive enables elasticsearch on the table, which is a fulltext search engine and probably a bit pricy for small applications and wouldn't be required here unless you would want to query based on e.g. the article description text.
listArticles(filter: {category: {eq: "Holiday"} }, limit: 10, sortDirection: DESC)
Amplify AppSync: filtering with pagination
let allClubsList = async (sport) => {
try {
let clubsList;
let clubsInfoList = [];
let nextTokenInfo = null;
do{
let clubs = await client.query({
query: gql(clubBySportStatus),
variables: {
sport: sport,
eq: { status: "ACTIVE" },
},
limit: 100,
nextToken: nextTokenInfo,
fetchPolicy: "network-only",
});
clubsList = clubs.data.clubBySportStatus.items;
clubsList.forEach((item) => clubsInfoList.push(item));
nextTokenInfo = clubs.data.clubBySportStatus.nextToken;
} while (Boolean(nextTokenInfo));
if (clubsInfoList && clubsInfoList.length) {
return {
success: true,
data: clubsInfoList,
};
}
} catch (eX) {
console.error(`Error in allClubsList: ${JSON.stringify(eX)}`);
return {
success: false,
message: eX.message,
};
}
};

Ember.computed.any with sync and async attributes

App.Farm = DS.Model.extend
location: DS.attr 'string'
App.Field = DS.Model.extend
location: DS.attr 'string'
farm: DS.belongsTo 'farm', async: true
markerLocation: Ember.computed.any('location', 'farm.location')
Somewhere else:
location = field.get('markerLocation')
If the field's location value is empty, will location equal the farm's location value? If not, what's the way to get this done?
Basically, the question here is this:
If I want to grab the markerLocation value, and I'm not using bindings, how do I do it so it waits for the async value if the sync one is not set? Even inside an observer I am having trouble doing this.
Maybe something like this:
markerLocation: ( ->
promise = new Ember.RSVP.Promise (resolve) =>
location = #get('customLocation')
if location
resolve(location)
else
#get('farm').then (farm) ->
resolve(farm.get('location'))
).property('location', 'farm.location')
And then get it by doing something like this:
location = field.get('markerLocation').then (location) ->
# do something with location
Trying to make it work this way now, and there's some promise there (no pun).
Question: Yes
Question: Like you did it or like on this jsbin: http://emberjs.jsbin.com/pukewo/1/edit
Play aroung with line 11 and you'll see the results.

ember / ember-data belongsTo and hasMany not updating as expected

I have two models:
App.Focusarea = DS.Model.extend
definition: DS.attr('string')
theme: DS.belongsTo('theme',
async: true
)
App.Theme = DS.Model.extend
definition: DS.attr('string')
focusareas: DS.hasMany('focusarea',
async: true
)
when creating one of each, I would like to associate the focus area to that theme
theme = store.createRecord("theme",
id: 3
definition: 'theme definition'
)
focusarea = store.createRecord("focusarea",
id: 4
definition: 'focusarea definition'
)
theme.get("focusareas").then (focusareas) ->
focusareas.pushObject focusarea
theme.save()
but when I run my tests
theme.get('focusareas').then (focusareas)->
expect(focusareas.toArray.length).to.equal(1)
it fails - the focusareas.toArray.length equals 0, in other words, the association failed.
What am I doing wrong or what am I missing? Any help will be greatly appreciated.
Update:
Figured it out, theme.get('focusareas') returns an unresolved promise, which is 0, that when resolved will return 1 eg:
focusareas = theme.get('focusareas')
focusareas.then ->
console.log focusareas.get('length') #=1
or
store.find('theme', 4).then (theme)->
theme.get('focusareas').then ->
expect(theme.get('focusareas').get('length')).to.equal(1)
in other words
store.find('theme', 4).then (theme)->
theme.get('focusareas').then ->
theme.get('focusareas').forEach (item) ->
console.log(item.get('definition')) #'focusarea definition'
item.get('theme').then ->
console.log(item.get('theme').get('definition')) #'theme definition'
I guess I should just RTFM!
During object init you need to assign focusareas to some sort of collection that the getters and setters can work with . An ArrayProxy would work nicely in this case. How about even one you can sort items automagically with?
theme = store.createRecord("theme",
id: 3
definition: 'theme definition',
focusareas: Ember.ArrayProxy.createWithMixins Ember.SortableMixin,
content: [],
sortProperties: ['id'], // Set this to any object property
sortAscending: true
)
Hope that helped!

Emberjs-1.0.0-rc.6 using enumerable to list events occurring on a particular date

When I define a controller action to display dates occuring a particular date, it works correctly, but If I convert that controller action to a property it stops displaying the date occuring on a particular event. The jsfiddle
App.EventsController = Em.ArrayController.extend({
todayEvent: function(date){
return this.get('content').filter(function(event) {
return (moment(event.get('start')).unix() == moment(date).unix());
});
}
});
I can fetch an instance of the controller:
u = App.__container__.lookup("controller:events")
on the event 25th, there are 2 events and I can fetch it with
u.todayEvent(new Date('2013-07-25').toString())
which correctly returns
[> Class, > class]
But in the CalendarEvent controller, I want to display the events for a particular date just like above but this time using computed-property, so I redefine todayEvent asa computed property as shown below, only this time, it only returns true or false instead returning class objects containg the events for that day.
The date property is set using controllerFor in the router serializers hook instead of passing it in as we did when we defined todayEvent as a controller action previously.
App.CalendarEventController = Em.ObjectController.extend({
date: null,
needs: ['appointments'],
todayEvent: function(){
var _self = this;
var appoint = _self.get('controllers.appointments');
var appCont = appoint.get('content');
return appCont.map(function(appointee) {
return (moment(appointee.get('event.start')).unix() == moment(_self.get('date')).unix());
});
}.property('date')
});
Now I click on the link for appointment, then the link for calendar and then click one of the dates in red from the calendar, so the serializer hook can set the controller date and I then go into the console:
u = App.__container__.lookup("controller:calendarEvent")
try to fetch the events occuring on that date in the console with:
u.get('todayEvent')
I either get an empty array like this [ ] or if I filter using map() instead of filter(), then it returns [false, false, false]
The jsfiddle
It looks like you need to add 'content.#each' to your computed property.
As it stands now 'todayEvent' will only be computed when 'date' changes I am guessing date is being set before or at the same time as the content.
todayEvent is returning [false, false] because you are using map not filter.
todayEvent: function(){
var _self = this;
var appoint = _self.get('controllers.appointments');
var appCont = appoint.get('content');
return appCont.filter(function(appointee) {
return (moment(appointee.get('event.start')).unix() == moment(_self.get('date')).unix());
});
}.property('content.#each', 'date')

Ember data: Insert new item at beginning of array instead of at the end

In my application I'm displaying a timeline of messages. We retrieve them from the server in descending chronological order, newest to oldest.
3 - Howdy
2 - Greetings
1 - Mahalo
Our users also have the ability to add a new message which by default gets inserted at the end of the queue like so
3 - Howdy
2 - Greetings
1 - Mahalo
4 - I'm the new message, last as usual
When I submit, I'd like new messages to show up at the top. I've written a function before that reverses the array of items, but that wouldn't work for items already in the array.
4 - I'm the new message, first finally
3 - Howdy
2 - Greetings
1 - Mahalo
What would be the best approach in this case? The ideal would be for Ember Data to prepend to the content array rather than append. Is there another option which might be better?
For most scenarios involving sorting it's recommented to use Ember.SortableMixin, which is baked into Ember.ArrayController.
Please refer to this conceptual example in JSFiddle: http://jsfiddle.net/schawaska/tbbAe/
In this sample the model has a DateTime field named when, which I'm using for filtering:
App.Greeting = DS.Model.extend({
text: DS.attr('string'),
when: DS.attr('date')
});
App.Greeting.FIXTURES = [
{id: 1, text: 'First', when: '3/4/2013 2:44:52 PM'},
{id: 2, text: 'Second', when: '3/4/2013 2:44:52 PM'},
{id: 3, text: 'Third', when: '3/4/2013 2:44:52 PM'},
{id: 4, text: 'Fourth', when: '3/4/2013 3:44:52 PM'}
];
In the controller the only thing I have to do is to set the name of the property and the sorting direction:
App.SortingMixinController = Em.ArrayController.extend({
sortProperties: ['when'],
sortAscending: false
});
Then in my Handlebars template, I can use the {{each}} helper as I would do normally.
Because in this sample, all the dates are the same except for the Forth (which because of sorting appears first), and also because of SortableMixin, these values will be sorted through another property - I'm assuming the Id here.
The other approach I've taken in that fiddle is using a computed property. I'm not really sure about that approach as it seems to consume more resources and the code in App.SortingPropertyController is worthy of laugh, but sort of works to show possibilities.
Can you use basic JavaScript to drop in the new item at a given location in the array? Not sure if this is basic ajax + js objects or full blown ember-data models but this works for the simple js array example
var arr = [];
arr[0] = "Jani";
arr[1] = "Hege";
arr[2] = "Stale";
arr[3] = "Kai Jim";
arr[4] = "Borge";
console.log(arr.join());
arr.splice(2, 0, "Lene");
console.log(arr.join());
The output of the code above will be:
Jani,Hege,Stale,Kai Jim,Borge
Jani,Hege,Lene,Stale,Kai Jim, Borge