Ember: property in array controller computed from property of item controller - ember.js

I have a typical ArrayController/ObjectController setup as follows:
App.PlayersController = Ember.ArrayController.extend({
itemController: 'playerItem',
visibleCount: function(){
//How do I compute this property?
}.property(...),
});
App.PlayerItemController = Ember.ObjectController.extend({
needs: 'players',
visible: function(){
...
}).property('...')
})
The visible property is computer on the PlayerItemController, but I want to have a global count of visible players from the visibleCount in the PlayersController. How do I achieve this?
Here's what I've tried:
visibleCount: function(){
return this.get('content').filter(function(p){
return p.get('visible')
}).length;
}.property('content.#each.visible'),
This doesn't work, as p.get('visible') returns undefined every time and the property returns 0.

You can trigger this completely off the array controller instance itself, since that's where the itemController is defined. Any time you attempt to iterate the this inside the array controller you will get the individual item controllers.
This makes just as much since when you think about it in the context of the template. If you iterate the controller, you get the item Controllers
{{#each item in controller}}
yay I have an item controller
{{/each}}
whereas if you just iterate the model/content you just get that record back
{{#each item in model}}
boo, boring model
{{/each}}
App.PlayersController = Ember.ArrayController.extend({
itemController: 'playerItem',
visibleItems: Em.computed.filterBy('','visible', true),
// you don't need this below, you could just do, `visibleItems.length` in the template
visibleCount: Em.computed.alias('visibleItems.length'),
// showing off getting item controller
randomProp: function(){
this.forEach(function(ic){
console.log(ic.get('visible'));
});
}.property('#each.visible')
});
Example: http://emberjs.jsbin.com/dalazawe/1/edit
It's really cool if you think of it in the way it's implemented. Essentially the array controller overrides how it returns items from the controller to you. When you attempt to get objectAt(1), if you've defined an itemController it will wrap the underlying object in the itemController and return it. Or if you haven't defined an itemController, it just returns the object to you. Most people are stuck on it just being how it's implemented in the template but that's just half of it, it's how the array controller returns objects to you.
Example: http://emberjs.jsbin.com/dalazawe/2/edit

Related

Ember 2.3 how to use itemControllers in each loops?

Before anyone brings up components, I must state that I am aware that Ember is moving away from controllers and views completely and adopting the component structure. Right now, I am compelled to use controller/view in ember2.3 using the legacy-controller and legacy-view addons that have been provided here:
https://github.com/emberjs/ember-legacy-controllers
https://github.com/emberjs/ember-legacy-views
as part of the process to upgrade to Ember 2.3 (from 1.7).
Now, I have a route called recordTypes, which lists all recordTypes. in the legacy code, each recordType was then associated with an itemController 'recordType'. Like so:
{{#each result in searchResults itemController="recordType"}}
...
{{/each}}
Surprisingly, this legacy syntax for Ember did not render anything to the page, but the following one did:
{{#each searchResults itemController="recordType" as |result| }}
...
{{/each}}
The itemController recordType is a legacy Object Controller and the recordTypes controller itself is a legacy Array Controller.
Now, for each result I have a few actions that can be performed. For example, on clicking the result, the editResultName action was to be fired. This action, in the legacy code, was in the recordType controller. Therefore, clicking the item in the recordTypes page would then defer this action to the recordType controller, which would then happily handle the rest.
This is not being fired in ember2.3, even with the legacy controllers. What surprises me more is that this code can be found in ember-legacy-controller.js
export default {
name: 'ember-legacy-controllers',
initialize: function() {
/**
Adds support for ArrayController in the legacy {{each}} helper
*/
Ember._LegacyEachView.reopen({
_arrayController: computed(function() {
var itemController = this.getAttr('itemController');
var controller = get(this, 'container').lookupFactory('controller:array').create({
_isVirtual: true,
parentController: get(this, 'controller'),
itemController: itemController,
target: get(this, 'controller'),
_eachView: this,
content: this.getAttr('content')
});
return controller;
}),
_willUpdate(attrs) {
let itemController = this.getAttrFor(attrs, 'itemController');
if (itemController) {
let arrayController = get(this, '_arrayController');
set(arrayController, 'content', this.getAttrFor(attrs, 'content'));
}
}
});
}
};
Here, it does have a line that references the itemController. However, when this list of searchResults is rendered, and a result is clicked, the error I get is this:
Nothing handled the action 'editResultName'. If you did handle the action, this error can be caused by returning true from an action handler in a controller, causing the action to bubble.
The action is there for sure, but nothing in the itemController is being recognised. Unfortunately a lot of the legacy code I am updating has itemController loops and therefore it would be immensely helpful to be able to use itemController for the time being.
How can I use itemController like it used to be implemented?
Replacing an itemController.
Create a component from the contents inside the each helper. The itemController would become the js side of the component and the template code the template
From this:
{{#each result in searchResults itemController="recordType"}}
<span>result: {{result.title}}</span>
{{/each}}
To this:
{{#each searchResults as |result| }}
{{result-list-item result=result}}
{{/each}}

emberjs itemcontroller property scope

I need an explanation for the usage of an itemcontroller in emberJS.
I created a handlebars template that looks like this:
{{#each thing in controller itemController="itemController"}}
{{view "testview" contentBinding="thing"}}
{{/each}}
The testview creates a html table and within the testview I use a second view in a each loop which creates several tr:
{{each item in view.content.thing}}
{{view 'trview' contentBinding="item"}}
{{/each}}
In addition to that I added a property "listOfProperties" (Ember.A()) to the itemController.
I use the click function of the trview to add a value to the "listOfProperties" array of the itemController.
And here I receive an error: If I click on a tr, the value is added to each itemControllers "listOfProperties" array and not only to one "things" itemController.
I'm going to guess since you didn't include your item controller, but it's likely you're running into a reference issue. Array's are a reference and as such are shared across instances of item controllers.
App.ItemController = Em.ObjectController.extend({
setup: function(){
this.set('listOfProperties', []);
}.on('init'),
listOfProperties: null,
});

How to use itemControllerClass to achieve a singleton like experience with ember.js

I've got a fairly simple form but it should never carry any state with it. I started reading an older discussion about how you can use itemControllerClass to get a "singleton" class created each time you enter the route.
If I want to use this how would I plug this into the template and wire it up from the parent controller?
Here is what I'm guessing you would do from the javascript side
App.FooController = Ember.Controller.extend({
itemControllerClass: 'someclass_name'
});
The only "must have" is that I need to have access to the parent route params from the child singleton controller.
Any guidance would be excellent -thank you in advance!
Update
Just to be clear about my use case -this is not an ArrayController. I actually just have a Controller (as shown above). I don't need to proxy a model or Array of models. I'm looking for a way to point at a url (with a few params) and generate a new instance when the route is loaded (or the parent controller is shown).
I'm doing this because the template is a simple "blank form" that doesn't and shouldn't carry state with it. when the user saves the form and I transition to the index route everything that happened in that form can die with it (as I've cached the data in ember-data or finished my $.ajax / etc)
Anyone know how to accomplish this stateless behavior with a controller?
I'm betting you're talking about this discussion. It's one of my personal favorite discoveries related to Ember. The outcome of it was the itemController property of an ArrayController; I use it all the time. The basic gist of it is, when you're iterating over an array controller, you can change the backing controller within the loop. So, each iterating of the loop, it will provide a new controller of the type you specify. You can specify the itemController as either a property on the ArrayController, or as an option on the {{#each}} handlebars helper. So, you could do it like this:
App.FooController = Ember.ArrayController.extend({
itemController: 'someclass'
});
App.SomeclassController = Ember.ObjectController.extend({});
Or, like this:
{{#each something in controller itemController='someclass'}}
...
{{/each}}
Within the itemController, you can access the parent controller (FooController, in this case), like:
this.get('parentController');
Or, you can specify the dependency using needs, like you ordinarily would in a controller. So, as long as the params are available to the parentController, you should be able to access them on the child controller as well.
Update
After hearing more about the use case, where a controller's state needs to reset every time a transition happens to a particular route, It sounds like the right approach is to have a backing model for the controller. Then, you can create a new instance of the model on one of the route's hooks; likely either model or setupController.
From http://emberjs.com/api/classes/Ember.ArrayController.html
Sometimes you want to display computed properties within the body of an #each helper that depend on the underlying items in content, but are not present on those items. To do this, set itemController to the name of a controller (probably an ObjectController) that will wrap each individual item.
For example:
{{#each post in controller}}
<li>{{title}} ({{titleLength}} characters)</li>
{{/each}}
App.PostsController = Ember.ArrayController.extend({
itemController: 'post'
});
App.PostController = Ember.ObjectController.extend({
// the `title` property will be proxied to the underlying post.
titleLength: function() {
return this.get('title').length;
}.property('title')
});
In some cases it is helpful to return a different itemController depending on the particular item. Subclasses can do this by overriding lookupItemController.
For example:
App.MyArrayController = Ember.ArrayController.extend({
lookupItemController: function( object ) {
if (object.get('isSpecial')) {
return "special"; // use App.SpecialController
} else {
return "regular"; // use App.RegularController
}
}
});
The itemController instances will have a parentController property set to either the the parentController property of the ArrayController or to the ArrayController instance itself.

Ember.js arraycontroller call from view

I might be using this all wrong, but:
I've got an ArrayController representing a collection of products. Each product gets rendered and there are several actions a user could take, for example edit the product title or copy the description from a different product.
Question is: how do you interact with the controller for the specific product you're working with? How would the controller know which product was being edited?
I also tried to create an Ember.Select with selectionBinding set to "controller.somevar" but that also failed.
I think the most important thing you need to do, is first move as much logic as you can away from the views, and into controllers.
Another thing that would be useful in your case, is to have an itemController for each product in the list. That way, you can handle item specific logic in this item controller.
I don't have enough information to understand your architecture, so I will make a few assumptions.
Given you have the following ProductsController:
App.ProductsController = Ember.ArrayController.extend();
You need to create a ProductController that will be created to wrap every single product on its own.
App.ProductController = Ember.ObjectController.extend();
You need to modify your template as follows:
{{#each controller itemController="product"}}
<li>name</li>
{{/each}}
Now every product in your list will have its own ProductController, which can handle one product's events and will act as the context for every list item.
Another option:
If you will only be handling one product at a time, you can use routes to describe which product you are working with:
App.Router.map(function() {
this.resource('products', { path: '/products' }, function() {
this.resource('product', { path: '/:product_id' }, function() {
this.route('edit');
});
});
});
And create a controller for editing a product:
App.ProductEditController = Ember.ObjectController.extend();
And your list items would link to that product route:
{{#each controller}}
<li>{{#linkTo "product.edit" this}}name{{/linkTo}}</li>
{{/each}}
If you define itemController on your ProductsController you don't need to specify that detail in your template:
App.ProductsController = Em.ArrayController.extend({
itemController: 'product',
needs: ['suppliers'],
actions: {
add: function() {
// do something to add an item to content collection
}
}
});
App.ProductController = Em.ObjectController.extend({
actions: {
remove: function() {
// do something to remove the item
}
}
});
Use a collection template like this:
<button {{action="add"}}>Add Item</button>
<ul>
{{#each controller}}
<li>{{name}} <button {{action="remove"}}>x</button></li>
{{/each}}
</ul>
The Ember documentation describesitemController here:
You can even define a function lookupItemController which can dynamically decide the item controller (eg based on model type perhaps).
The thing I found when rendering a collection wrapped in an ArrayController within another template/view is the way #each is used. Make sure you use {{#each controller}} as Teddy Zeeny shows otherwise you end up using the content model items and NOT the item controller wrapped items. You may not notice this until you try using actions which are intended to be handled by the item controller or other controller based content decoration.
When I need to nest an entire collection in another view I use the view helper as follows to set the context correctly so that any collection level actions (eg an add item button action) get handled by the array controller and not by the main controller setup by the route.
So in my products template I would do something like this to list the nested suppliers (assuming your route for 'product' has properly the 'suppliers' controller):
{{view controller=controllers.suppliers templateName="products/suppliers"}}
The suppliers template just follows the same pattern as the template I show above.

How do I make a computed, filtered property?

I've got something like this:
Epic = Ember.Object.extend({
children:[],
children_filtered: function(){
return this.get("children").filterProperty("archived",false);
}.property("children"),
init: function() {
this._super();
this.set("children", Ember.ArrayController.create({content:[]}) );
this.set("stories", Ember.ArrayController.create({content:[]}) );
},
});
Note the children_filtered computed property.
If I use children_filtered in a view...
{{#each content.children_filtered }}
hi
{{/each}}
My application hangs with cpu # 100%
Any ideas what I'm doing wrong? Is there a better pattern to follow for an object that has a list of items plus a list of filtered items?
Your problem is that you need the computed property to be set as cacheable. Otherwise, its value is recomputed upon every iteration of the #each. There has been discussion about whether cacheable should be the default for all computed properties.
children_filtered: function(){
return this.get("children").filterProperty("archived",false);
}.property("children").cacheable()
Here's a jsFiddle example: http://jsfiddle.net/ebryn/9ZKSY/