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);
}
}
});
Related
hi I am trying to build Coverflow in emberjs using Collectionview
I guess i will be needing css calculations after all children has rendered but i am not able to do it as call back is not triggered in my case
entire bin enter link description here
my collection view looks like this
App.CoverflowView = Ember.CollectionView.extend({
items: null,
itemSize: null,
props: ['width', 'Width', 'left', 'Left'],
itemWidth: null,
itemHeight: null,
duration: null,
current: null,
elementId: 'coverflow',
createChildView: function (viewClass, attrs) {
viewClass = App.CoverView;
return this._super(viewClass, attrs);
},
onChildViewsChanged: function (obj, key) {
console.log('check');
var length = this.get('childViews.length');
if (length > 0) {
Ember.run.scheduleOnce('afterRender', this, 'childViewsDidRender');
}
}.observes('childViews'),
childViewsDidRender: function () {
console.log('childViewsDidRender - count of paragraphs: ');
}
});
Regardless the needs of coverflow, i'm assuming it is required to fire the function for each view. In this setup the observer will not fire for property childViews for each child view and at first glance of the related code i don't see a property that gets set in order to observe it. So i propose a simple solution, to call the function from createChildView since this has been overriden and also keep observing the childViews property if the content changes dynamically at some point.
Example,
http://emberjs.jsbin.com/weleqabu/1 (look at the console)
createChildView: function(viewClass, attrs){
viewClass = App.CoverView;
this.onChildViewsChanged();
return this._super(viewClass,attrs);
}
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
In my Application I have the following rawNodes property, which I am using as an application-wide cache:
var App = Ember.Application.createWithMixins({
...
/**
The rawNodes property is a nodes deposit to be used
to populate combo boxes etc.
**/
rawNodes: null,
getNodes: function () {
if (!this.rawNodes) {
this.rawNodes = this.Node.find();
}
},
...
});
In some of my controllers I am modifying data which should also be updated in this generic cache. I would like to implement a couple of functions, to update a given node, and to delete a given node. Something like:
updateNode: function(node_id, node) {
this.rawNodes.update(node_id, node);
},
deleteNode: function(node_id) {
this.rawNodes.delete(node_id);
}
But I do not really know how to work with an ArrayController, not even if those operations are at all possible. I see no examples of this kind of procedures in the ArrayController documentation. Could somebody offer an example, or point me in the right direction?
Rather than using a rawNodes property, I think it might be more useful to
maintain a Node model and a NodesController. Assign the model property
with setupController so you can be sure that nodes are always fetched.
Since this is an application-wide cache, use needs in ApplicationController so it can delegate to its methods.
App.ApplicationRoute = Em.Route.extend({
setupController: function() {
this.controllerFor("nodes").set("model", App.Node.find());
}
});
App.ApplicationController = Em.Controller.extend({
needs: "nodes",
});
App.NodesController = Em.ArrayController.extend({
getNodes: function() {
// ...
}
});
App.NodeController = Em.ObjectController.extend({
updateNode: function() {
// ...
},
deleteNode: function() {
// ...
}
});
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.
I have this test application which should print "filtered" and "changed" each time the applications view is clicked, because a computed property is called. However the property binding is only triggered when updating the property with an empty array:
window.App = Ember.Application.create({
Settings: Ember.Object.create({
filter: []
}),
ApplicationView: Ember.View.extend({
click: function(event) {
filter = App.get('Settings.filter');
console.dir(filter);
if (App.get('Room.filtered')) {
filter = filter.filter(function(item) {
return item != App.get('Room.name');
});
} else {
filter.push(App.get('Room.name'));
}
App.set('Settings.filter', filter);
}
})
});
Room = Ember.Object.extend({
name: "test",
_filterer: function() {
console.dir("changed");
}.observes('filtered'),
filtered: function() {
console.dir("filtered");
filter = App.get('Settings.filter');
for (var i = 0; i < filter.length; i++) {
if (filter[i] == this.get('name')) return true;
}
return false;
}.property('App.Settings.filter', 'name').volatile()
});
App.Room = Room.create();
setTimeout(function() {App.set('Settings.filter', ['test']); }, 3000);
Here is the jsbin: http://jsbin.com/edolol/2/edit
Why is the property binding only triggered when setting the Setting to an empty array? And why does it trigger when doing it manually in the timeout?
Update
Here is a working jsbin: http://jsbin.com/edolol/11/edit
When you're going to add/remove items from an array, and not change the entire array, you need to inform the computed property to observe the items inside the array, not only the array itself:
The syntax is:
function() {
}.property('App.settings.filter.#each')
The reason it was working with setTimeout is because you were replacing the entire array instead of the items inside it.
I fixed your jsbin: http://jsbin.com/edolol/10/edit
I fixed some minor other stuff such as filter.push is now filter.pushObject (Using Ember's MutableArray).
And after changing the filter array (filter = filter.filter()) you need to set the new filter variable as the property: App.set('Settings.filter', filter);
The Problem is that I have used .push() to add to App.Settings.filter and .filter() to remove from it. The first approach does not create a new array, the latter does. Thats why removing from that array has worked, but not adding.
I assume that using Ember.ArrayProxy and an observer for .#each would have worked. But thats out of my knowledge. This little problem is solved by just creating a new array though.
filter = App.get('Settings.filter').slice(0);