SelectAll feature with Ember ObjectController and ItemController - ember.js

I am trying to implement a multiselect row in a table. The parent controller is just an object controller. It has a model, and the view iterates over the recordset of the model as individual rows.
I have implemented an itemController for all the rows in the model. That works.
But for the 'selectAll' functionality, in the parent controller, I am not able to get hold of all the items (individually). Do you have any idea how to go abt it?
Here's my work so far :
export
default Ember.ObjectController.extend({
// parent Controller
itemController: 'checkbox',
selectAll: function(key, value) {
var items = this.get('model.items');
if (arguments.length == 2) {
this.setEach('isSelected', value); //setEach is throwing an error sine it comes from ArrayController where as I am using ObjectController as the parent controller type
return value;
} else {
return this.isEvery('isSelected', true); //isEvery is also throwing error for the same reason
}.property('model.items.#each.isSelected')
And my Item Controller (checkboxcontroller) is as follows :
export default Ember.ObjectController.extend({
isSelected: false,
selectedListOfItems: [],
isSelectedChange: function() {
var selectedListOfItems = this.get('selectedListOfItems');
var itemId = this.get('id'); // comes from the model.items.id
debugger;
if (this.get('isSelected')) {
// add itemId to the selected array
var index = selectedListOfItems.indexOf(itemId);
if (index > -1) {
selectedListOfItems.splice(index, 1, itemId);
} else {
selectedListOfItems.push(itemId);
}
} else {
// remove itemId from the selected array
var index = selectedListOfItems.indexOf(itemId);
if (index > -1) {
selectedListOfItems.splice(index, 1);
}
}
this.set('selectedListOfItems', selectedListOfItems);
}.observes('isSelected')
});
My doubt is how do I do selectAll on the parent controller (that is of ObjectController type) that selects all the checkboxes of all the children. I am not sure if the info I've provided above is enough. Kindly let me know if you need more info. Thanks in advance

I got it working by adding a listener to the child (ItemController) that listens for any change in the parent's variable.
Here's what I did :
parentControllerDidChange: function() {
if (this.get('parentController.selectedAllItems')) {
this.set('isSelected', true);
} else {
this.set('isSelected', false);
}
}.observes('parentController.selectedAllItems')
That did the trick. Now If I toggle the boolean variable on the parent controller, all the children react. Ember the beauty !

Related

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);
}
}
});

ContainerView childViews binding

Is it possible to bind the childViews property to one within the controller? Thus,
App.DashboardView = Ember.ContainerView.extend({
tagName: 'section',
childViewsBinding: 'controller.viewChildren',
...
});
And within the controller, a view object is dynamically created (qryView) and then appended to the controller's array:
this.get('viewChildren').pushObject(qryView.create());
I've been trying this but I don't see any change in the containerView's rendering after the array is populated.
Bryan
I found a way to do this, as binding didn't seem to work. I created an observer in the containerView:
App.DashboardView = Ember.ContainerView.extend({
tagName: 'section',
updChildViews: (function() {
var children, ths;
try {
ths = this;
children = this.get('controller.viewChildren');
children.forEach(function(chld) {
if (!ths.get('childViews').contains(chld)) {
ths.pushObject(chld);
}
});
} catch (e) {
console.error("DashboardView.updChildViews error:", e);
}
}).observes('controller.viewChildren.#each')

What's the proper way to change views in Ember, based on an action from the controller?

I currently have an action set up in a template for which the purpose is tracking a user's selection, and then changing pages based on that selection.
This is the applicable portion of my router:
this.resource('simpleSearch', function() {
this.resource('simpleSearchOption', {path: ':simpleSearchOption_id'});
Here's the action:
<div {{action "select" this}} class="questiontile">
And here's the controller:
App.SimpleSearchOptionController = Ember.ObjectController.extend({
needs: ["simpleSearch"],
simpleSearch: Ember.computed.alias("controllers.simpleSearch"),
actions: {
select: function(optionId) {
var nextOptionId = parseInt(this.get("id")) + 1;
var numOfOptions = this.get('simpleSearch').get('model').length;
if(nextOptionId < numOfOptions) {
console.log('going to next option');
/** What do I do here?
* This is my current implementation,
* and it works, but is it proper?
*/
this.transitionToRoute('/simpleSearch/' + nextOptionId);
}
}
}
});
The next page is basically the next index up an array of objects which is the model for the parent route/controller/view.
How I'm doing it at the moment is working - but is that proper? Is it 'Ember idiomatic'?
Sorry about previous post, accidentally deleted it!
The transitionToRoute takes two arguments, first is the resource/route name and the second is the model. So this should work
actions: {
select: function(optionId) {
var nextOptionId = parseInt(this.get("id")) + 1;
this.store.find('simpleSearch', nextOptionId).then(function(model){
this.transitionToRoute('simpleSearchOption', model);
});
//OR MAYBE YOU COULD GET IT FROM THE PARENT CONTROLLER??
/*
MAYBE
this.get('simpleSearch.content').forEach(function(model){
if(model.get('id') === nextOptionId){ do transition}
else{ alert some msg!! }
})
*/
}
}
More info here : http://emberjs.com/api/classes/Ember.Controller.html#method_transitionToRoute

How can I programmatically add/remove models to a controller?

This shouldn't be too hard.
I have a datepicker UI widget, and each time the user clicks on a month, I want to add or remove that month from the MonthsController (an ArrayController). The MonthsController is not associated with a route, so in my ApplicationTemplate I simply have
{{render months}}
A simplified version of my datepicker view is
App.DatepickerView = Ember.View.extend({
click: function(e) {
var id = $(this).datepicker().data('date').replace(" ", "-");
this.get('controller.controllers.months').toggleMonth(id);
}
});
and I handle the event in my MonthsController:
App.MonthsController = Ember.ArrayController.extend({
toggleMonth: function(id) {
var month = App.Month.find(id),
index = this.indexOf(month);
if (index === -1) {
this.pushObject(month);
} else {
this.removeAt(index);
}
}
});
I thought I had this working, but then I realized that month in the last snippet wasn't really an App.Month, it was just (I suppose) an anonymous object.
How can I programmatically add/remove models to a controller?
Your App.Month.find(id) will return a promise. If that month hasn't loaded yet you would also be loading this data from the server. You need to wrap your code in the promise's then.
toggleMonth: function(id) {
var _this = this;
App.Month.find(id).then(function(month) {
var index = _this.indexOf(month);
if (index === -1) {
_this.pushObject(month);
} else {
_this.removeAt(index);
}
});
}

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.