Ember.js Controller with computed property not being recomputed - ember.js

I'm trying to add permissions to groups, and I have a drag and drop set up so that a user can pull the unselected permissions over to selected, or vice versa. Unselected permissions are computed via removing the selected permissions from all permissions. This code is all functioning properly. The first time a user brings up the page, only those permissions that are unselected appear in the unselected side, and the same for selected.
However, when the user chooses another group to look at, the selected side is correct, while the unselected side shows what was displayed for the last group. Here is the route and controller:
App.GroupsEditRoute = Ember.Route.extend({
setupController: function(controller, model) {
this._super(controller, model);
controller.set('allPermissions', this.store.find('permission'));
},
actions: {
'update': function(group){
var route = this;
group.save().then(function(){
route.transitionTo('groups');
});
},
'cancel': function(group){
group.rollback();
this.transitionTo('groups');
},
'delete': function(group){
group.destroyRecord();
this.transitionTo('groups');
}
}
});
App.GroupsEditController = Ember.ObjectController.extend({
unselectedPermissions: function() {
console.log('UNSELECTED');
var allPermissions=this.get('allPermissions');
var permissions=this.get('permissions');
var self=this;
allPermissions.then( function() {
permissions.then( function() {
var unselected=allPermissions.filter(function(permission) {
return !permissions.contains(permission);
});
unselected=Ember.ArrayProxy.createWithMixins(Ember.SortableMixin, {
sortProperties: ['name'],
content: unselected
});
self.set('unselectedPermissions',unselected);
});
});
}.property('model.unselectedPermissions'),
selectedPermissions: function() {
console.log('SELECTED');
return Ember.ArrayProxy.createWithMixins(Ember.SortableMixin, {
sortProperties: ['name'],
content: this.get('permissions')
});
}.property('model.selectedPermissions')
});
When I use unselectedPermissions in my view via {{#each}}, it only fires once. I never see UNSELECTED in my log after that. However, the SELECTED, which is used in the same fashion, fires every time. Of course, the data displayed on the page is not updated, either, unless I refresh.
The setupController is being called each time a page is displayed, as it should.
I'm not sure what I'm doing wrong.
Any ideas?

in general computed properties shouldn't be set. When you set them you destroy the computed property portion of the code. There are a couple of different ways to handle this, the easiest is using an observer instead of computed property and setting the property.
unselectedPermissionList: [],
unselectedWatcher: function() {
console.log('UNSELECTED');
var allPermissions=this.get('allPermissions');
var permissions=this.get('permissions');
var self=this;
allPermissions.then( function() {
permissions.then( function() {
var unselected=allPermissions.filter(function(permission) {
return !permissions.contains(permission);
});
unselected=Ember.ArrayProxy.createWithMixins(Ember.SortableMixin, {
sortProperties: ['name'],
content: unselected
});
self.set('unselectedPermissionList',unselected);
});
});
}.observes('selectedPermissions')
The other way is to return an array reference, then push objects into that array after the fact.
unselectedWatcher: function() {
console.log('UNSELECTED');
var allPermissions=this.get('allPermissions'),
permissions=this.get('permissions'),
self=this,
ret = [];
allPermissions.then( function() {
permissions.then( function() {
var unselected=allPermissions.filter(function(permission) {
return !permissions.contains(permission);
});
unselected=Ember.ArrayProxy.createWithMixins(Ember.SortableMixin, {
sortProperties: ['name'],
content: unselected
});
unselected.forEach(function(item){
ret.pushObject(item);
});
});
});
return ret;
}.property('selectedPermissions')
Additionally your two properties claim to be dependent on each other, which should fire an infinite loop of property updating (a changes, b is dirty, b updates, a is dirty etc).
I'm not sure why selectedPermissions is a computed property, it seems like it would just be a list that's added to or removed from, and unselectedPermissions would just be allPermisions not selectedPermissions

Related

Ember: ArrayController computed property based on array item properties

I have an ArrayContoller on which I want to set a boolean property based on the properties of its contents.
Plain-language description of the logic:
If the array contains any items with a property of isRetired equal to true, set the retiredShoes property of the ArrayController to true, otherwise, set the ArrayController retiredShoes property to false.
It seems like this should be a simple matter, but I haven't found a solution anywhere, and I'm still pretty new at this.
I'll put together a jsfiddle if necessary.
Here are the controllers for the array and the object:
App.ApplicationController = Ember.ArrayController.extend({
sortProperties: ['title'],
itemController: 'shoe',
retiredShoes: function() {
//how do I compute this sucker?
}
});
App.ShoeController = Ember.ObjectController.extend({
needs: ['application'],
actions: {
delete: function() {
var shoe = this.get('model'),
runs = shoe.get('runs');
shoe.deleteRecord();
shoe.save();
},
toggleRetired: function() {
var shoe = this.get('model');
shoe.toggleProperty('isRetired');
shoe.save();
}
}
});
Off top of my head, without jsbin. If there's a problem/bug, drop me a comment and I'll look it over again.
App.ApplicationController = Ember.ArrayController.extend({
retiredShoes: function() {
return this.get("model").isAny("isRetired", true);
}.property("model.#each.isRetired")
});

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')

Mutual Exclusion in an Ember ArrayController

I have a list of items in an array controller. Clicking on an item makes it "active", but I’d like only one item to be active at any a time (akin to radio buttons).
I have this working by storing the active item in a computed property, then toggling its active state in an action on the array controller, see: http://emberjs.jsbin.com/wavay/2/edit
However, this doesn’t handle the case where an item is made active by some other means i.e. not through the action.
I have experimented with observing the isActive change (using .observesBefore('#each.isActive')), and flipping the state of the activeItem, but of course, this approach causes an infinite loop.
Is there a better way?
This can be solved using a combination of Ember.reduceComputed and observers.
The removedItem and addedItem callbacks in Ember.reduceComputed are given access to the object which has changed, as well as instanceMeta, which can be used to store the “active” item:
App.IndexController = Ember.ArrayController.extend({
itemController: 'item',
activeItem: Ember.reduceComputed('#this.#each.isActive', {
initialValue: null,
removedItem: function (accumulatedValue, item, changeMeta, instanceMeta) {
if (item.get('isActive')) {
var previousActiveItem = instanceMeta.activeItem;
if (previousActiveItem) previousActiveItem.set('isActive', false);
return instanceMeta.activeItem = item;
}
return instanceMeta.activeItem = null;
},
addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) {
return instanceMeta.activeItem;
}
})
…
However if activeItem is not accessed anywhere, removedItem and addedItem will never be called, and therefore items will remain active until they are manually toggled. To fix this, an observer can be set up to call this.get('activeItem') whenever an isActive property is changed:
setActiveItem: function () {
this.get('activeItem');
}.observes('#each.isActive')
See the updated jsbin: http://emberjs.jsbin.com/wavay/3/edit?js,output
Related: David Hamilton’s presentation on Array Computing Properties.
A possible solution based on your toggleActive implementation is available here.
This solution works if the "active" flag is updated only with the toggleActive controller method. So far the controller represents the state, it makes sense that provides the api to update its data correctly.
App.IndexController = Ember.ArrayController.extend({
itemController: 'item',
activeItem: function() {
return this.findBy('isActive');
}.property('#each.isActive'),
toggleActiveModel: function(model) {
var controller = this.findBy('model', model);
this._toggleActive(controller);
},
_toggleActive: function(controller) {
var previouslyActive = this.get('activeItem');
if(previouslyActive && previouslyActive !== controller) {
previouslyActive.set('isActive', false);
}
controller.set('isActive', !controller.get('isActive'));
},
actions: {
toggleActive: function(controller) {
this._toggleActive(controller);
},
toggle: function(modelValue) {
this.toggleActiveModel(modelValue);
}
}
});

Trouble accessing controller's computed property from view

My controller has a computed property:
App.IndexController = Ember.ArrayController.extend({
grandTotal: function () {
return this.getEach('total').reduce(function(accum, item) {
return accum + item;
}, 0);
}.property('#each.total'),
});
but I'm having trouble accessing it with my view. Here's my view:
App.SummaryView = Ember.View.extend({
templateName: 'summary',
companiesChanged: function() {
Ember.run.once(this, 'logCompanies');
}.observes('controller.#each'),
logCompanies: function() {
console.log(this.get('controller').get('model').get('length'));
console.log(this.get('controller').get('grandTotal'));
}
});
.get('length') returns correctly, so I know when this is called the models are loaded. But grandTotal is coming back as NaN, even though I know it's coded correctly since it's being rendered in the template. I need to access it within my view for additional reasons.
Any ideas?
Even though the controller's computed property changes with #each.total, the view only cares about the controller's property. Thus, the view was wrongly observing #each model, when it should have just been observing controller.grandTotal:
App.SummaryView = Ember.View.extend({
templateName: 'summary',
companiesChanged: function() {
Ember.run.once(this, 'logCompanies');
}.observes('controller.grandTotal'),
logCompanies: function() {
console.log(this.get('controller').get('model').get('length'));
console.log(this.get('controller').get('grandTotal'));
}
});

Set controllers content without model hook

I'm running RC-3 and want to setup the content of an arraycontroller without the model hook. This is because I need to add some filtering and don't want to reload the content with every transition.
I found that this.get('content') is sometimes undefined. I'm not sure why this is. Here's the code:
App.StockRoute = Em.Route.extend({
setupController: function(controller) {
if (controller.get('content') === undefined) {
controller.set('content', App.Stock.find());
}
}
});
What is the equivalent code in the setupController for the model hook?
Update
I've included this as a fuller description.
I took the ember guide of the todo app, and built off that. Currently I'm building a screen to mangage/view stock levels. What I'm trying to do is have a screen on which I can toggle all/specials/outofstock items (as per the todo, each has its own route), but then on the screen I need to filter the list eg by name or by tag. To add a challenge, I display the number of items (all, on special and out of stock) on the screen all the time, based on the filter (think name or tag) but not on the toggle (think all/on special/ out of stock)
Since its essentially one screen, I've done the following in the route code
App.StockIndexRoute = Em.Route.extend({
model: function() {
return App.Stock.find();
},
setupController: function(controller) {
// if (controller.get('content') === undefined) {
// controller.set('content', App.Stock.find());
// }
// sync category filter from object outside controller (to match the 3 controllers)
if (controller.get('category') != App.StockFilter.get('category')) {
controller.set('category', App.StockFilter.get('category'));
controller.set('categoryFilter', App.StockFilter.get('category'));
}
// a hack so that I can have the relevant toggle filter in the controller
if (controller.toString().indexOf('StockIndexController') > 0) {
controller.set('toggleFilter', function(stock) { return true; });
}
}
});
App.StockSpecialsRoute = App.StockIndexRoute.extend({
setupController: function(controller) {
this._super(controller);
controller.set('toggleFilter', function(stock) {
if (stock.get('onSpecial')) { return true; }
});
}
});
App.StockOutofstockRoute = App.StockIndexRoute.extend({
setupController: function(controller) {
this._super(controller);
controller.set('toggleFilter', function(stock) {
if (stock.get('quantity') === 0) { return true; }
});
}
});
You'll see that the only difference in the routes is the definition of the toggle filter, which needs to be applied to the model (since stock is different to stock/special or to stock/outofstock)
I haven't yet figured out how to link one controller to multiple routes, so I have the following on the controller side
App.StockIndexController = Em.ArrayController.extend({
categoryFilter: undefined,
specialCount: function() {
return this.get('content').filterProperty('onSpecial', true).get('length');
}.property('#each.onSpecial'),
outofstockCount: function() {
return this.get('content').filterProperty('quantity', 0).get('length');
}.property('#each.quantity'),
totalCount: function() {
return this.get('content').get('length');
}.property('#each'),
// this is a content proxy which holds the items displayed. We need this, since the
// numbering calculated above is based on all filtered tiems before toggles are added
items: function() {
Em.debug("Updating items based on toggled state");
var items = this.get('content');
if (this.get('toggleFilter') !== undefined) {
items = this.get('content').filter(this.get('toggleFilter'));
}
return items;
}.property('toggleFilter', '#each'),
updateContent: function() {
Em.debug("Updating content based on category filter");
if (this.get('content').get('length') < 1) {
return;
}
//TODO add filter
this.set('content', content);
// wrap this in a then to make sure data is loaded
Em.debug("Got all categories, lets filter the items");
}.observes('categoryFilter'),
setCategoryFilter: function() {
this.set('categoryFilter', this.get('category'));
App.StockFilter.set('category', this.get('category'));
}
});
// notice both these controllers inherit the above controller exactly
App.StockSpecialsController = App.StockIndexController.extend({});
App.StockOutofstockController = App.StockIndexController.extend({});
There you have it. Its rather complicated, perhaps because I'm not exactly sure how to do this properly in ember. The fact that I have one url based toggle and a filter that works across those 3 routes is, I think, the part that makes this quite compicated.
Thoughts anybody?
Have you tried to seed your filter with some data?
App.Stock.filter { page: 1 }, (data) -> data
That should grab the materialized models from the store, and prevent making any more calls to the server.