Correct clean up code - ember.js

I have the following two routes for edit and new:
WZ.ExercisesNewRoute = Em.Route.extend
model: ->
WZ.Exercise.createRecord()
deactivate: ->
#_super.apply this, arguments
#get('currentModel.transaction').rollback()
WZ.ExercisesEditRoute = Em.Route.extend
model: (params) ->
WZ.Exercise.find(params.exercise_id)
serialize: (params, options) ->
exercise_id: params.get('id')
deactivate: ->
#_super.apply this, arguments
tx = #get('currentModel.transaction')
tx.rollback() if tx
I would like to know what the correct code should be in each deactivate so the store is in a correct state if the user does not save, does save or whatever.
Currently if I route to the edit route and then directly to the new route without saving, I get the following error:
Uncaught Error: Attempted to handle event willSetProperty on
while in state rootState.deleted.saved.
Called with {reference: [object Object], store: ,
name: name}

This question is for an older version of ember data, but answer would have been to first check the state for isDeleted and only rollback if the record is not already deleted.
In the newer ember data there is no concept of a transaction, but you can still run into a similar issue if you are trying to rollback a record that is not yet persisted.
I would probably do this in the routers willTransition event as you can do things like abort the transition if you want to give the user the option to save the changes.
willTransition: function(transition) {
controller = this.get('controller')
if( controller.get('content.isDirty') ) {
if( controller.get('content.isNew') && confirm('Closing this window will revert all unsaved changes.') ){
controller.get('content').deleteRecord();
} else {
controller.get('content').rollback()
}
}
}

Related

Reload from save() response with Ember-Data

I'm currently using EmberJs along with Ember-Data to build an app backed by a Laravel JSON api.
I got a little issue on the saving process, mostly on model creation.
Here is my workflow :
The Ember ObjectController saves itself this.get("model").save()
Laravel (REST api) receives the data and persist it, therefore
creating a unique ID for it
The api return the new data (that
respect Ember convention) with the proper ID
???? Ember-Data doesn't
seems to care about the response since it does nothing...
The problem here : the id remains undefined even if it has been given...
The workaround I found is to reload models... but it's a big performance flaw considering that the data I want to be reloaded it available to Ember straight after the save()
any ideas ?
EDIT **
The problem only occurs on the first object that I add. When I repeat the process the next objects are refreshed correctly. If I hit refresh, it start again : the first object miss his refresh and the following are okay.
Here my code about the add process :
route
App.CategoriesNewRoute = Ember.Route.extend({
model: function()
{
return this.store.createRecord("category").set("active", true);
},
setupController: function(ctrl, model)
{
ctrl.set("errors", 0);
ctrl.set("model", model);
}
});
I don't have any Router for CategoriesRoute since the data is all in my ArrayController from the start.
controller
App.CategoriesNewController = Ember.ObjectController.extend({
needs: "application",
actions:
{
save: function()
{
this.get("model").save();
this.get("target").transitionTo("categories");
},
cancel: function()
{
this.get("model").rollback();
this.get("target").transitionTo("categories");
}
}
});
EDIT ** 2
I tried the code provided below with no success...
I added 2 records, and the first don't have it's ID... the second got it, so the problem appears to only be on the first save...
Here are the 2 responses I got from my API
ObjectA
{"category":{"nameFr":"aaa","nameEn":"aaa","active":true,"id":10}}
ObjectB
{"category":{"nameFr":"bbb","nameEn":"bbb","active":true,"id":11}}
It could be because you're transitioning before the save finishes, and so the model hook on the categories route fires while the model you're saving is still in flight (are you getting any errors in the console?). Try changing the save action to
save: function()
{
var that = this;
this.get("model").save().then(function(){
that.get("target").transitionTo("categories");
});
},
Also, you don't need to this.get('target')... as there's a controller method transitionToRoute. You can simplify to:
save: function()
{
var that = this;
this.get("model").save().then(function(){
that.transitionToRoute("categories");
});
},
Found that the problem seems to be on Ember-Data's side...
documented the whole thing here :
http://discuss.emberjs.com/t/missing-id-on-first-save-on-a-new-object/4752

Transitioning when encountering an error in Ember

I am trying to do this.transitionTo('route name') in the ApplicationRoute error action.
What seems to happen is when I click on the link, it hits the error, and doesn't transition to the target route, it seems to say it's transitioning to the route it was just in again. This is using {{#link-to}} so I have a feeling there is some magic going on in there...
I am trying to use this example.
The error is happening in the model() method on the route (which is using jquery and returning a promise [jqXHR]) because I am returning a 401 http code.
Is this not correct?
App.ApplicationRoute = Ember.Route.extend(
actions:
error: (error, transition) ->
if error.status == 401
#transitionTo('login')
)
Another thing I've tried is setting the .ajaxError method and transitioning in there but the result seems to not transition either.
App.ApplicationRoute = Ember.Route.extend(
setupController: (controller, model) ->
route = #
Ember.$(document).ajaxError((event, jqXHR, ajaxSettings, error) ->
if jqXHR.status == 401
route.transitionTo('login')
)
controller.set('content', model)
)
Has anyone got this working on ember 1.2.0?
Try returning true in your error hook, to bubble the error event.
From the documentation (http://emberjs.com/guides/routing/asynchronous-routing/#toc_when-promises-reject):
actions: {
error: function(reason) {
alert(reason); // "FAIL"
// Can transition to another route here, e.g.
// this.transitionTo('index');
// Uncomment the line below to bubble this error event:
// return true;
}
}
Turns out it was an issue on my end. I had some extra logic that was essentially countering my transition! Whoops!

Navigating to Record works, but deep link throws error?

I have a simple EmberJS application with 2 simple models (ember-model). Accounts and Items, while an Account hasMany Items.
So when i navigate to #/accounts/1/items with the links in the application it works perfectly fine. However when i directly reload #/accounts/1/items i get an error:
Assertion failed: The value that #each loops over must be an Array. You passed <App.Account:ember335> (wrapped in (generated items controller)) ember.js?body=1:382
Uncaught TypeError: Object [object Object] has no method 'addArrayObserver' ember.js?body=1:19476
Assertion failed: Emptying a view in the inBuffer state is not allowed and should not happen under normal circumstances. Most likely there is a bug in your application. This may be due to excessive property change notifications. ember.js?body=1:382
This is how my App looks like:
App.Router.map ()->
#resource 'accounts', ->
#resource 'account', path: ':account_id', ->
#resource 'items'
App.AccountRoute = Ember.Route.extend
model: (params) ->
App.Account.find(params.account_id)
App.ItemsRoute = Ember.Route.extend
model: ->
#.modelFor('account').get('items')
App.Account = Ember.Model.extend
name: Ember.attr('string')
item_ids: Ember.attr(),
items: (->
App.Items.find(#.get('comment_ids'))
).property('comment_ids')
App.Item = Ember.Model.extend
name: Ember.attr('string')
Controllers are standard (empty).
In the JS console a call like this works fine and returns the correct results, even after the error is thrown (and nothing rendered):
App.Account.find(1).get('items')
I have no idea why this is happening and the code seems so straight forward that its really annoying not to have a clue. Anyone has an idea?
I am no ember-data expert, but it seems that it is returning a promise. Therefore you should try:
App.ItemsRoute = Ember.Route.extend({
model : function(){
var accountPromise = App.Account.find(1);
var itemsPromise = Ember.Deferred.create();
accountPromise.then(function(account){
itemsPromise.resolve(account.get("items"));
});
return itemsPromise;
}
});
Why does it have to be it that way?
App.Account.find(1); performs an asynchronous call and therefore returns a promise.
That's why you can't immediately return the items, you have to wait for accountPromise to be fulfilled.
You return a new promise (itemspromise) which gets fulfilled when the accountPromise gets fulfilled.
Because you return a Promise, Ember waits for it to be fulfilled and uses the result as the model for your Controller.
PS: Actually this seems a little bit complicated to me. I thinks this will work, but there might be a more elegant solution.

getting error when trying to update a model inside a ObjectController in Ember

the page load starts at when a user is viewing a user's profile. And he makes some action and my code does ajax call to update it's user type-
App.UserController = Ember.ObjectController.extend
convert: ->
$.get '/api/user/convert_to_client/' + id, (response) ->
self.set('user_type', response.user.user_type)
But whenever i go to the user listing table and Ember tries to fetch all the users from the UsersRoute:
module.exports = App.UsersRoute = Em.Route.extend
model: ->
App.User.find({}).then (response) ->
self.controllerFor('users').set('content', response)
Then i end up getting all errors similar to these:
Error Attempted to handle event `loadedData` : Object not updated after deleteRecord
Update ember-data model\
I think this article here explains the issue -
http://www.thomasboyt.com/2013/05/01/why-ember-data-breaks.html
Uncaught Error: Attempted to handle event loadedData on while in
state rootState.loaded.updated.uncommitted. Called with {}
What this means is that you're trying to do something to the record
that it can't do in its current state. This often happens when trying
to update a record that's currently saving or when trying to render a
property on an object that's been deleted.
But note that when its the other way around, I first go to the user listing table, and then go and view a user's profile, update the user - this error never comes out.
Edit:
Sample response from users:
{
users: [
{
_id: 521e1112e8c5e10fb40002a0
..
}
]
}
and for a single user:
{
user: {
_id: 521e1116e8c5e10fb40004ca
}
}
You should set up your controller in the `setupController' hook like this:
model: function() {
return App.User.find({});
},
setupController: function(controller, model) {
controller.set('content', model);
}
Not sure if that's the only problem though. Could you post more information on the error you are getting?
You should use store.update to update the record in the store without changing the record's state (instead of using set on the record) e.g.
$.get '/api/user/convert_to_client/' + id, (response) =>
#store.update 'user', id: id, user_type: response.user.user_type
Or, if you structure your response correctly, just:
#store.update 'user', response.user
Note: update may not be available in older versions of EmberData

How to rollback relationship changes in EmberData

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