save store with hasmany and belong to relationship - ember.js

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"...

Related

Emberjs promiseArray inside route doesn't return properly

I have a controller for showing item.
Users can put the item in their wish list.
(Item has many users, User has many Items.)
So, when user enter the webpage, I want to show a AddToList or RemoveFromList button to the user based on isAddedToList property.
Below is the code.
User Model:
var User = DS.Model.extend({
username: DS.attr('string'),
email: DS.attr('string'),
avatar: DS.attr('string'),
items: DS.hasMany("item", { async: true })
});
export default User;
ItemModel:
var Item = DS.Model.extend({
name: DS.attr("string"),
priceInCent: DS.attr("number"),
salePriceInCent: DS.attr("number"),
brand: DS.belongsTo("brand"),
itemImages: DS.hasMany("itemImage", { async: true }),
users: DS.hasMany("user", { async: true }),
});
export default Item;
ItemRoute:
var ItemRoute = Ember.Route.extend({
model: function(params) {
var userId = this.get("session").get("userId");
return Ember.RSVP.hash({
item: this.store.find('item', params.item_id),
user: this.store.find('user', userId),
});
},
setupController: function(controller, model) {
controller.set('item', model.item);
controller.set('user', model.user);
}
});
export default ItemRoute;
ItemController:
var ItemController = Ember.Controller.extend({
needs: ["current-user", "application"],
currentUser: Ember.computed.alias("controllers.current-user"),
isAddedToList: function() {
var promiseUsers = this.get("item.users"), user = this.get("user");
return promiseUsers.contains(user);
}.property("item"),
actions: {
addToList: function() {
var item = this.get("item"), user = this.get("user");
item.get("users").pushObject(user);
item.set("addedUserIds", [user.get("id")]);
item.save();
},
removeFromList: function() {
var item = this.get("item"), user = this.get("user");
item.get("users").removeObject(user);
item.set("removedUserIds", [user.get("id")]);
item.save();
}
}
});
export default ItemController;
The problem is when I check the length of promiseUsers with
promiseUsers.get("length")
it always returns 0.
but when I try the same with Chrome console, it returns the length properly.
Do I miss something in the route? How to fix the problem?
The problem is you're using your code synchronously, despite it being an asynchronous property.
The first time you attempt to use an async relationship it will begin resolving the relationship, making a callback to the server is necessary. In your case you try to use the users right away, but they are going to be empty the first time, so you're contains will return false. Since you aren't watching the users' collection it will then update, but the computed property won't update since the computed property was just watching item. This is why when you try it from the console it works, because by the time you attempt to use it in the console it's finished resolving the async collection of users.
isAddedToList: function() {
var promiseUsers = this.get("item.users"), user = this.get("user");
return promiseUsers.contains(user);
}.property("user", 'item.users.[]')

Filtering child records in Ember Data

I have not been able to filter the childrecords of objects in ArrayController.
The structure of my models is like this:
var Shop = DS.Model.extend({
name: DS.attr('string'),
products: DS.hasMany('product')
});
var Product = DS.Model.extend({
name: DS.attr('string'),
shop: DS.belongsTo('shop')
});
Shop has many products, and product belongs to a shop. I would like to filter the childrecords of each parent based on a Ember.TextField. Filtering works if I'm only filtering the parent records based on a property they have, using a regexp.
productSearchResults: function() {
var productSearchTerm = this.get('productSearchTerm');
var regExp = new RegExp(productSearchTerm,'i');
Ember.Logger.log('productSearchTerm', productSearchTerm);
var filteredResults = this.map(function(shop){
var products = shop.get('products');
return products.filter(function(product){
regExp.test(product.get('name'));
});
});
// all items are returned always..
return filteredResults;
}.property('products.#each', 'productSearchTerm')
Edit
I tried to use promises here (source: Filter child-records (hasMany association) with Ember.js ), but it seems like this productSearchResults property is never accessed. I do not get any log output from here. In the template, I'm looping over filteredProducts and there is nothing there. If it's of any relevance, I'm using Ember 1.5.0 and Ember Data 1.0.0-beta.7+canary.b45e23ba .
productSearchResults: function() {
var _that = this;
var productSearchTerm = this.get('productSearchTerm');
var regExp = new RegExp(productSearchTerm,'i');
this.store.find('shop').then(function(shops) {
var promises = shops.map(function (shop) {
return Ember.RSVP.hash({
shop: shop,
products: shop.get('products').then(function (products) {
return products.filter(function (product) {
return regExp.test(product.name);
});
})
});
});
Ember.RSVP.all(promises).then(function (filteredProducts) {
_that.set('filteredProducts', filteredProducts);
});
});
}.property('products.#each', 'productSearchTerm')

Ember: use controller data in route or how to fetch data properly

This question is a follow up on my previous question: Architecture for reusable object in ember
In my app I create multiple charts using an Ember.Component. The daterange for all the charts is controlled by a Daterangepicker which has its own controller etc.. Now the data for each chart is fetched in the IndexRoute (with an ajax call), and the daterange is passed in the query string.
The problem is that I can't seem to figure out how to access the daterange from the IndexRoute. Here's my code:
IndexRoute.js
App.IndexRoute = Ember.Route.extend({
model: function(){
var that = this;
return Ember.Object.extend({
registrationsData: null,
registrations: function() {
var self = this;
$.ajax({
url: Routing.generate('ysu_user_api_statistics_registrations', {startDate: that.dateRange.startDate, endDate: that.dateRange.endDate}),
success: function(data) {
var labels = [];
var values = [];
var chartData = {
labels : data.labels,
datasets : [
{
data : data.values,
}
],
};
self.set('registrationsData', chartData);
}
});
}.property(),
}).create();
},
dateRange: Ember.Object.create({
id: 1,
startDate: '2013-08-01',
endDate: '2013-08-31'
}),
});
Index.hbs
{{ my-chart title="Registrations" dataBinding=model.registrations registrationsDataBinding=model.registrationsData}}
MyChartComponent.js
App.MyChartComponent = Ember.Component.extend({
...
dataBinding: null,
registrationsDataBinding: null,
dateRangeBinding: null,
modelDateRangeBinding: null,
chartContext: null,
myChartController: null,
didInsertElement: function() {
/* Create and set controller */
if (!this.get('myChartController')) {
myChartController = new App.MyChartController()
this.set('myChartController', myChartController);
}
this.set('chartContext', $(this.get('element')).find('canvas')[0].getContext("2d"));
},
drawChart: function() {
if(this.get('chartContext')) {
var ctx = this.get('chartContext');
var options = {
bezierCurve : false,
pointDotRadius : 6,
pointDotStrokeWidth : 4,
datasetStrokeWidth : 4,
}
var myNewChart = new Chart(ctx).Line(this.get('registrationsDataBinding'), options);
}
}.observes('registrationsDataBinding', 'myChartController.dateRange'),
});
MyChartController.js
App.MyChartController = Ember.ArrayController.extend({
container: App.__container__,
needs: ['daterangepicker'],
dateRange: 'controllers.daterangepicker.selectedRange',
dateRangeBinding: 'controllers.daterangepicker.selectedRange',
});
I must admit, this setup feels kinda weird. So ultimately my question is:
What would be the correct way to fetch data for my charts based on startDate and endDate set in my DatePickerController?
I have been struggling with this problem as well.
In some of my apps, I've needed the URL to control the date range (e.g. a particular month). In these cases, I would created a MonthRoute and a MonthModel - think of it as a monthly report. The MonthModel has a hasMany property of the actual data I wanted to chart:
App.Month = DS.Model.extend({
companies: DS.hasMany('App.Company')
});
A datepicker would let the user enter a new route, which would fetch (say) the Jan-2013 month model
{
month: {
id: 'Jan-2013',
companies: [
{name: 'Acme, Inc', revenue: 10425, ...},
...
]
}
}
Then, I would set the embedded companies data on my CompaniesController in the setupController hook:
App.MonthRoute = Ember.Route.extend({
setupController: function(controller, model) {
controller.set('model', model);
this.controllerFor('companies').set('model', model.get('companies'));
}
});
Then, I would do the various array manipulations on my CompaniesController, and make that data available to my charts.
I have some code for this up on github, as well as a demo. I'd be interested to hear your thoughts.

Delete associated model with ember-data

I have two models:
App.User = DS.Model.create({
comments: DS.hasMany('App.Comment')
});
App.Comment = DS.Model.create({
user: DS.belongsTo('App.User')
});
When a user is deleted, it also will delete all its comments on the backend, so I should delete them from the client-side identity map.
I'm listing all the comments on the system from another place, so after deleting a user it would just crash.
Is there any way to specify this kind of dependency on the association? Thanks!
I use a mixin when I want to implement this behaviour. My models are defined as follows:
App.Post = DS.Model.extend(App.DeletesDependentRelationships, {
dependentRelationships: ['comments'],
comments: DS.hasMany('App.Comment'),
author: DS.belongsTo('App.User')
});
App.User = DS.Model.extend();
App.Comment = DS.Model.extend({
post: DS.belongsTo('App.Post')
});
The mixin itself:
App.DeletesDependentRelationships = Ember.Mixin.create({
// an array of relationship names to delete
dependentRelationships: null,
// set to 'delete' or 'unload' depending on whether or not you want
// to actually send the deletions to the server
deleteMethod: 'unload',
deleteRecord: function() {
var transaction = this.get('store').transaction();
transaction.add(this);
this.deleteDependentRelationships(transaction);
this._super();
},
deleteDependentRelationships: function(transaction) {
var self = this;
var klass = Ember.get(this.constructor.toString());
var fields = Ember.get(klass, 'fields');
this.get('dependentRelationships').forEach(function(name) {
var relationshipType = fields.get(name);
switch(relationshipType) {
case 'belongsTo': return self.deleteBelongsToRelationship(name, transaction);
case 'hasMany': return self.deleteHasManyRelationship(name, transaction);
}
});
},
deleteBelongsToRelationship: function(name, transaction) {
var record = this.get(name);
if (record) this.deleteOrUnloadRecord(record, transaction);
},
deleteHasManyRelationship: function(key, transaction) {
var self = this;
// deleting from a RecordArray doesn't play well with forEach,
// so convert to a normal array first
this.get(key).toArray().forEach(function(record) {
self.deleteOrUnloadRecord(record, transaction);
});
},
deleteOrUnloadRecord: function(record, transaction) {
var deleteMethod = this.get('deleteMethod');
if (deleteMethod === 'delete') {
transaction.add(record);
record.deleteRecord();
}
else if (deleteMethod === 'unload') {
var store = this.get('store');
store.unloadRecord(record);
}
}
});
Note that you can specify via deleteMethod whether or not you want to send the DELETE requests to your API. If your back-end is configured to delete dependent records automatically, then you will want to use the default.
Here's a jsfiddle that shows it in action.
A quick-and-dirty way would be to add the following to your user model
destroyRecord: ->
#get('comments').invoke('unloadRecord')
#_super()
I adapted the answer of #ahmacleod to work with ember-cli 2.13.1 and ember-data 2.13.0. I had an issue with nested relationships and the fact that after deleting an entity from the database its id was reused. This lead to conflicts with remnants in the ember-data model.
import Ember from 'ember';
export default Ember.Mixin.create({
dependentRelationships: null,
destroyRecord: function() {
this.deleteDependentRelationships();
return this._super()
.then(function (model) {
model.unloadRecord();
return model;
});
},
unloadRecord: function() {
this.deleteDependentRelationships();
this._super();
},
deleteDependentRelationships: function() {
var self = this;
var fields = Ember.get(this.constructor, 'fields');
this.get('dependentRelationships').forEach(function(name) {
self.deleteRelationship(name);
});
},
deleteRelationship (name) {
var self = this;
self.get(name).then(function (records) {
if (!records) {
return;
}
var reset = [];
if (!Ember.isArray(records)) {
records = [records];
reset = null;
}
records.forEach(function(record) {
if (record) {
record.unloadRecord();
}
});
self.set(name, reset);
});
},
});
Eventually, I had to set the relationship to [] (hasMany) or null (belongsTo). Else I would have run into the following error message:
Assertion Failed: You cannot update the id index of an InternalModel once set. Attempted to update <id>.
Maybe this is helpful for somebody else.

Can a nested ember.js route use a different model and still retain controller context?

I have a basic person object
PersonApp.Person = DS.Model.extend({
username: DS.attr('string')
});
I have a route to find all people
PersonApp.Router.map(function(match) {
this.resource("person", { path: "/" }, function() {
this.route("page", { path: "/page/:page_id" });
this.route("search", { path: "/search/:page_term" });
});
});
In my route I'm looking at the params coming in
PersonApp.PersonRoute = Ember.Route.extend({
selectedPage: 1,
filterBy: '',
model: function(params) {
if (get(params, 'page_id') !== undefined) {
this.selectedPage = get(params, 'page_id');
} else {
this.selectedPage = 1;
}
if (get(params, 'page_term') !== undefined) {
this.filterBy = get(params, 'page_term');
} else {
this.filterBy = '';
}
console.log(this.selectedPage);
console.log(this.filterBy);
return PersonApp.Person.find();
}
});
My nested routes are using a different model (not person directly) as they contain data that isn't persisted (and really only let me flip a bit on the controller)
Yet when I manually put something on the url or click a link that does a full blown transition the "params" coming into my model hook above are always empty.
Here is the basic page model I'm using (w/ search support)
PersonApp.Page = Ember.Object.extend({
term: ''
});
When a user does a search I have a view that invokes transitionTo
PersonApp.SearchField = Ember.TextField.extend({
keyUp: function(e) {
var model = PersonApp.Page.create({term: this.get('value')});
this.get('controller.target').transitionTo('person.search', model);
}
});
Any way I can pass this "page" model to a nested view and still retain the basic "person" controller context (ie- so I can manipulate the view around this array of model objects)