I have 3 different fixture models, as shown below.
var Room = DS.Model.extend({
title: DS.attr('string'),
categories: DS.hasMany('Category', { async: true }),
isSelected: DS.attr('boolean')
});
var Category = DS.Model.extend({
title: DS.attr('string'),
room: DS.belongsTo('Room', {async: true }),
materials: DS.hasMany('Material', { async: true }),
isSelected: DS.attr('boolean')
});
var Material = DS.Model.extend({
title: DS.attr('string'),
category: DS.belongsTo('Category', {async: true} ),
isSelected: DS.attr('boolean')
});
I find when I try to view the contents inside the Materials model it is blank. In my controller I expose the materials by doing this:
currentMaterials: function() {
var room = this.filterBy('isSelected', true).get('firstObject');
var categories = room.get('categories');
var selectedCategory = categories.get('firstObject');
var material = selectedCategory.get('materials');
return material;
}.property('#each.isSelected')
However when I try to access currentMaterials the value is null. I am ONLY able to access its values if I first access the Rooms/Categories using a {{#each} loop. Oddly once I do the {{#each}} I am then able to access the values in currentMaterials.
Does anyone understand why?
It's due to fact of promises existance. Your categories relationship is async, which means that it's not present initially and ember-data should fetch it if needed. However, it takes time to fetch data, therefore ember-data returns a promise from this: var categories = room.get('categories'). After that promise, you first get firstObject from it, which does not exist for a promise (is null), and than you get materials relationship from that null. It simply is null.
However, ember templates are smart and if you put an each on them, they know that these relationships are needed and makes ember-data fetch these data.
What you can do? If you need this data to perform page-specific job, you should make sure that you have access to it before showing the page to the user - therefore in the model hook. You can use Ember.RSVP to make multiple fetch calls and set them up in the controller:
model: function() {
data =
room: store.find("room")
categories: store.find("category)
materials: store.find("material")
return Ember.RSVP.hash(data)
}
However, take notice that it will fetch all the materials, etc. If you need only the ones connected to your model, you should consider speeding up your data fetching using side loading. If you are using fixtures, it won't work.
Last that I can think of is making computed property a method that would fetch the data, but set them on other variable. You can use some kind of flag to inform the app when the data is ready:
currentMaterials: function() {
var room = this.filterBy('isSelected', true).get('firstObject');
room.get('categories').then(function(categories) {
return categories.get('firstObject').get('materials');
}).then(function(materials) {
// here you have your materials
// you can pass _this to that method and set these materials
// on some kind of controller property (e.g. materialsChosen)
// and use a flag like setting 'is Fetching' on the start of this
// computed property and setting down right here
});
}.property('#each.isSelected')
Related
i have two models:
User:
export default DS.Model.extend({
books: hasMany('book', { async: true }),
});
book:
export default DS.Model.extend({
title: DS.attr('string'),
state: DS.attr('number'),
});
in my of my controller i am getting one user as my model and i want to create a computed property like this
activeBooks: Ember.computed('model.books', function() {
var books = this.get('model.books').filter(function (book, index, array) {
// debugger;
return (this.get('book.state') === 1);
}.bind(this));
return books;
}),
but the filter is not working and basically return a empty array.
P.S 1. i am side loading books alone with user. and at the debugger line i query for this.get('user.books.length') i get the correct number of books for each user. could someone point out what i am doing wrong here?
thanks a lot!
You can use filterBy and return the result.
activeBooks:Ember.computed('model.books', function() {
return this.get('model.books').filterBy('state',1);
})
You don't need to use bind(this) inside filter function.
I have a Property model and a Pricing Summary model, which relate to each other and are shown below:
App.Property = DS.Model.extend({
totalRoomCount: DS.attr(),
name: DS.attr(),
address: DS.attr(),
city: DS.attr(),
state: DS.attr(),
zip: DS.attr(),
pricingSummaries: DS.hasMany('pricingSummary', {async: true})
});
App.PricingSummary = DS.Model.extend({
startDate: DS.attr(),
endDate: DS.attr(),
days: DS.hasMany('day', {async: true}),
property: DS.belongsTo('property', {async: true})
});
Inside of my Property route I set the model to a Property, and then in the template, I want to output a list of the PricingSummary's that are related to that Property, as follows:
{{#each pricingSummary in pricingSummaries}}
{{render 'summaryRow' pricingSummary}}
{{/each}}
This works, and I'm able to output the attributes of each particular PricingSummary inside of the summaryRow template, like its startDate and endDate, for example. But what I REALLY want to do here is modify/format the startDate and output this formatted version. Basically I think I want a controller at this point, but I don't know how to tie a controller to the specific Pricing Summary model being output.
How do I do this? And furthermore, you can see that a PricingSummary also has a relationship to my Day model, so I'm going to want to do this again, another level deep.
Please help!
There are several ways to accomplish this, and all of them are relatively simple.
In relation to actually decorating a model, the easiest method would be to create a computed property on the model itself. Some people don't like this because they believe the models should be skinny and decorators should be in controllers/components, but it's all up to your preference. You could accomplish it this way:
App.YourModel = DS.Model.extend({
date: attr('date'),
formattedDate: function() {
var date = this.get('date');
return date ? this.get('date').toDateString() : null ; // Use your own format :-)
}.property('date')
});
Alternatively, I like to use a getter/setter pattern so you can use two-way bindings and it will marshal the value to a date on set, or to a string on get. In the following example, I'm using moment.js to parse/format:
App.YourModel = DS.Model.extend({
date: attr('date'),
dateMarshal: function(key, value) {
if (arguments.length > 1) {
var parsed = moment(value);
this.set('date', parsed.isValid() ? parsed.toDate() : null);
}
return this.get('date') ? moment(this.get('date')).format('MM/DD/YYYY') : null ;
}.property('date'),
});
Another option would be to provide an itemController property to the {{#each}} helper, but that's effectively the same as using render without having to use a custom view.
If you're using more properties and perhaps some actions on the pricing summary row (to delete it, for instance), my preference would be to use a component:
{{#each pricingSummary in pricingSummaries}}
{{pricing-summary-item content=pricingSummary}}
{{/each}}
And your component:
App.PricingSummaryItem = Ember.Component.extend({
content: null,
dateFormatted: function() {
var formattedDate = this.get('content.date');
// Format your date
return formattedDate;
}.property('content.date')
actions: {
'delete': function() {
this.get('content').deleteRecord();
},
markRead: function() {
this.set('content.isRead', true);
this.get('content').save();
}
}
});
Finally, to address JUST the date issue and not decoration, I would make a bound helper. Again, this example uses moment.js (and I'm using ember-cli as well, so pardon the ES6 syntax):
import Ember from 'ember';
export function formatDate(input, options) {
input = moment(input);
if (options.hashContexts.fromNow) {
return input.fromNow();
} else {
return input.format(options.hash.format || 'LL');
}
}
export default Ember.Handlebars.makeBoundHelper(formatDate);
Then you just use {{format-date yourDateProperty}} in your template.
I am trying to get an aggregated list of employees. Each organization is a model that hasMany employees.
var Organization = DS.Model.extend({
name: attr('string'),
employees: hasMany('employee', { async: true })
});
On my controller, I have this property.
employees: function(){
var employees =[];
this.get('organizations').forEach(function(organization){
employees.pushObjects(organization.get('employees'));
});
return employees;
}.property('organizations.#each.employees.isFulfilled')
When I loop through these on the template, the employees are being loaded, I can see the api returning the data (Im using async: true), but the return value of employees is still an empty array.
It seems like I might be listening to the wrong thing on the property. Please help.
Ember doesn't support dependent properties multiple levels deep.
http://emberjs.com/guides/object-model/computed-properties-and-aggregate-data/
And your employees being an async property will be empty when you attempt to push in this fashion. You'd need to move the fulfilled employees a level higher (something like this):
var Organization = DS.Model.extend({
name: attr('string'),
employees: hasMany('employee', { async: true }),
loadedEmployees: function(){
return this.get('employees');
}.property('employees.[]')
});
With a computed property using loadedEmployees
employees: function(){
var employees =[];
this.get('organizations').forEach(function(organization){
employees.pushObjects(organization.get('loadedEmployees'));
});
return employees;
}.property('organizations.#each.loadedEmployees')
I have a computed property, which fetches an associated record and tries to print it. The first time I fetch the record, it's null. All subsequent accesses work correctly. It is set as 'async: true', but setting it as false doesn't change this behavior.
MyApp.ThingsController = Ember.ArrayController.extend({
myProperty: function() {
var content = this.get('content');
return content.filter(function(thing) {
console.log(thing.get('title')); // Since this is a direct attribute on the model, it prints fine.
var associatedThing = thing.get('associatedThing'), otherThings = [];
console.log(associatedThing.get('content')); // This is a hasMany attribute on the model, and is null the *first* time, but fine on subsequent accesses.
otherThings = associatedThing.get('content'); // Obviously doesn't work the first time either.
return thing.get('title') + otherThings[0].get('name'); // Or similar.
});
}.property('content.#each') // adding observers for content.associatedThing.#each does not seem to make any difference.
});
Models are like:
MyApp.Thing = DS.Model.extend({
title: DS.attr('string'),
associatedThings: DS.hasMany('associatedThing', { async: true })
});
MyApp.AssociatedThing = DS.Model.extend({
name: DS.attr('string')
});
Obviously, I cannot use promises here since I need to return a value from the function, so I cannot use a callback (since we're in a computed property.) How can I make this work the first time this associated record is accessed?
Edit: myProperty is a computed property on an ArrayController, and is used for showing or hiding Things
Actually, you can use a promise, just not in the way you're thinking. For hasMany relationships, Ember-Data returns a PromiseArray. That means that it returns a promise that will resolve to an array. But in the meantime, the proxy will actually respond to get requests that you make with undefined. Then, when the promise resolves, any observers are fired. So, if you have your property depend on the associatedThings property, it will update when the promise resolves. In other words, this will work as expected:
MyApp.Thing = DS.Model.extend({
title: DS.attr('string'),
associatedThings: DS.hasMany('associatedThing', { async: true }),
sum: function() {
var things = this.get('associatedThings');
return things.filter(function(thing) {
return shouldFilterThing(thing);
});
}.property('associatedThings.#each.size')
});
Also, please don't be bugged by the fact that this doesn't happen synchronously. Trying to change it from asynchronous to synchronous will just make your code that much more fragile. Let Ember do its job and handle all of the properties and bindings for you.
My solution to this was simply to access the associated data in the ArrayController's init method:
init: function() {
var content = this.get('content');
content.forEach(thing) {
// Prime the data.
var associatedThings = get('associatedThings');
});
}
This makes everything work as expected.
I'm trying to build the following view with Ember.js:
Users: (x in total)
* User 1: y Posts
* User 2: z Posts
I've created a itemController that is responsible for getting the number of posts of each user.
App.IndexItemController = Ember.ObjectController.extend({
postCount: function() {
var posts = this.get('content').get('posts');
return posts.get('length');
}.property()
});
Full code on jsbin.
Somehow I always get 0 posts for each user, I guess that is because the relationship is not resolved correctly at this.get('content').get('posts'). What would be the right way to do this? Or am I going a completely wrong way?
Bonus question: What can I pass to the property() and should I pass something to it?
You need to set the dependent keys of your computed property, in your case content.posts.length. So the postCount knows when need to be updated.
App.IndexItemController = Ember.ObjectController.extend({
postCount: function() {
var posts = this.get('content').get('posts');
return posts.get('length');
}.property('content.posts.length')
});
Now your computed property is correct, but no data is loaded, this happen because there isn't posts associated with your users, no in the user -> post direction. So you need to add it in the fixture:
App.User.FIXTURES = [
{
id: 1,
name: 'Jon',
nick: 'Jonny',
posts: [1]
},
{
id: 2,
name: 'Foo',
nick: 'Bar',
posts: [2]
}
];
After this an error is raised Uncaught Error: Assertion Failed: You looked up the 'posts' relationship on '<App.User:ember280:1>' but some of the associated records were not loaded. Either make sure they are all loaded together with the parent record, or specify that the relationship is async (`DS.hasMany({ async: true })`).
Ember data identified that you have an async relationship, and warns you to setup the property with async: true
App.User = DS.Model.extend({
name: DS.attr('string'),
nick: DS.attr('string'),
posts: DS.hasMany('post', { async: true })
});
This your updated jsbin