transition after saving model of ember data - ember.js

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));
}
}

Related

"undefined" instead of ID in URL on transition in Ember and Ember Data

I have a pretty standard post model with a title and a text field. I have two routes for the model -- new route and show route. I want to create a post from new route and then transition to show route.
This is my router file
this.route('post-new', { path: '/posts/new' });
this.route('post-show', { path: '/posts/:postId' });
and submit action in post-new controller is something like this.
actions: {
submit() {
const { title, text } = this.getProperties('title', 'text');
let post = this.store.createRecord('post', {
title: title,
text: text
});
post.save().then(() => {
//success
this.transitionToRoute('post-show', post);
}, () => {
//error
});
}
}
So I am expecting this to redirect from http://localhost:4200/posts/new to something like http://localhost:4200/posts/23 (assuming 23 is id).
The save() is successful and record is created on the backend (which is rails) and I also see the post record updated in browser (it now has an ID) using Ember Inspector. But the redirection is happening to http://localhost:4200/posts/undefined.
How can I make this to redirect to something like http://localhost:4200/posts/23 after save ?
Btw, The versions are:
ember cli : 2.3.0-beta.1
ember : 2.3.0
ember data : 2.3.3
UPDATE
I was able to make it work by replacing this
this.transitionToRoute('post-show', post);
with this
this.transitionToRoute('/posts/' + post.id);
But I am hoping for a solution using the route name and not actual route path.
Try:
post.save().then(savedPost => {
//success
this.transitionToRoute('post-show', savedPost);
},
You can implement the serialize hook on your route.
serialize(model) {
return { postId: model.get('id') };
}
This will allow you to avoid calling the model hook if you already have the model. So, both of these will work as expected:
this.transitionToRoute('post-show', post); // this will NOT call model() hook
this.transitionToRoute('post-show', post.id); // this will call the model() hook
More information available in the API docs for Route.

Ember: How to cleanly replace model data and have progress indicators

I have a certain route that shows a list of projects, and it gets initial data from my RESTAdapter based on who the user is.
I am now implementing a search function that will issue a new API call so the user can get records besides the default ones for them, and the response should replace the model for that route. I have all that working, but I'm not sure how to do a loading or progress indicator (as the response from the database could potentially take 5-10 seconds depending on the amount of data). I know about loading substates, but in this case I'm not transitioning between routes. I just want to have at minimum a spinner so the user knows that it's working on something.
Would anyone that's done this before be willing to share how they handled a)replacing the model with new data, and b)keeping the user informed with a spinner or something?
Form action called when user clicks the Search button
searchProjects: function() {
var query = this.get('queryString');
if (query) {
var _this = this;
var projects = this.store.find('project', {q: query});
projects.then(function(){
_this.set('model', projects);
});
}
}
a) replacing the model with new data
You don't need to do anything. If you sideload records properly from the backend, Ember will automatically update them on the frontend.
b) keeping the user informed with a spinner or something
The loading substate is an eager transition. Ember also supports lazy transitions via the loading event.
You can use that event in order to display the spinner.
Here's an example from the docs:
App.ApplicationRoute = Ember.Route.extend({
actions: {
loading: function(transition, route) {
showSpinner();
this.router.one('didTransition', function() {
hideSpinner();
});
return true; // Bubble the loading event
}
}
});
UPD1
I need to do at least what I'm doing right? Setting the model to the response?
You need to reflect the search in the URL via query params. This will let the router automatically update the model for you.
what I would put in showSpinner to affect stuff on the page (like, can I use jQuery to show or hide a spinner element?), or show the actual loading substate.
I would set a property on that page's controller:
App.IndexRoute = Ember.Route.extend({
queryParams: {
search: {
refreshModel: true
}
},
model () {
return new Ember.RSVP.Promise( resolve => setTimeout(resolve, 1000));
},
actions: {
loading (transition, route) {
this.controller.set('showSpinner', true);
this.router.one('didTransition', () => {
this.controller.set('showSpinner', false);
});
return true;
}
}
});
App.IndexController = Ember.Controller.extend({
queryParams: ['search'],
search: null,
showSpinner: false,
});
Demo: http://emberjs.jsbin.com/poxika/2/edit?html,js,output
Or you could simply put the spinner into the loading template, which will hide obsolete data:
http://emberjs.jsbin.com/poxika/3/edit?html,js,output
Or you could put your spinner into the loading template:
Just in case others want to see, here's my working code based on #lolmaus's answers.
These Docs pages were helpful as well
Route's queryParams and Find method
Controller
//app/controllers/project.js
export default Ember.ArrayController.extend({
queryParams: ['q'],
q: null,
actions: {
searchProjects: function() {
var query = this.get('queryString');
if (query) {
this.set('q', query);
}
}
}
})
Route
export default Ember.Route.extend(AuthenticatedRouteMixin, {
model: function(params) {
if (params.q) {
return this.store.find('project', params);
} else {
return this.store.findAll('project');
}
},
queryParams: {
q: {
refreshModel: true
}
},
actions: {
loading: function(/*transition, route*/) {
var _this = this;
this.controllerFor('projects').set('showSearchSpinner', true);
this.router.one('didTransition', function() {
_this.controllerFor('projects').set('showSearchSpinner', false);
});
return true; // Bubble the loading event
}
}
});
My issue now is that when I use the parameter query, it works great, but then if I clear the query (with an action, to effectively "go back") then the records fetched by the query stay in the store, so when it does a findAll() I have both sets of records, which is not at all what I want. How do I clear out the store before doing findAll again?

Best Practice for Creating New Record with belongsTo Relationship

I am wondering about the best practice for creating a new record in Ember with createRecord() and then persisting it to the API? Specifically, should Ember's POST request generally be a single JSON that embeds all the model's relationships, or is it customary to POST each relationship individually?
In my code, I'm not able to get a single JSON, so I'm wondering if I'm missing the "Ember Way" or (more likely) I have a mistake in my code?
DETAILS:
Here are the details of my setup. I have two models:
/models/OrgUser.js:
DS.Model.extend({
...
orgPerson: DS.belongsTo('org-person', { inverse: 'org-user', async: true, embedded: 'always' }),
});
/models/OrgPerson.js:
DS.Model.extend({
...
orgUser: DS.belongsTo('org-user'),
})
I'm attempting to create a new user on the "Create New User" page. The route for that page is below. Is this the best place to call createRecord() for my new models?
/routes/org-users/add.js:
Ember.Route.extend({
model: function() {
var orgPerson = this.store.createRecord('org-person');
var orgUser = this.store.createRecord('org-user' );
orgUser.set('orgPerson', orgPerson);
return orgUser;
},
...
}
Using Chrome console to look at the orgUser object after I call set shows no evidence at all that I have added anything to orgUser. The "Ember" tab of Chrome Debug Tools does reflect the relationship, though.
On my "Create New User" page, my input fields all correspond to both OrgUser properties and OrgUser.OrgPerson properties. Here's an example:
/templates/org-users/add.hbs
...
{{input value=username}} // a property of OrgUser
{{input value=orgPerson.firstName}} // a property of OrgUser.orgPerson
...
In my route, when I go to save() Ember Data POSTs only the orgUser JSON with a null value for orgPerson. I'd like it to embed the orgPerson serialized object in the orgPerson property.
/routes/org-users/add.js:
Ember.Route.extend({
...
actions: {
submitForm: function() {
...
this.currentModel.save().then( onSuccess ).catch( onFailure );
...
}
}
});
This results in a POST request with the following body:
{
"orgUser":{
"username":"MyUsername",
"orgPerson":null,
"id":null
}
Note that orgPerson is null. Thanks in advance for any assistance!
UPDATE: Once again, I think I will need to take a fresh look at my serializer. Here's how it's currently defined.
/serializers/application.js:
DS.RESTSerializer.extend({
// Use the default approach to serializing, but add the id property
serialize: function(record, options) {
var json = this._super.apply(this, arguments);
json.id = record.id;
return json;
},
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].serialize( { includeId: true } ) : null;
}
else {
json[key] = data[relationship.key] ? data[relationship.key].get('id') : null;
}
if (relationship.options.polymorphic) {
this.serializePolymorphicType(record, json, relationship);
}
}
});
Per #Kingpin2k's comment, there appears to be some ambiguity (and bugs!) on how best to handle serialize() for a belongsTo relationship. My serializer customization above works great for records that are obtained through this.store.find(), but now I need to enable them for createRecord(). Additional suggestions, pointers are welcome!
It's a bug. https://github.com/emberjs/data/issues/1542#issuecomment-49443496
A workaround is to get the async belongsTo record before attempting to save (It tricks Ember Data into initializing it). In your case you could do it in the model hook.
model: function() {
var orgPerson = this.store.createRecord('org-person');
var orgUser = this.store.createRecord('org-user');
orgUser.set('orgPerson', orgPerson);
return orgUser.get('orgPerson').then(function(){
return orgUser;
});
},
So, I finally figured this out. With the release of Ember-Data-1.0.0-Beta.9, http://emberjs.com/blog/2014/08/18/ember-data-1-0-beta-9-released.html, the EmbeddedRecordsMixin has been introduced. This pretty much solves all my issues!
So, I wound up doing the following:
Upgraded to Ember-Data-1.0.0-Beta.9
Deleted my serializeBelongsTo customization from my serializer
I now define a custom serializer for each model using the EmbeddedRecordsMixin as documented at http://emberjs.com/api/data/classes/DS.EmbeddedRecordsMixin.html.
This wound up working perfectly, because I get full declarative control over how and when my records are embedded.
Special thanks to #Kingpin2k for helping me realize my serializer was the problem and for the discussion to help me understand the options.

Set a different controller's model via action doing a fetch query

I am trying to set the content of an another controller but my model returns undefined. I've tried everything I could think of to get the queried results prior to trying to set the other controller's model.
Mapmaker.CategoriesController = Ember.ArrayController.extend({
needs: ['filters'],
actions: {
setCategories: function(item) {
var content = this.getFilters(item.id);
console.log(content.fulfillmentValue._data.objects);
this.get("controllers.filters").set('model', content.fulfillmentValue._data.objects);
}
},
getFilters: function(id){
//trying to force sync
return Mapmaker.Tile.fetch('?categories=' + id);
}
});
Any thoughts? let me know if I need to show more code.
I am using ember-model's restful adapter to query the results.
I am getting results but they are just isLoaded:false the second I try to set the controller's model.
fetch in ember model returns a promise, not a model, use the promise
var promise = this.getFilters(item.id);
promise.then(function(content){
console.log(content);
this.get("controllers.filters").set('model', content);
}

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