I've this code (http://jsfiddle.net/stephane_klein/gyHmS/2/) :
App = Ember.Application.create({});
App.Item = Ember.Object.extend({
title: null,
parent: null
});
App.MyList = Ember.Object.extend({
title: null,
content: [],
changed: function() {
console.log('here');
}.observes('content')
});
App.list = App.MyList.create({
title: "foobar",
content: [
App.Item.create({
item: "item1"
}),
App.Item.create({
item: "item2"
})
]
});
console.log(App.list.content);
App.list.content.pushObject(
App.Item.create({
item: "item3"
})
);
console.log(App.list.content);
Why "console.log('here')" is never called ?
I want set App.Item.parent when App.Item is inserted in App.MyList. I don't know how to observe App.MyList.content field.
Thanks for your help.
Best regards,
Stephane
You're not changing the content property, you're just pushing an object in there.
You have two solutions:
You can observe each item of the content (using .observes('content.#each')), but take attention, the method could be called several times
Or manually notify that this property has changed (using this.notifyPropertyChange('content'))
Here is the first solution: jsfiddle using #each
And here is the second solution: jsfiddle using notifyPropertyChange
You also have to notice you should not use directly App.list.content but App.list.get('content') instead. Take a look at this article written by Roy Daniels if you want more information.
EDIT
Please notice the use of #each has slightly changed. The Ember.Array##each documentation says:
Returns a special object that can be used to observe individual
properties on the array. Just get an equivalent property on this
object and it will return an enumerable that maps automatically to the
named key on the member objects.
If you merely want to watch for any items being added or removed to
the array, use the [] property instead of #each.
Lets see that with an example:
App.Post = Ember.Object.extend({
createdAt: null
});
App.Blog = Ember.Object.extend({
posts: null,
init: function() {
this._super();
this.set 'posts', [];
},
newerPost: function() {
return this.get('posts').sortBy('createdAt').get('firstObject');
}.property('posts.#each.createdAt'),
postsCount: function() {
return this.get('posts.length');
}.property('posts.[]')
});
newerPost needs to observes a particular property of each posts, whereas postsCount just needs to know when the posts array changes.
Related
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')
I'm struggling with limiting a data set represented by an ArrayController that also relies upon an ObjectController as a decorator for actions and computed properties. When I define a computed 'results' property and have it return either 'content' or 'arrangedContent', it seems my ObjectController (itemController) is entirely bypassed, and no references to the 'shipment' model are included.
Route:
App.ShipmentsManifestsRoute = Ember.Route.extend({
model: function() {
return this.store.find('manifest');
}
})
Models:
App.Shipment = DS.Model.extend({
from: DS.attr("string"),
tracking: DS.attr("string"),
manifest: DS.hasMany("manifest", { async: true }),
received: DS.attr("number", {
defaultValue: function() {
return moment(Firebase.ServerValue.TIMESTAMP);
}
})
});
App.Manifest = DS.Model.extend({
upc: DS.attr("string"),
quantity: DS.attr("number", { defaultValue: 1 }),
condition: DS.attr("string", { defaultValue: 'New' }),
status: DS.attr("string", { defaultValue: 'Accept' }),
title: DS.attr("string"),
notes: DS.attr("string"),
shipment: DS.belongsTo("shipment", { async: true }),
});
Controllers:
App.ManifestController = Ember.ObjectController.extend({
actions: {
save: function() {
this.get('model').save();
}
},
receivedDate: function() {
return moment(this.get('shipment.received')).format('YYYY-MM-DD');
}.property('shipment.received')
})
App.ShipmentsManifestsController = Ember.ArrayController.extend({
itemController: 'manifest',
sortProperties: ['shipment.received'],
sortAscending: false,
results: function() {
return this.get('arrangedContent').slice(0, 10);
}.property('arrangedContent.[]')
})
Also worth noting is that my itemController actions essentially don't seem to exist when using 'results' to render my data set. I have some inline editing functionality baked in that calls the 'save' action on the itemController, and Ember is throwing an error that 'save' doesn't exist.
This all of course works as it should if I iterate over {{#each controller}} rather than {{#each results}}.
I guess ultimately the problem is that 'content' doesn't return all of the data elements / properties / computed properties that would otherwise be available on the controller.
Is there a best practice way around this limitation?
UPDATE:
The problem is definitely related to the missing itemController when referencing arrangedContent. When iterating over the ArrayController directly, my View is referencing App.ManifestController as the controller. However, when iterating over arrangedContent, my View is instead referencing App.ShipmentsManifestsController as the controller. Still unsure as to why that is.
UPDATE 2:
Based on this, it looks like my issue is a duplicate of Setting itemController on a filtered subset of an ArrayController's model
A work-around that involves additional handlebars parameters was offered, which I will try. But would still love any input on whether this is intended behaviour or a bug.
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 working on building up unit tests for our Ember applications. The current set of tests I'm targeting are our Models. I've got a set of tests that work really well for Models based on Ember Data, but seem to fail when based on Ember.Object. Here are two examples:
App.Person = DS.Model.extend({
First: DS.attr("string"),
Last: DS.attr("string")
});
App.Person2 = Ember.Object.extend({
First: null,
Last: null
});
And the test which passes for DS.Model:
it('has a valid attribute: First', function() {
var property = App.Person.metaForProperty('First');
expect( property.type ).to.eql('string');
expect( property.isAttribute ).to.eql(true);
});
Then, when using the same structure for Ember.Object:
it('has a valid attribute: First', function() {
var property = App.Person2.metaForProperty('First');
});
I get the following error:
Error: Assertion Failed: metaForProperty() could not find a computed property with key 'First'.
at new Error (native)
at Error.Ember.Error (http://0.0.0.0:3385/app/js/components/ember/ember.js:844:19)
at Object.Ember.assert (http://0.0.0.0:3385/app/js/components/ember/ember.js:73:11)
at Function.Mixin.create.metaForProperty (http://0.0.0.0:3385/app/js/components/ember/ember.js:13247:11)
at Context.<anonymous> (http://0.0.0.0:3385/tests/model-person-test.js:6:35)
at invoke (http://0.0.0.0:3385/tests/bower_components/ember-mocha-adapter/adapter.js:60:8)
at Context.suite.on.context.it.context.specify.method (http://0.0.0.0:3385/tests/bower_components/ember-mocha-adapter/adapter.js:102:13)
at Test.require.register.Runnable.run (http://0.0.0.0:3385/tests/assets/mocha.js:4200:15)
at Runner.require.register.Runner.runTest (http://0.0.0.0:3385/tests/assets/mocha.js:4591:10)
at http://0.0.0.0:3385/tests/assets/mocha.js:4637:12
Can anyone offer insight as to what might be going wrong?
Yeah, First/Last aren't computed properties on Person2, they are just properties.
This would work
App.Person2 = Ember.Object.extend({
First: null,
Last: null,
Blah: function(){
}.property('First')
});
var j = App.Person2.metaForProperty('Blah');
console.log(j);
When you do DS.attr() Ember Data is actually injecting a computed property right there, see: Ember Data attr Source
Here's an updated set of assertions that work for me. It looks like because it's only using Ember.Object, the extra goodies from DS.Model simply aren't there.
it('has a valid attribute: First', function() {
var person = App.Person.create({
id: 1,
First: 'Andy'
});
expect( App.Person.proto().hasOwnProperty('First') ).to.eql(true);
expect(typeof person.get('First')).to.eql('string');
expect(person.get('First')).to.eql('Andy');
});
I have a model:
app.ObjectOne = Em.Object.extend({
id: null,
count: null
});
And another model, which computed property 'SomeProperty' I want to depend on property 'count' from ObjectOne
app.ObjectTwo = Em.Object.extend({
id: null,
someProperty: function(){
return count+5;
}.property('app.SomeObjectController.count')
});
So, can I do it that way?
Or is there any other way to do it;
The main idea is that data of ObjectOne model comes after ObjectTwo, so I what to recompute property and rerender view
If I understand well, the behavior you want is exactly what Ember.js can bring to you.
First, here is a very basic template:
<script type="text/x-handlebars">
Here, the template is updated each time this property is updated (equivalent to bound property)
{{App.objectTwo.someProperty}}
</script>
Here is the javascript. I don't know if you really want to use an ObjectController here, but you mention it, so I have use it. An ObjectController's act as a proxy around it's content property. That means here that someObjectController.get('count') will try to get the count property from the controller itself, and if it does not exist, then the controller retrieve the count property from its content.
App = Ember.Application.create();
App.someObjectController = Ember.ObjectController.create();
App.ObjectOne = Em.Object.extend({
id: null,
count: null
});
App.objectOne = App.ObjectOne.create({
id: 1,
count: 42
});
App.someObjectController.set('content', App.objectOne);
App.ObjectTwo = Ember.Object.extend({
id: null,
someProperty: function(){
return App.someObjectController.get('count')+5;
}.property('App.someObjectController.count')
});
App.objectTwo = App.ObjectTwo.create({
id: 1
});
//in 3 seconds the count property is set to 0. You can see the template is updated
Ember.run.later(function(){
App.objectOne.set('count', 0);
},3000);
Here is the working jsfiddle: http://jsfiddle.net/Sly7/M3727/4/