What is the Ember way to do the following?
App.IndexController = Ember.Controller.extend({
actions: {
change: function(){
var model = this.get('model');
model[0] = true;
this.set('model', model);
}
}
});
I want to toggle an element (index 0 in this example) in model.
Here is the jsbin: http://emberjs.jsbin.com/doyejipagu/1/edit. The change to the model is not being reflected.
The solution is to use replace to modify the array:
change: function(){
this.get('model').replace(0, 1, [true]);
}
See http://emberjs.com/api/classes/Ember.MutableArray.html#method_replace. The above means "starting at position 0, replace 1 element, with the single element true". replace notifies Ember that the array contents have changed, so it is reflected everywhere.
It would be nice if there were a replaceAt API, allowing us to just say model.replaceAt(0, true), but there's not. Of course, you could write your own:
Ember.MutableArray.reopen({
replaceAt: function(pos, val) {
return this.replace(pos, 1, [val]);
}
});
The problem with your code is that nothing alerts Ember to the fact that the internal values of model have changed. model[0] = true triggers nothing. Your this.set('model', model) does not change the value of the model property itself; so neither does it trigger any observers or bindings.
You could also create a new array (here using slice), which would work:
var model = this.get('model').slice();
model[0] = true;
this.set('model', model);
Now, Ember sees that model has changed, and does all its magic.
What you try to do is not possible. A model either has to be an object or an array of objects otherwise you cannot set properties on it.
So you could do for example:
App.IndexRoute = Ember.Route.extend({
model: function() {
return [
Ember.Object.create({value: false}),
Ember.Object.create({value: true}),
Ember.Object.create({value: false})
];
}
});
App.IndexController = Ember.Controller.extend({
actions: {
change: function(){
this.get('model')[0].toggleProperty('value');
}
}
});
Related
I have a store with "row" and "column" models (tr and td in the html), buttons add rows and columns into rows.
I want to save the changes to server only when a "save" button is pressed.
At this time have done this code, it nearly work, but I have some problem with the "save" method :
The "id" of row and columns came from server when they are saved, but when a row is saved it don't know allready the columns id, and visversa. So that the save method is buggy and the code not nice at all.
I beggin with Ember, certainly there is a better way to do that ?, thank you if you can give me some help.
May be also something could be better done in my addRow and addColumn methods ?
Ember 1.11 with restAdapter
App.IndexRoute = Ember.Route.extend({
model: function () {
return this.store.find('row');
},
setupController: function(controller, model) {
controller.set('model', model);
}
});
App.Row = DS.Model.extend({
titre: DS.attr('string'),
columns: DS.hasMany('column', {async: true, embedded: 'always'}),
});
App.Column = DS.Model.extend({
titre: DS.attr('string'),
row: DS.belongsTo('row', {async: true}),
});
App.RowSerializer = DS.RESTSerializer.extend({
serializeHasMany: function(record, json, relationship) {
var hasManyRecords, key;
key = relationship.key;
hasManyRecords = Ember.get(record, key);
if (hasManyRecords && relationship.options.embedded === "always") {
json[key] = [];
hasManyRecords.forEach(function(item, index) {
json[key].push(item.get('id'));
});
}else{
this._super(record, json, relationship);
}
}
});
App.IndexController = Ember.Controller.extend({
selectedColumn: null,
actions: {
save: function(){
var row = this.store.all('row');
row.forEach(function(r, index, self){
r.save().then(function(r2){
r2.get('columns').forEach(function(c){
c.set('row',r2);
c.save().then(function(){
r.save();
})
})
})
})
},
clickCol: function(column){
this.set('selectedColumn', column)
},
addRow: function(){
_this = this;
var newRow = _this.store.createRecord('row',{titre:'Titre row'});
var newColumn = _this.store.createRecord('column', {
titre: 'Titre colonne',
})
newRow.get('columns').addObject(newColumn);
},
addColumn: function(){
_this = this;
this.get('selectedColumn').get('row').then(function(r){
var newColumn = _this.store.createRecord('column', {
titre: 'Titre colonne',
row: r
})
})
}
}
})
EDIT
I did find this :
DS.RESTAdapter: Robust Support for Parent->Child Hierarchies
where there is among other writted : "Allow parent and child records to be saved in the same commit", "[RESTAdapter] Allow new parent, child to be saved at once"...
witch seems to be something like I'm looking for, but can't find anywhere how to make it work ?
also this :
Client-Side IDs with Ember Data
witch say "Consider the case where the user creates a post with several attachments. You’ll need to make sure that the post is saved and has its ID resolved before attempting to save the child models in order to preserve this relationship"...
I have an ArrayContoller on which I want to set a boolean property based on the properties of its contents.
Plain-language description of the logic:
If the array contains any items with a property of isRetired equal to true, set the retiredShoes property of the ArrayController to true, otherwise, set the ArrayController retiredShoes property to false.
It seems like this should be a simple matter, but I haven't found a solution anywhere, and I'm still pretty new at this.
I'll put together a jsfiddle if necessary.
Here are the controllers for the array and the object:
App.ApplicationController = Ember.ArrayController.extend({
sortProperties: ['title'],
itemController: 'shoe',
retiredShoes: function() {
//how do I compute this sucker?
}
});
App.ShoeController = Ember.ObjectController.extend({
needs: ['application'],
actions: {
delete: function() {
var shoe = this.get('model'),
runs = shoe.get('runs');
shoe.deleteRecord();
shoe.save();
},
toggleRetired: function() {
var shoe = this.get('model');
shoe.toggleProperty('isRetired');
shoe.save();
}
}
});
Off top of my head, without jsbin. If there's a problem/bug, drop me a comment and I'll look it over again.
App.ApplicationController = Ember.ArrayController.extend({
retiredShoes: function() {
return this.get("model").isAny("isRetired", true);
}.property("model.#each.isRetired")
});
I have a controller handling a list of models. These models are of two different types (e.g. Message and Comment). In order to use an ArrayController I would have to merge both lists into one. Is there a way to do this ?
Class-based polymorphism, as proposed in this thread, would solve my problem, but they are not likely to be implemented soon.
In my current solution, I use an ObjectController reveiving both comments and messages. I then merge them using a computed property:
App.SomeRoute = Ember.Route.extend({
model: function (params) {
return Em.Object.create({
comments: this.store.find('comment'),
messages: this.store.find('message'),
});
},
});
App.SomeIndexController = Ember.ObjectController.extend({
merged: Em.computed.union('messages', 'comments'),
});
It works, but I don't benefit from all the niceties of an ArrayController (like sortProperties for example).
What I would like to do is something like:
App.SomeRoute = Ember.Route.extend({
model: function (params) {
var comments = this.store.find('comment');
var messages = this.store.find('message');
return merge(comments, messages);
},
});
where merge returns something similar to what is returned by this.store.find('model').
I asked a similar question recently, here is how I solved the issue.
App.SomeIndexController = Ember.ObjectController.extend({
sortProperties: ['some field'],
sortAscending: false, // false for descending
merged: function() {
var comments = this.get('comment') || [], // This gets wherever you've stored the comments array
messages = this.get('message') || [];// This gets wherever you've stored the messages array
var stream = Ember.A();
stream.pushObjects(comments.toArray());
stream.pushObjects(messages.toArray());
return Em.ArrayProxy.createWithMixins(Ember.SortableMixin, {
content: stream,
sortProperties: this.sortProperties,
sortAscending: this.sortAscending
});
}.property('messages.#each', 'comments.#each')
});
Hope this works for you as well. Just an FYI, for my example, my controller is actually one that is rendered, so I do not set up the model for it in the route. I simply have properties on my controller, lets say, commments and messages that constantly updated themselves as RecordArrays.
So for your example you may need to observe .property('model.messages.#each', 'model.comments.#each')
Inspired by #bmeyers' answer, and after exploring ember-data's source a little bit, I came up with a solution that is reusable and not too terrible. It is probably not optimal, but it does the work.
App.Store = DS.Store.extend({
findMultiple: function (types) {
var self = this;
var recordsByType = types.map(function (type) {
return self.find(type);
});
return self.mergeArrayPromises(recordsByType);
},
mergeArrayPromises: function (promises) {
var promise = Ember.RSVP.all(promises).then(function(arrays) {
var mergedArray = Ember.A();
arrays.forEach(function (records) {
mergedArray.pushObjects(records.toArray());
});
return mergedArray;
});
return DS.PromiseArray.create({
promise: promise,
});
},
});
App.SomeRoute = Ember.Route.extend({
model: function (params) {
return this.store.findMultiple(['comment', 'message']);
},
});
This might help. I stumbled upon this a while back and your question reminded me
https://gist.github.com/sebastianseilund/6096696
I wanna create a property that depends on a global attribute:
App.Test= Em.Object.extend();
App.Test.reopenClass({ all: Em.A() });
App.Other = Em.object.extend({
stuff: function() {
return "calculated stuff from this.get('foo') and App.Test.all";
}.property('foo', 'App.Test.all.#each.bar')
});
As a workarround I could create a observer and always set a dummy property with a new random value to trigger the property change, but is there a better way to do this?
I need this for some caching. I've a really crazy, and single threaded backend. So I write my own Model classes. So I try to reimplement a bit of the logic in the client for a better caching.
Ive an Item class (App.Item) and another class where each instance has a calculated reduced list of Items.
App.Model = Em.Object.extend({
});
App.Model.reopenClass({
all: Em.A(),
load: function(hash) {
return this.get('all').pushObject(this.create(hash));
}
});
App.Item = App.Model.extend({
});
App.List = App.Model.extend({
loadedInitItems: false,
items: function() {
if(!this.get('loadedInitItems')) { this.set('loadedInitItems', true); Backend.call('thelist', function(item) { App.Item.load(this); }); }
return App.Item.all.filter(function(item) {
// heavy filter stuff, depends on a lot of propertys on the current list instance
});
}.property('someprops', 'App.Item.all.#each.foo')
});
Backend.call represents some AJAX stuff
the point is, that now any item could change so that the filter will return something diffrent. And there are other places om the application, where the user can add Items. I dont want to call the backend again, because its very slow! And I know that the backend will not modify the list! So I wanna cache it.
This is just a reduced example of my use case, but I think've described the point. In reallity I have this dozend of times, with over 25000 objects.
have you tried adding 'Binding' to your property and then the value you want to bind to ?, something like this:
App.PostsController = Em.ArrayController.extend({
nameOfYourVariableBinding: "App.SomeObject.propertyYouWantToBindTo"
})
It looks like the problem is the double uppercase letter. So App.test ist working, but not App.Foo.test.
But I was able to find a Solution with the ArrayProxy.
Its about this:
App.Model = Em.Object.extend({
});
App.Model.reopenClass({
all: Em.A(),
load: function(hash) {
return this.get('all').pushObject(this.create(hash));
}
});
App.Item = App.Model.extend({
});
App.List = App.Model.extend({
loadedInitItems: false,
items: function() {
var self = this;
if(!this.get('loadedInitItems')) {
this.set('loadedInitItems', true);
Backend.call('thelist', function(item) {
App.Item.load(this);
});
}
return Em.ArrayProxy.extend({
content: App.Item.all,
arrangedContent: function() {
return this.get('content').filter(function(item) {
// heavy filter stuff, depends on a lot of propertys on the current list instance
// use self.get('someprops')
})
}.property('content.#each.foo')
});
}.property('someprops')
items: function() {
if(!this.get('loadedInitItems')) { this.set('loadedInitItems', true); Backend.call('thelist', function(item) { App.Item.load(this); }); }
return App.Item.all.filter(function(item) {
// heavy filter stuff, depends on a lot of propertys on the current list instance
});
}.property('someprops', 'App.Item.all.#each.foo')
});
My controller has a computed property:
App.IndexController = Ember.ArrayController.extend({
grandTotal: function () {
return this.getEach('total').reduce(function(accum, item) {
return accum + item;
}, 0);
}.property('#each.total'),
});
but I'm having trouble accessing it with my view. Here's my view:
App.SummaryView = Ember.View.extend({
templateName: 'summary',
companiesChanged: function() {
Ember.run.once(this, 'logCompanies');
}.observes('controller.#each'),
logCompanies: function() {
console.log(this.get('controller').get('model').get('length'));
console.log(this.get('controller').get('grandTotal'));
}
});
.get('length') returns correctly, so I know when this is called the models are loaded. But grandTotal is coming back as NaN, even though I know it's coded correctly since it's being rendered in the template. I need to access it within my view for additional reasons.
Any ideas?
Even though the controller's computed property changes with #each.total, the view only cares about the controller's property. Thus, the view was wrongly observing #each model, when it should have just been observing controller.grandTotal:
App.SummaryView = Ember.View.extend({
templateName: 'summary',
companiesChanged: function() {
Ember.run.once(this, 'logCompanies');
}.observes('controller.grandTotal'),
logCompanies: function() {
console.log(this.get('controller').get('model').get('length'));
console.log(this.get('controller').get('grandTotal'));
}
});