I have an Ember app that displays a list of borrowed articles in a table. One table cell has a select helper that has either 'borrowed' or 'returned' as value.
I also have a checkbox that triggers the showing of returned items via query Parameters.
When I set my checkbox to not show returned items and set one item from 'borrowed' to 'returned', the article will stay visible.
So what I will have to do is reload the 'filteredResults' with the state change incorporated.
I read about Ember.observer but I'm not sure it's the right thing to use for this.
import Ember from 'ember';
export default Ember.Controller.extend({
queryParams: ['showReturned'],
showReturned: false,
possibleStates: ['borrowed', 'returned'],
filteredResults: Ember.computed('showReturned', 'model', function() {
const articles = this.get('model');
if (this.get('showReturned')) {
return articles;
} else {
return articles.filterBy('state', 'borrowed');
}
})
});
You need to watch the state attribute on each model with model.#each.state.
filteredResults: Ember.computed('showReturned', 'model.#each.state', function() {
const articles = this.get('model');
if (this.get('showReturned')) {
return articles;
} else {
return articles.filterBy('state', 'borrowed');
}
})
Along the same lines, if you wanted to just watch whether any articles were created or destroyed, you could watch the array - 'model.[]'.
You want Ember.computed (doc) because you are consuming the result as a property. You would use Ember.observer (doc) if there was an action you wanted to perform every time one of the states changed (such as automatically saving the model).
Related
I have an Ember app that uses server-side storage. In the products route, I have a list of products that I display in my product route (which are fetched in the usual way upon entering the route)
{{#each item in sortedProducts}}
{{/each}}
....
fetch
App.ProductRoute = Ember.Route.extend({
model: function(){
return Ember.RSVP.hash({
store: this.store.find('product'),
//other code ommitted
})
}
})
In the controller, I do the sorting
sortProperties: ['date:desc'] //dated added
sortedProducts: Ember.computed.sort('model', 'sortProperties'),
This works, however, I give the user the option to filter the records displayed. Upon clicking a button, an action is called that queries the server for a subset of records (it doesn't just filter the records that are already in the store cache)
actions: {
filterByPriceAndColor: function(){
this.store.find('product', {price: pricevariable, color: colorvariable});
}
}
This queries and returns the desired records, but the list on the page isn't updated i.e. the list on the page still displays all the records that are fetched upon application load.
Question: how do I get the page to update with the new records fetched from the server without a route change, (and will the solution integrate with the computed sort that already exists for ordering the entries by date)
To update your model from an action (or anywhere else) you simple need to set a new value for it and Ember will to the hard work for you.
In your case it should look like this:
actions: {
filterByPriceAndColor: function() {
var promise = this.store.find('product', {price: pricevariable, color: colorvariable});
var self = this;
promise.then(function(data) {
self.set('model', data);
});
}
}
Here is a JSBin demonstrating how it works: http://emberjs.jsbin.com/walunokaro/3/edit
I am attempting to save an Ember Data DS.Model after it's been updated, but when I call myModel.save(), I'm finding that Ember Data is sending the original, non-updated model instead of the updated one. I'm trying to understand why this is happening and what I need to do differently.
Here are some details. First, I have two models:
/models/OrgUser.js:
DS.Model.extend({
...
orgPerson: DS.belongsTo('org-person', { inverse: 'org-user', async: true, embedded: 'always' }),
});
Note that I am using a customized RESTSerializer (see below), so the only use of embedded: 'always' is how my custom RESTSerializer handles it.
/models/OrgPerson.js:
DS.Model.extend({
...
orgUser: DS.belongsTo('org-user'),
})
To persist these models, I'm using the RESTAdapter. In an attempt to generate a single JSON request to my API that contains both models above, I've made a single customization to the adapter. I don't think this is affecting anything, but just in case I'm missing something, here it is:
/serializers/application.js:
DS.RESTSerializer.extend({
serializeBelongsTo: function(record, json, relationship) {
var key = relationship.key;
key = this.keyForRelationship ? this.keyForRelationship(key, 'belongsTo') : key;
var data = record.get('data');
if (relationship.options.embedded && relationship.options.embedded === 'always') {
json[key] = data[relationship.key] ? data[relationship.key].get('data') : null;
}
else {
json[key] = data[relationship.key] ? data[relationship.key].get('id') : null;
}
if (relationship.options.polymorphic) {
this.serializePolymorphicType(record, json, relationship);
}
}
})
With that setup, I have a template where I update the orgPerson properties. I can confirm these are bound properties because updating their input updates their display on another part of the template in real-time. I then call an action on my controller, and within that action do the following:
/controllers/my-page.js:
export default Ember.ObjectController.extend( FormMixin, {
actions: {
submitForm: function() {
...
this.get('model') // Chrome console shows that _data.orgPerson._data.firstName has the (incorrect) old property
this.get('model').serialize() // returns (incorrect) old firstName
this.get('orgPerson.firstName') // returns (correct) updated firstName
this.get('orgPerson').get('firstName') // returns (correct) updated firstName
...
}
}
});
Any idea why I am getting two different versions of the same model? How can I serialize the correctly updated model? Thanks for any input!
SOLUTION:
Thanks (again!) to #kingpin2k, I have resolved this issue. Here are the steps I took:
My serializer was in fact the problem, and using Ember's old preserved data. I replaced the line data[relationship.key].get('data') with the line data[relationship.key].serialize() and this was fixed.
I then ran into another issue, which was that if I edited my record, did NOT save it, and then went back to my list of records, the list still showed the edit. My first thought was that I needed to update my list page's array model to show only the latest content, but there didn't appear to be any Ember facilities for this.
So I ultimately solved this by using the following code in my route. Note that because orgPerson is async: true I had to wrap my model in a promise. Note also that I had to directly call model.orgPerson versus just model.
Updated route:
actions: {
willTransition: function( transition ) {
this.controller.get('model.orgPerson').then( function( value ) {
if ( value.get('isDirty') ) {
value.rollback();
}
});
}
}
Going forward, I just want to call this.controller.get('model').rollback(), so I'm going to write a util function that traverses eachRelationship and then individually calls rollback() on any of the objects. Whew, a lot of subtlety to get this working right.
Ember Data stores the original values in the data obj. It stores modified values in _attributes obj. During a save it moves _attributes obj to inFlightAttributes obj, then after the save is complete it merges them from inFlightAttributes to data. All of this is so you can rollback your record.
When you define a property as attr it hooks up the magical get where it first checks _attributes, then inFlightAttributes, then data and returns that property's result.
function getValue(record, key) {
if (record._attributes.hasOwnProperty(key)) {
return record._attributes[key];
} else if (record._inFlightAttributes.hasOwnProperty(key)) {
return record._inFlightAttributes[key];
} else {
return record._data[key];
}
}
https://github.com/emberjs/data/blob/v1.0.0-beta.8/packages/ember-data/lib/system/model/attributes.js#L267
In your case, Ember Data doesn't know you are saving that record, and you are manually grabbing the old properties from the data obj. You'd either need to manually merge _attributes to data or trick Ember Data into thinking you'd saved it.
I am testing my application, so I am doing the following:
I show an index view (#/locators/index), of Locator objects, which I initially load with App.Locator.find();
I modify the backend manually
Manually (with a button/action) I trigger a refresh of the data in the ember frontend, without changing the route. I do this with App.Locator.find().then(function(recordArray) {recordArray.update();});. I see via console logging that a list request is sent to the backend, and that the up-to-date data is received. I assume this is used to update the store.
BUT: The view does not update itself to show this new data
Why does the view not get automatically updated when the store receives new data? Isn't that the whole point of the data binding in Ember?
If I now do the following:
Open any other route
Go back to the locators index route (#/locators/index)
Ember sends a new request to list the locators
The index view is shown, with the correct data (since it was already in the store?)
New data is received
(I am not 100% sure that 4 and 5 happen in that order, but I am quite certain)
So, my impression is that the data is properly updated in the store, but that somehow a full re-rendering of the view is needed to display this new data, for example by leaving and re-entering the route. Is this true? Can I force this re-rendering programmatically?
Ember changes view data when the underlying model is changed by the controller(Which is binded to the view)
(Only when the state of the application changes(url changes) router hooks are called)
Your problem could be solved when you do this.refesh() inside your route by capturing the action triggered by your view.
App.IndexRoute = Ember.Route.extend({
actions: {
dataChanged: function() {
this.refresh();
}
},
//rest of your code goes here
});
for this to work your handlebar template which modifies the data shoud have an action called dataChanged
example :
Assume this action is responsible for changing/modifying/deleting the underlying data
<button {{action 'dataChanged'}}> Change Data </button>
Refresh method actually does a model refresh and passes it to the corresponding controller which indeed changes the view.
There a couple of things that come to mind you could try:
If you are inside of an ArrayController force the content to be replaced with the new data:
this.replaceContent(0, recordArray.get('length'), recordArray);
Or try to call reload on every single record trough looping the recordArray:
App.Locator.find().then(function(recordArray) {
recordArray.forEach(function(index, record) {
record.reload();
}
}
And if the second approach works, you could also override the didLoad hook in your model class without having to loop over them one by one:
App.Locator = DS.Model.extend({
...
didLoad: function(){
this.reload();
}
});
If this works and you need this behaviour in more model classes consider creating a general mixin to use in more model classes:
App.AutoReloadMixin = Ember.Mixin.create({
didLoad: function() {
this._super();
this.reload();
}
});
App.Locator = DS.Model.extend(App.AutoReloadMixin, {
...
});
App.Phone = DS.Model.extend(App.AutoReloadMixin, {
...
});
Update in response to your answer
Handlebars.registerHelper is not binding aware, I'm sure this was causing your binding not to fire. You should have used Handlebars.registerBoundHelper or simply Handlebars.helper which is equivalent:
Handlebars.helper('grayOutIfUndef', function(property, txt_if_not_def) {
...
});
Hope this helps.
Somehow this seems to be due to the fact that I am using custom handlebar helpers, like the following:
Handlebars.registerHelper('grayOutIfUndef', function(property, txt_if_not_def) {
// HANDLEBARS passes a context object in txt_if_not_def if we do not give a default value
if (typeof txt_if_not_def !== 'string') { txt_if_not_def = DEFAULT_UNDEFINED_STR; }
// If property is not defined, we return the grayed out txt_if_not_def
var value = Ember.Handlebars.get(this, property);
if (!value) { value = App.grayOut(txt_if_not_def); }
return new Handlebars.SafeString(value);
});
Which I have been using like this:
{{grayOutIfUndef formattedStartnode}
Now I have moved to a view:
{{view App.NodeIconView nodeIdBinding="outputs.startnode"}}
Which is implemented like this:
App.NodeIconView = Ember.View.extend({
render: function(buffer) {
var nodeId = this.get('nodeId'), node, html;
if (nodeId) {
node = App.getNode(nodeId);
}
if (node) {
html = App.formattedLabel.call(node, true);
} else {
html = App.grayOut(UNDEFINED_NODE_NAME);
}
return buffer.push(html);
}
});
I am not sure why, but it seems the use of the custom handlebars helper breaks the property binding mechanism (maybe my implementation was wrong)
I want to make transition after a create a post.
post/new > click submit > rails backend successfully create post and response a json > redirect to newly created post's path
in ember_data_example github source code. they use this approach
transitionAfterSave: function() {
// when creating new records, it's necessary to wait for the record to be assigned
// an id before we can transition to its route (which depends on its id)
if (this.get('content.id')) {
this.transitionToRoute('contact', this.get('content'));
}
}.observes('content.id'),
It works fine, because The model has ID of null when model created, and its ID would change when model saving is successful because this function observes change of models ID.
But maybe, this function will be executed whenever model's ID property is changed.
I'm finding some more semantic way.
I want transition to be executed
when the model's status is changed to 'isDirty' = false && 'isNew' == true form 'isDirty' = true, 'isNew' = false.
How can I implement this?
Ideally, the id is not supposed to change. However, you are correct, semantically, this method doesn't seem right.
There is a cleaner way to do this:
save: function(contact) {
contact.one('didCreate', this, function(){
this.transitionToRoute('contact', contact);
});
this.get('store').commit();
}
UPDATE 2013-11-27 (ED 1.0 beta):
save: function(contact) {
var self = this;
contact.save().then(function() {
self.transitionToRoute('contact', contact);
});
}
Note for Ember 2.4 It is encoraged to handle saving actions in the component or route level (and avoid controllers). Here's an example below. Note the id on the model object in the transition. And note how we use transitionTo and not transitionToRoute in the route.
actions: {
save() {
var new_contact = this.modelFor('contact.new');
new_contact.save().then((contact) => {
this.transitionTo('contact.show', contact.id);
});
},
actions: {
buttonClick: function () {
Ember.debug('Saving Hipster');
this.get('model').save()
.then(function (result) {
this.transitionToRoute('hipster.view', result);
}.bind(this));
}
}
I have two models with parent-child relationship: training and exercise:
App.Training = DS.Model.extend({
exercises: DS.hasMany('App.Exercise')
})
App.Exercise = DS.Model.extend({
training: DS.belongsTo('App.Training')
})
I want to have a page where a training with all its related exercises is displayed. If the user presses the Edit button, the page becomes editable with the possibility of adding new exercises. I also want to have a Cancel button which discards all the changes made.
Here is my controller:
App.TrainingsShowController = Em.ObjectController.extend({
editing: false,
edit: function() {
this.set('editing', true);
transaction = this.get('store').transaction();
transaction.add(this.get('model'));
this.get('model.exercises').forEach(function(x){
transaction.add(x);
});
},
cancel: function() {
this.set('editing', false);
this.get('model.transaction').rollback();
},
save: function() {
this.set('editing', false);
this.get('model.transaction').commit();
},
addExercise: function() {
this.get('model.exercises').createRecord({});
}
})
There are four event handlers in the controller:
edit: The user pressed the Edit button: a transaction is created, the page is put into "Editing" mode.
cancel: The user pressed the Cancel button: transaction is rolled back and back to "Normal" mode.
save: The user pressed the Save button: transaction is commited and back to "Normal" mode.
addExercise: The user pressed the Add exercise button: a new exercise is created (in the same transaction) and added to the trainings.
The rollback functionality works fine except for newly created records: if I push the Edit button, add a new exercise and push the Cancel button, the newly created exercise stays on the page.
What is the best way to get rid of the discarded child record?
UPDATE:
I've created a jsFiddle to reproduce problem, but it worked. Unlike my application here I used DS.FixtureAdapter: http://jsfiddle.net/tothda/LaXLG/13/
Then I've created an other one using DS.RESTAdapter and the problem showed up: http://jsfiddle.net/tothda/qwZc4/5/
In the fiddle try: Edit, Add new and then Rollback.
I figured it out, that in case of the RESTAdapter when I add a new child record to a hasMany relationship, the parent record won't become dirty. Which seems fine, but when I rollback the transaction, the newly created child record stays in the parent's ManyArray.
I still don't know, what's the best way to handle the situation.
A proper dirty check and rollback for hasMany and belongsTo relationships are sorely lacking in Ember Data. The way it currently behaves is often reported as a bug. This is a big pain point for a lot of developers and there is an ongoing discussion on how to resolve this here:
https://github.com/emberjs/rfcs/pull/21
Until there's a proper solution in place, you can workaround this problem by using the following approach.
First, you'll want to reopen DS.Model and extend it. If you're using globals, you can can just put this (e.g. DS.Model.reopen({})) anywhere, but if you're using Ember CLI, it's best to create an initializer (e.g. ember g initializer model):
import DS from 'ember-data';
export function initialize(/* container, application */) {
DS.Model.reopen({
saveOriginalRelations: function() {
this.originalRelations = {};
this.constructor.eachRelationship(function(key, relationship) {
if (relationship.kind === 'belongsTo')
this.originalRelations[key] = this.get(key);
if (relationship.kind === 'hasMany')
this.originalRelations[key] = this.get(key).toArray();
}, this);
},
onLoad: function() {
this.saveOriginalRelations();
}.on('didLoad', 'didCreate', 'didUpdate'),
onReloading: function() {
if (!this.get('isReloading'))
this.saveOriginalRelations();
}.observes('isReloading'),
rollback: function() {
this._super();
if (!this.originalRelations)
return;
Ember.keys(this.originalRelations).forEach(function(key) {
// careful, as Ember.typeOf for ArrayProxy is 'instance'
if (Ember.isArray(this.get(key))) {
this.get(key).setObjects(this.originalRelations[key]);
this.get(key).filterBy('isDirty').invoke('rollback');
return;
}
if (Ember.typeOf(this.get(key)) === 'instance') {
this.set(key, this.originalRelations[key]);
return;
}
}, this);
},
isDeepDirty: function() {
if (this._super('isDirty'))
return true;
if (!this.originalRelations)
return false;
return Ember.keys(this.originalRelations).any(function(key) {
if (Ember.isArray(this.get(key))) {
if (this.get(key).anyBy('isDirty'))
return true;
if (this.get(key).get('length') !== this.originalRelations[key].length)
return true;
var dirty = false;
this.get(key).forEach(function(item, index) {
if (item.get('id') !== this.originalRelations[key][index].get('id'))
dirty = true;
}, this);
return dirty;
}
return this.get(key).get('isDirty') || this.get(key).get('id') !== this.originalRelations[key].get('id');
}, this);
}
});
};
export default {
name: 'model',
initialize: initialize
};
The code above essentially stores the original relationships on load or update so that it can later be used for rollback and dirty checking.
model.rollback() should now roll back everything, including hasMany and belongsTo relationships. We still haven't fully addressed the 'isDirty' check though. To do that, we need to override isDirty in the concrete implementation of a model. The reason why we need to do it here and we can't do it generically in DS.Model is because DS.Model doesn't know what property changes to watch for. Here's an example using Ember CLI. The same approach would be used with globals, except that you'd assign this class to something like App.Book:
import DS from 'ember-data';
var Book = DS.Model.extend({
publisher: DS.belongsTo('publisher'),
authors: DS.hasMany('author'),
isDirty: function() {
return this.isDeepDirty();
}.property('currentState', 'publisher', 'authors.[]', 'authors.#each.isDirty').readOnly()
});
export default Book;
For the dependent arguments of isDirty, make sure to include all belongsTo relationships and also include 'array.[]' and 'array.#each.isDirty' for every hasMany relationship. Now isDirty should work as expected.
This isn't pretty but you can force it to rollback by manually dirtying the parent record:
parent.send('becomeDirty');
parent.rollback();
parent.get('children.length'); // => 0
#tothda and other readers to follow. As of Ember Data : 1.0.0-beta.10+canary.7db210f29a the parent is still not designed to make parentTraining.isDirty() a value of true when a child is rolled back. Ember Data does consider a parent record to be dirty when an attribute is changed, but not when a DS.hasMany array has changes (this allows save() to work, so you can updated any changes to the parent's attributes on the server).
The way around this for the case mentioned, where you want to do a rollback() on a newly created child, is to replace the .rollback() with a .deleteRecord() on the child record you want to discard. Ember Data then automatically knows to remove it from the DS.hasMany array then, and you can pat yourself on the back for a rollback well done!
Late to the party, but here we go:
I created an addon that resolves this issue.
Just call rollbackRelationships() and it will rollback all your relationships (belongsTo & hasMany). Look at the README for more options.
https://www.npmjs.com/package/ember-rollback-relationships