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
Related
This is a similar question to this one, except this is for the latest versions of Ember and Active Model Serializers (0.10.2).
I have a simple Parent:Child relationship.
app/models/trail.js
import Ember from 'ember';
import DS from 'ember-data';
export default DS.Model.extend({
name: DS.attr(),
// relationships
employees: DS.hasMany('employee', { async: true }),
});
app/models/employee.js
import DS from 'ember-data';
import Person from '../models/person';
export default Person.extend({
status: DS.attr(),
statusCode: DS.attr(),
});
app/models/person.js
import Ember from 'ember';
import DS from 'ember-data';
export default DS.Model.extend({
avatarUrl: DS.attr(),
firstName: DS.attr(),
lastName: DS.attr(),
fullName: Ember.computed('firstName', 'lastName', function() {
return `${this.get('lastName')}, ${this.get('firstName')}`;
}),
});
When I create a new Trail, and select two employees for the 'hasMany', the following json arrives the server (from the Rails log):
{"data":
{"attributes":
{"name":"TEST3",
"gpx-file-url":"a url",
"distance-value":"5"},
"relationships":
{"employees":{"data":[]}}, "type":"trails"}}
My question is, what has happened to the employees? Where are the id's of the employees (they already exist both in the database and in the Ember Store - ie, I am not trying to create child records in this request).
EDIT
I just found this question, which explains that the id's for a hasMany relationship are not sent by Ember's JSONAPISerializer to the API - since the foreign key here actually has to be persisted in each child record. So essentially by 'selecting' employees, you need to save the fact that they now have a parent. So the selected employee records need to be persisted.
But my understanding was that this all works "out of the box" and that Ember would automatically fire a POST request to do this, but that seems to not be the case.
This then gets to the real question - how do I update those children?
UPDATE - BOUNTY ADDED AS THIS HAS QUESTION HAS EVOLVED
After further analysis, it became clear that a new model was required - Assignments. So now the problem is more complex.
Model structure is now this:
Trail
hasMany assignments
Employee
hasMany assignments
Assignment
belongsTo Trail
belongsTo Employee
In my 'new Trail' route, I use the fantastic ember-power-select to let the user select employees. On clicking 'save' I plan to iterate through the selected employees and then create the assignment records (and obviously save them, either before or after saving the Trail itself, not sure which is best yet).
The problem is still, however, that I don't know how to do that - how to get at the 'selected' employees and then iterate through them to create the assignments.
So, here is the relevant EPS usage in my template:
in /app/templates/trails/new.hbs
{{#power-select-multiple options=model.currentEmployees
searchPlaceholder="Type a name to search"
searchField="fullName"
selected=staff placeholder="Select team member(s)"
onchange=(route-action 'staffSelected') as |employee|
}}
<block here template to display various employee data, not just 'fullName'/>
{{/power-select-multiple}}
(route-action is a helper from Dockyard that just automatically sends the action to my route, works great)
Here is my model:
model: function () {
let myFilter = {};
myFilter.data = { filter: {status: [2,3] } }; // current employees
return Ember.RSVP.hash({
trail: this.store.createRecord('trail'),
currentEmployees: this.store.query('employee', myFilter).then(function(data) {return data}),
});
},
actions: {
staffSelected (employee) {
this.controller.get('staff').pushObject(employee);
console.log(this.controller.get('staff').length);
},
}
I only discovered today that we still need controllers, so this could be my problem! Here it is:
import Ember from 'ember';
export default Ember.Controller.extend({
staff: [] <- I guess this needs to be something more complicated
});
This works and I see one object is added to the array in the console. But then the EPS refuses to work because I get this error in the console:
trekclient.js:91 Uncaught TypeError: Cannot read property 'toString' of undefined(anonymous function) # trekclient.js:91ComputedPropertyPrototype.get # vendor.js:29285get #
etc....
Which is immediately follow by this:
vendor.js:16695 DEPRECATION: You modified (-join-classes (-normalize-class "concatenatedTriggerClasses" concatenatedTriggerClasses) "ember-view" "ember-basic-dropdown-trigger" (-normalize-class "inPlaceClass" inPlaceClass activeClass=undefined inactiveClass=undefined) (-normalize-class "hPositionClass" hPositionClass activeClass=undefined inactiveClass=undefined) (-normalize-class "vPositionClass" vPositionClass activeClass=undefined inactiveClass=undefined)) twice in a single render. This was unreliable in Ember 1.x and will be removed in Ember 3.0 [deprecation id: ember-views.render-double-modify]
So I imagine this is because the examples in the documentation just uses an array containing strings, not actual Ember.Objects. But I have no clue how to solve this.
So, I decided to throw away the controller (ha ha) and get creative.
What if I added a property to the Trail model? This property can basically be a 'dummy' property that collected the selected employees.
in /app/models/trail.js
selectedEmps: DS.hasMany('employee', async {false})
I set async to false since we will not persist them and before saving the new Trail I can just set this to null again.
in /app/templates/trails/new.js
{{#power-select-multiple options=model.currentEmployees
searchPlaceholder="Type a name to search"
searchField="fullName"
selected=model.selectedEmps placeholder="Select team member(s)"
onchange=(action (mut model.selectedEmps)) as |employee|
}}
<block here again/>
{{/power-select-multiple}}
This works, it doesn't 'blow up' after selecting the first employee. I can select multiple and delete them from the template. The control seems to work fine, as it is mutating 'model.selectedEmps' directly.
Now, I think this is a hack because I have two problems with it:
If I change the 'mut' to an action, so I can add further logic, I
cannot figure out how to access what is actually stored in the
propery 'model.selectedEmps'
Even if I can figure out (1) I will have to always make sure that
'selectedEmps' is emptied when leaving this route, otherwise the
next time this route is entered, it will remember what was
selected before (since they are now in the Ember.Store)
The fundamental issue is that I can live with 'mut' but still have the problem that when the user hits 'Save' I have to figure out which employees were selected, so I can create the assignments for them.
But I cannot figure out how to access what is selected. Maybe something this Spaghetti-Monster-awful mess:
save: function (newObj) {
console.log(newObj.get('selectedEmps'));
if (newObj.get('isValid')) {
let emp = this.get('store').createRecord('assignment', {
trail: newObj,
person: newObj.get('selectedEmps')[0]
})
newObj.save().then( function (newTrail) {
emp.save();
//newTrail.get('selectedEmps')
// this.transitionTo('trails');
console.log('DONE');
});
}
else {
alert("Not valid - please provide a name and a GPX file.");
}
},
So there are two problems to solve:
How to get the selected employees, iterate and create the
assignments.
How to then save the results to the API (JSON-API using Rails). I
presume that newObj.save and each assignment.save will take care
of that.
UPDATE
The developer of EPS kindly pointed out that the action handler receives an array, since I changed to using a multiple select, not a single select as it had been earlier. So the action is receiving the full array of what is currently selected. DOH!
I was thus able to update the action handler as follows, which now successfully stores the currently selected employees in the staff property of the controller. One step closer.
staffSelected(newList) {
existing.forEach(function(me){
if (!newList.includes(me)) {
existing.removeObject(me); // if I exist but the newList doesn't have me, remove me
}
});
newList.forEach(function(me){
if (!existing.includes(me)) {
existing.pushObject(me); // if I don't exist but the newList has me, add me
}
});
}
Perhaps not the best way to intersect 2 arrays but that's the least of my concerns at 4am on a Saturday night. :(
FINAL PROBLEM UPDATE - how to save the data?
Ok, so now that I can get the selected employees, I can create assignments, but still cannot figure out what Ember requires for me to save them, this save action throws an error:
save: function (newObject) {
if (newObject.get('isValid')) {
let theChosenOnes = this.controller.get('theChosenOnes');
let _store = this.get('store');
theChosenOnes.forEach(function (aChosenOne) {
_store.createRecord('assignment', {
trail: newObject,
person: aChosenOne,
});
});
newObject.save().then(function (newTrail) {
newTrail.get('assignments').save().then(function() {
console.log('DONE');
});
});
}
get(...).save is not a function
The problem with your final update is that in Ember Data 2.x, relationships are asynchronous by default, so what's returned from newTrail.get('assignments') is not a DS.ManyArray, which has a .save, but a PromiseArray, which doesn't have that.
You need a small tweak to do this instead, so you call .save on the resolved relationship:
newObject.save().then(function (newTrail) {
newTrail.get('assignments').then(assignments => assignments.save()).then(function() {
console.log('DONE');
});
});
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).
I am new to Ember and developing simple app that interact with user through form. If the user clicks 'reset' i want to reset the model to initial data.
To achieve this, i am cloning the model and set into the controller as 'oldModel'. If the user clicks reset i want to replace the model with oldModel.
jsbin: http://jsbin.com/EJISAne/673/edit
Please suggest me how can i achieve this by following the best practices.
In your setupController , change this
controller.set('oldModel', Ember.copy(model));
to
controller.set('oldModel', Ember.copy(model,true));
The true option is the key here. It will make a deep clone of the object.
Also there was a typo in your template.
<button action 'reset'>Reset</button>
should be
<button {{action 'reset'}}>Reset</button>
Working jsbin.
EDIT : The earlier jsbin was also throwing the assertion. The assertion was thrown because, Ember.Object doesn't implement Ember.Copyable mixin as told in the exception.
In the method App.parseSource
arr.push(Ember.Object.create(item))
can be changed to just,
arr.push(item)
This won't throw any exception as the check for implementation of copy will be done only for instances of Ember.Object
Update jsbin without exception
Ive implemented my reset like this
My ROUTE
The "routeHasBeenLoaded" property now lets user to change routes, and come back to route without losing any data previously inserted. On the other hand no properties have to be set manually after "save, edit"
e.g this.set('property1', []); after save is no needed. All you do is this.set('routeHasBeenLoaded', null);
import RouteResetter from 'appkit/misc/resetmixin';
export default Ember.Route.extend(Ember.SimpleAuth.AuthenticatedRouteMixin, RouteResetter, {
model : function(){
var self = this;
if(Ember.isNone(self.get('controller.routeHasBeenLoaded'))){
return Ember.RSVP.hash({
property1: this.store.findAll('das/dasdasfa'),
property2: [],
});
} else {
return;
}
}
});
Controller on load
routeHasBeenLoaded : null,
init : function(){
this._super();
this.set('routeHasBeenLoaded', true);
},
RouteResetterMixin
export default Ember.Mixin.create({
theModel: null,
theModelFunction : null,
afterModel : function(model){
this._super();
this.set('theModel', model);
this.set('theModelFunction', this.model.bind(this));
},
actions : {
triggerReset : function(){
var self = this;
this.get('theModelFunction')().then(function(resolvedModel){
self.set('controller.model', resolvedModel);
self.set('controller.routeHasBeenLoaded', true);
});
}
}
});
So im storing my inital model as well as model(); which i get in afterModel hook from parameter (model). And on reset, i reset the model to initial date.
I hope this helps.
Would like to see other solutions on that as well.
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));
}
}
How can you disregard a record that you have changed in an EmberJS view with Ember Data?
Something like delete without actually deleting it from the persistent storage.
I thought App.store.removeFromRecordArrays(record); would work.
You could extend DS.Model with a flag if this case (deleted client-side but do not delete server-side) is present for this model. Additionally a method is convenient to set this status (call model.deleteLocal()).
DS.Model.reopen({
deleteLocalFlag: false,
deleteLocal: function () {
this.set('deleteLocalFlag',true);
this.deleteRecord();
}
});
Then you need to customize the deleteRecords method in your adapter.
DS.YourAdapter.reopen({
deleteRecord: function(store, type, model) {
if (!model.get('deleteLocalFlag') {
// code for deleting in persitence layer
}
store.didDeleteRecord(model, model.toJSON({associations: true}));
}
});
Warning: This code was not tested, but works in my head ;)
May be a cleaner solution would be to use the stateManager of the object and transition into a different state instead of setting the flag. But I find the code around stateManager quite difficult to understand und probably is not worth the hassle.
1) You can use a transaction and rolback the transaction.
2) Or you can just rollback a record by using it's statemanager.
if(record.isDirty)
record.get('transaction').rollback();
you can for instance loop all records in the stores recordCache and rollback al the dirty records.
I personally use a record rollback mechanism on the willDestroyElement event in a view, so if a user leaves the view, he will be asked to save dirty records.
PatientTransport.FirmView = Ember.View.extend({
templateName: 'firm-view',
willDestroyElement: function() {
if (this.getPath('controller.content.isDirty')) {
var self = this;
Bootstrap.ConfirmBox.popup({
heading: "Some data has changed.",
message: "Do you want to save changes?",
callback: function(options, event) {
if (options.primary) {
self.getPath('controller.content.transaction').commit();
} else {
self.getPath('controller.content.transaction').rollback();
}
}
});
}
}
});