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.
Related
I am trying to do this: I have a model called 'trip', and inside trip, an attribute called 'createdToday', which returns the date when a trip is created. What I want is to return a list of trips that were made today.
Here is my trip model:
import DS from 'ember-data';
export default DS.Model.extend({
driver: DS.belongsTo('driver', {
async: true,
inverse: 'trip'
}),
..... etc .......
createdAt: DS.attr('string', {
defaultValue() {
return new Date();
}
}),
isBookedToday: function(trip) {
var today = new Date().toDateString();
return (today === trip.get('createdAt').toDateString);
},
getTripsToday: Ember.computed('trip.#each.createdAt', function() {
var tripsToday = this.get('trip');
return tripsToday.filterBy('isBookedToday', true).get('length');
})
});
In my isBookedToday, I'm trying to see if an individual trip's created time is the same as todays time, and in getTripsToday, I am trying to loop through all the trips and filtering by isBookedToday.
And in my .hbs file, I'm saying: {{trips.getTripsToday}}, which won't render anything, so something's wrong.
I guess I am most confused at Ember's #each and exactly how it works.
Thanks for any feedback.
First you have to understand that your Trip Model instances represents a single Trip! Its absolutely not the right place to put a function that gives you a filtered list of trips!
Next isBookedToday is a normal function not a Computed Property. So you can't filterBy on it.
You may want to implement a isBookedToday on your trip, but you definitely have to filter your trips on the same place where you fetch them! Probably in a model() hook or a Computed Property on a component or a controller.
So you could do but don't need to do in your models/trip.js:
...
isBookedToday: Ember.computed('createdAt', {
get() {
let now = new Date();
let created = get(this, 'createdAt');
return now.getFullYear() === created.getFullYear() &&
now.getMonth() === created.getMonth() &&
now.getDate() === created.getDate();
}
})
...
And then in your model hook:
model() {
return this.store.findAll('trip').then(trips => trips.filterBy('isBookedToday'));
}
Or in a Computed Property in a controller or a component:
tripsToday: Ember.computed('trips.#each.isBookedToday', {
return get(this, 'trips').filterBy('isBookedToday');
})
Be careful. This will result in confusing things if you leave the page open overnight! when your date changes the Computed Properties will not recompute automatically!
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
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);
}
}
});
I wanna create a property that depends on a global attribute:
App.Test= Em.Object.extend();
App.Test.reopenClass({ all: Em.A() });
App.Other = Em.object.extend({
stuff: function() {
return "calculated stuff from this.get('foo') and App.Test.all";
}.property('foo', 'App.Test.all.#each.bar')
});
As a workarround I could create a observer and always set a dummy property with a new random value to trigger the property change, but is there a better way to do this?
I need this for some caching. I've a really crazy, and single threaded backend. So I write my own Model classes. So I try to reimplement a bit of the logic in the client for a better caching.
Ive an Item class (App.Item) and another class where each instance has a calculated reduced list of Items.
App.Model = Em.Object.extend({
});
App.Model.reopenClass({
all: Em.A(),
load: function(hash) {
return this.get('all').pushObject(this.create(hash));
}
});
App.Item = App.Model.extend({
});
App.List = App.Model.extend({
loadedInitItems: false,
items: function() {
if(!this.get('loadedInitItems')) { this.set('loadedInitItems', true); Backend.call('thelist', function(item) { App.Item.load(this); }); }
return App.Item.all.filter(function(item) {
// heavy filter stuff, depends on a lot of propertys on the current list instance
});
}.property('someprops', 'App.Item.all.#each.foo')
});
Backend.call represents some AJAX stuff
the point is, that now any item could change so that the filter will return something diffrent. And there are other places om the application, where the user can add Items. I dont want to call the backend again, because its very slow! And I know that the backend will not modify the list! So I wanna cache it.
This is just a reduced example of my use case, but I think've described the point. In reallity I have this dozend of times, with over 25000 objects.
have you tried adding 'Binding' to your property and then the value you want to bind to ?, something like this:
App.PostsController = Em.ArrayController.extend({
nameOfYourVariableBinding: "App.SomeObject.propertyYouWantToBindTo"
})
It looks like the problem is the double uppercase letter. So App.test ist working, but not App.Foo.test.
But I was able to find a Solution with the ArrayProxy.
Its about this:
App.Model = Em.Object.extend({
});
App.Model.reopenClass({
all: Em.A(),
load: function(hash) {
return this.get('all').pushObject(this.create(hash));
}
});
App.Item = App.Model.extend({
});
App.List = App.Model.extend({
loadedInitItems: false,
items: function() {
var self = this;
if(!this.get('loadedInitItems')) {
this.set('loadedInitItems', true);
Backend.call('thelist', function(item) {
App.Item.load(this);
});
}
return Em.ArrayProxy.extend({
content: App.Item.all,
arrangedContent: function() {
return this.get('content').filter(function(item) {
// heavy filter stuff, depends on a lot of propertys on the current list instance
// use self.get('someprops')
})
}.property('content.#each.foo')
});
}.property('someprops')
items: function() {
if(!this.get('loadedInitItems')) { this.set('loadedInitItems', true); Backend.call('thelist', function(item) { App.Item.load(this); }); }
return App.Item.all.filter(function(item) {
// heavy filter stuff, depends on a lot of propertys on the current list instance
});
}.property('someprops', 'App.Item.all.#each.foo')
});
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() {
// ...
}
});