Merge two model arrays in a route - ember.js

I have a controller handling a list of models. These models are of two different types (e.g. Message and Comment). In order to use an ArrayController I would have to merge both lists into one. Is there a way to do this ?
Class-based polymorphism, as proposed in this thread, would solve my problem, but they are not likely to be implemented soon.
In my current solution, I use an ObjectController reveiving both comments and messages. I then merge them using a computed property:
App.SomeRoute = Ember.Route.extend({
model: function (params) {
return Em.Object.create({
comments: this.store.find('comment'),
messages: this.store.find('message'),
});
},
});
App.SomeIndexController = Ember.ObjectController.extend({
merged: Em.computed.union('messages', 'comments'),
});
It works, but I don't benefit from all the niceties of an ArrayController (like sortProperties for example).
What I would like to do is something like:
App.SomeRoute = Ember.Route.extend({
model: function (params) {
var comments = this.store.find('comment');
var messages = this.store.find('message');
return merge(comments, messages);
},
});
where merge returns something similar to what is returned by this.store.find('model').

I asked a similar question recently, here is how I solved the issue.
App.SomeIndexController = Ember.ObjectController.extend({
sortProperties: ['some field'],
sortAscending: false, // false for descending
merged: function() {
var comments = this.get('comment') || [], // This gets wherever you've stored the comments array
messages = this.get('message') || [];// This gets wherever you've stored the messages array
var stream = Ember.A();
stream.pushObjects(comments.toArray());
stream.pushObjects(messages.toArray());
return Em.ArrayProxy.createWithMixins(Ember.SortableMixin, {
content: stream,
sortProperties: this.sortProperties,
sortAscending: this.sortAscending
});
}.property('messages.#each', 'comments.#each')
});
Hope this works for you as well. Just an FYI, for my example, my controller is actually one that is rendered, so I do not set up the model for it in the route. I simply have properties on my controller, lets say, commments and messages that constantly updated themselves as RecordArrays.
So for your example you may need to observe .property('model.messages.#each', 'model.comments.#each')

Inspired by #bmeyers' answer, and after exploring ember-data's source a little bit, I came up with a solution that is reusable and not too terrible. It is probably not optimal, but it does the work.
App.Store = DS.Store.extend({
findMultiple: function (types) {
var self = this;
var recordsByType = types.map(function (type) {
return self.find(type);
});
return self.mergeArrayPromises(recordsByType);
},
mergeArrayPromises: function (promises) {
var promise = Ember.RSVP.all(promises).then(function(arrays) {
var mergedArray = Ember.A();
arrays.forEach(function (records) {
mergedArray.pushObjects(records.toArray());
});
return mergedArray;
});
return DS.PromiseArray.create({
promise: promise,
});
},
});
App.SomeRoute = Ember.Route.extend({
model: function (params) {
return this.store.findMultiple(['comment', 'message']);
},
});

This might help. I stumbled upon this a while back and your question reminded me
https://gist.github.com/sebastianseilund/6096696

Related

Ember toggle element in an array

What is the Ember way to do the following?
App.IndexController = Ember.Controller.extend({
actions: {
change: function(){
var model = this.get('model');
model[0] = true;
this.set('model', model);
}
}
});
I want to toggle an element (index 0 in this example) in model.
Here is the jsbin: http://emberjs.jsbin.com/doyejipagu/1/edit. The change to the model is not being reflected.
The solution is to use replace to modify the array:
change: function(){
this.get('model').replace(0, 1, [true]);
}
See http://emberjs.com/api/classes/Ember.MutableArray.html#method_replace. The above means "starting at position 0, replace 1 element, with the single element true". replace notifies Ember that the array contents have changed, so it is reflected everywhere.
It would be nice if there were a replaceAt API, allowing us to just say model.replaceAt(0, true), but there's not. Of course, you could write your own:
Ember.MutableArray.reopen({
replaceAt: function(pos, val) {
return this.replace(pos, 1, [val]);
}
});
The problem with your code is that nothing alerts Ember to the fact that the internal values of model have changed. model[0] = true triggers nothing. Your this.set('model', model) does not change the value of the model property itself; so neither does it trigger any observers or bindings.
You could also create a new array (here using slice), which would work:
var model = this.get('model').slice();
model[0] = true;
this.set('model', model);
Now, Ember sees that model has changed, and does all its magic.
What you try to do is not possible. A model either has to be an object or an array of objects otherwise you cannot set properties on it.
So you could do for example:
App.IndexRoute = Ember.Route.extend({
model: function() {
return [
Ember.Object.create({value: false}),
Ember.Object.create({value: true}),
Ember.Object.create({value: false})
];
}
});
App.IndexController = Ember.Controller.extend({
actions: {
change: function(){
this.get('model')[0].toggleProperty('value');
}
}
});

Ember setupController fires observes, how can i prevent that

My code looks something like this
App.ItemRoute = Em.Route.extend({
setupController: function(controller) {
var model = this.modelFor('item');
controller.setProperties({
name : model.get('name'),
title: model.get('title')
});
}
});
App.ItemController = Em.ObjectController.extend({
saveOnChange: function() {
console.log('saveOnChange');
}.observes('name', 'title'),
});
From my understanding because i am using setProperties the observe should only fire once , but it fire two times
also wrapping the setProperties with beginPropertyChanges & endPropertyChanges still fires twice
what i ultimately is for it to not fire at all, so what i ended up doing was changing the controller code to be like this
App.ItemController = Em.ObjectController.extend({
load: false,
saveOnChange: function() {
if(!this.get('load')) {
this.set('load', true);
return;
}
console.log('saveOnChange');
}.observes('name', 'title'),
});
this code would work if the change is only fired once, but it won't work if its fired multiple times (that's my case)
The setProperties function doesn't coalesce your observers (unfortunately there's no way to do that), it just groups them into one operation. The source might help you to better see what it does:
Ember.setProperties = function(self, hash) {
changeProperties(function() {
for(var prop in hash) {
if (hash.hasOwnProperty(prop)) { set(self, prop, hash[prop]); }
}
});
return self;
};
So, back to your problem. The best way that I can think of is to debounce your function.
App.ItemController = Em.ObjecController.extend({
load: false,
saveOnChange: function() {
Em.run(this, 'debouncedSave', 150);
}.observes('name', 'title'),
debouncedSave: function() {
if(!this.get('load')) {
this.set('load', true);
}
}
});
If you're not familiar with debouncing, you can read about it here. There are probably some other solutions involving direct manipulation of the properties, but I'm not sure if that's a road you want to go down.

Updated : FirstObject is undefined emberjs array

I need to get firstObject of my carousel and set it as active
this is how I am setting carousel property
JSBIN for code
App.ApplicationController = Ember.ArrayController.extend({
content: [],
carouselData: function () {
var categories = this.get('content');
var products = Em.A([]);
categories.forEach(function (category) {
category.get('items').then(function (data) {
data.forEach(function (product) {
products.addObject(product);
});
});
});
console.log(products);
console.log(products.get('firstObject'));
return products;
}.property('content')
});
Update
#ppcano Thanks for the explanation :). I got what you are asking me to do. return model only when hasMany has fulfilled. and then with computed property save them in carouselData property. but may be I am missing something in implementation the cdata.get('firstObject') returns a promise updated jsbin UPDATED JSBIN in App.Caroure
update 2
SOLVED enter link description here
Your problem is that the computed property does not work within async execution.
category.get('items').then(function (data)
The products variable is returned before any data can be pushed into products, because the items must be requested.
You could solve it when you ensure that items are loaded when the property is computed. You could do it in your route model as:
model: function(){
return this.store.find('facture').then(function(factures){
var productPromises = factures.getEach('items');
return Ember.RSVP.all(productPromises).then(function(products) {
return factures;
});
});
}
Then, you could define your CP as:
carouselData: function(){
var result = Em.A([]);
this.get('content').getEach('items').forEach(function(items){
result.pushObjects(items.toArray());
});
return result;
}.property('content.#each.items.#each')

Multiple models to Ember route

I have a list of printers
GradingX.PrintersRoute = Ember.Route.extend({
model: function () {
var printerList = Em.A();
//Ember.
$.getJSON("http://localhost/printers").then(function (data) {
data.forEach(function (item) {
printerList.addObject(item);
}), function () {
alert("$.getJSON failed!");
};
});
return printerList;
},
});
That I'm trying to access from my header
GradingX.HeaderRoute = Ember.Route.extend({
model: function () {
//console.log("test in header model route");
//return Ember.Object.create({
return Ember.RSVP.hash({
printers: What Goes Here?,
otherObjects: More Stuff Here
});
},
});
I'm trying to follow the answer here https://stackoverflow.com/a/20523510/697827, but since I'm not accessing through Ember-Data I don't think this.store.find('printers') is going to get me what I need.
I'm missing something. Please help!
RSVP.hash expects an object with keys and promises as values. So I think that the following could work:
GradingX.HeaderRoute = Ember.Route.extend({
model: function () {
return Ember.RSVP.hash({
printers: $.getJSON("http://localhost/printers"),
otherObjects: $.getJSON("http://localhost/???")
});
},
});
In the referenced answer is used this.store.find, which also returns a promise, but it's resolved with a DS.RecordArray (an array like object provided by ember data). So what matters for RSVP.hash are promises.

Redirecting to first item in ArrayController

I am trying to redirect to the first item in an ArrayController. I have found a few other questions related to this, but none had answers that seemed to work (a lot of changes have been happening so this is understandable).
One particular answer from Yehuda here:
App.DoctorsRoute = Ember.Route.extend({
model: function() {
return App.Doctor.find();
},
redirect: function() {
var doctor = this.modelFor('doctors').get('firstObject');
this.transitionTo('doctor', doctor);
}
});
I 'think' I have recreated this scenario, but I must have done something wrong...
Any insight into what I am doing wrong is greatly appreciated.
Example JSBin here.
The problem here is that your list of models is not yet loaded from the server. Depending on your needs i would recomend using a promis to let the rout wait abit until your model is loaded.
App.DoctorsRoute = Ember.Route.extend({
model: function() {
return App.Doctor.find().then(function (list) {
return list.get('firstObject');
});
},
redirect: function() {
var doctor = this.modelFor('doctors');
this.transitionTo('doctor', doctor);
}
});
ofcourse.. wel that would make the redirect abit silly, so if you just want to wait for the list to load you could try:
App.DoctorsRoute = Ember.Route.extend({
model: function() {
return App.Doctor.find();
},
redirect: function() {
var self = this;
this.modelFor('doctors').then(function (list) {
return list.get('firstObject');
}).then(function (doctor){
if(!doctor)
self.transitionTo('doctor', doctor);
});
}
});