Working with nested controllers


#1

I’d like to discuss best practices for how we should deal with nested array controllers. The use case I have in mind concerns communicating between a post controller and its child comment controllers in a template like this:

{{#each posts itemController="post"}}
  {{#each comments itemController="comment"}}
    ...
  {{/each}}
{{/each}}

I’ve been thinking that it might make sense to include a controllerName property to the each helper that would work similar to the viewName property in the view helper. Here is an example of how it might be used:

{{#each posts itemController="post"}}
  {{#each comments itemController="comment" controllerName="comments"}}
    ... {{input type="checkbox" checked=isSelected}} ...
  {{/each}}
{{/each}}
App.PostController = Em.ObjectController.extend({
  isSelected: function() {
    this.get('controllers.comments').anyBy('isSelected')
  }.property('controllers.comments.@each.isSelected')
});
App.CommentController = Em.ObjectController.extend({
  isSelected: false
});

I also imagine that you could optionally explicitly define the array controller if you need further customization:

App.CommentsController = Em.ArrayController.extend({
  ...
});

Does this change seem reasonable? Are there any existing alternative approaches for this use case that I haven’t figured out yet?

Thanks for reading!


#2

It is not needed,

Ember maintains a relationship between the parent and its sub controllers, First, you can easily access a parent controller from a child by using the parentController property, this is actually documented here: http://emberjs.com/api/classes/Ember.ArrayController.html

The itemController instances will have a parentController property set to either the the parentController property of the ArrayController or to the ArrayController instance itself.

Now here’s the tricky part, when you set itemController on the each helper level, it will auto generate a parent controller for this specific iteration context, but referencing it is not trivial,

If you need both relations, you should avoid setting itemController on the each helper level and specify the child controller in the parent controller by:

App.ParentController = Ember.ArrayController.extend({
    ...
    itemController: 'child'
})

This different from specifying the item controller on the each level as you will be able to access the sub controllers from this parent controller by the ‘subControllers’ property.

Goodluck,


Ember for Bootstrap: https://github.com/ember-addons/bootstrap-for-ember

Asaf.


#3

I fail to see how parentController would allow you to implement the App.PostController in the post above. The feature of note here is that you are referencing the children from within the logical parent controller which is probably an object controller (ie. PostController) and not the immediate array controller (ie. CommentsController).

The purpose of controllerName is to give a reference of the immediate array controller, generated or explicit, to the ambient, logical parent controller.


#4

Here is an image that I hope makes the problem clearer. This shows the possible interactions between the levels of the tree.

Note: Technically the right side of the graph is not correct because there is no CommentsController when using {{each}}, so the parentController is actually post.


#5

I am very interested to see what comes of this discussion. I think the topic of nesting can be very nuanced. So this discussion is helpful.

Nested ArrayControllers is an interesting idea. And your diagram helps. I could see a use where you have nested collections of nested collections. Especially if the collections themselves are generated from computed filters.

The idea being that one collection (ArrayController) represents a set of records with one type of filter, and another collection (ArrayController) represents another set of records with a different filter. The individual models are the same type but the collection (ArrayController) that they exist in may be different.

Navigating such a nested structure could be very powerful. And the rationale for nested collections is that it may be expensive to create filtered collections so it would be nice to maintain state in separate array collections that share the same type of models.

So using the example Posts --> Comments the filters might something like this.

Posts (Abstract ArrayController)

PostsA (Concrete ArrayController that Inherits from Posts, and is filtered to show only "A Topics")

PostsB (Concrete ArrayController that Inherits from Posts, and is filtered to show only "B Topics")

and so on.

Another way of asking this is how might one model the Reddit interface as an ember application?

Here is your reddit data API:

http://www.reddit.com/r/emberjs.json

http://www.reddit.com/r/angularjs.json

Start with the above topics and then branch off to cover n number of topics on reddit.

@mmun have you considered drafting up a cookbook that addresses this? It might a good way for the community to get some clarity on the different nesting use cases.


#6

Hi @eccegordo. I’ve made a pull request that tries to address this concern in a different way. See: https://github.com/emberjs/ember.js/pull/3424. I decided that it would be more flexible to solve the problem as a computed property rather than in the template. Hopefully that PR will generate some further discussion.


#7

I realise this is an old topic but handling nested content is not directly covered AFAIKT in the guides.

I also noticed that the array controller is never in the view context by default.

I think you can achieve the result you are looking for as follows:

App.PostsController = Em.ArrayController.extend({
      itemController: 'post'
});

App.PostController = Em.ObjectController.extend({
  needs: [ 'comments'],
  isSelected: function() {
    this.get('controllers.comments').anyBy('isSelected')
  }.property('controllers.comments.@each.isSelected')
});

App.CommentsController = Em.ArrayController.extend({
      itemController: 'comment'
});

App.CommentController = Em.ObjectController.extend({
      isSelected: false
});

Then in the post template:

{{title}}
{{body}}
{{etc}}
{{#view controller=controllers.comments}}
  {{#each controller}}
   ...
  {{/each}}
{{/view}}

The important thing I found is that without the use of the view helper the comments array controller context was completely out of the picture when #each is used. Using view sets the context to the array controller, and using #each on controller and NOT content things just work.

The router does however need to setup the comments controller for the post in setupController. eg:

setupController: function(controller, model) {
  this.controllerFor('comments').set('model', model.get('comments'));
  this._super.apply(this, arguments);
}

The above works for the singular post route, but since the original question is trying to do this for a collection of posts some more work is needed in the route. You will probably have to create a comments controller for each post and set comments directly on the post controller, rather than relying on needs since AFAIKT there would only be a singular ‘comments’ controller and that wouldn’t work for multiple posts.

I would happy to be enlightened on how to make this work better if anyone else has a better suggestion.

I’m still not satisfied this is the right approach when dealing with nested object graphs/complex resources. The user interface cannot be functional if everything is decomposed to addressable router based fragments as you can only deal with a single slice of something at a time. What if I have say a person that has multiple addresses, multiple emails or multiple other related important things and I want it all presented / editable. I won’t be using lots of routes to see just a slice of a person when those things are logically part of the person. Having to fetch a model and then construct a controller decorator hierarchy manually is not really a good solution.

I am thinking there should be a way similar to needs where the controller can declare that it wants nested controller facades created for certain content properties rather than the router having to do it. I’m relatively new to Ember so there may be a very simple way to do that but I’m not aware of one yet having poured over the documentation.

UPDATE: I’m using the code from @mmun’s PR above and its working terrifically. Its a lot saner than the combination of needs (and some alias properties) and manually setting up the nested controllers in routes. Thanks @mmun ! I just hope something this useful makes it into Ember proper, its an amazingly simple yet powerful approach for decorating model properties as nested controllers. I still need the view helper/wrapper for setting context around {{#each}} but the combination of Em.computed.controller and the view helper works well for rendering and interacting with arbitrarily nested/embedded object graphs at an Ember route endpoint.


#8

@ahacking I’m glad to hear that. You may be interested to know that @jayphelps released a small bower component that encapsulates that PR: https://github.com/jayphelps/ember-computed-content-controller.


#9

as an ember noob, I’m not sure if the solution provided here is gonna be useful for my need, so let me ask some questions.

I have a MonthController. The model for month controller has some properties as meta data and also has an array of logs. The logs have each their own controller which decorates the model with additional computed properties. I render the logs inside my month template with following syntax:

{{#each log in logs}}
    {{render "log" log}}
{{/each}}

this will render the log template using the LogController. I have to do it like this because the day template is also used on some other view and I want to avoid duplication.

This is all fine and working. Now I want to access one of the computed properties of the LogController in my MonthController for aggression reasons.

lets say my LogController calculates the sum of age of all logs it has into one property and I want to calculate the sum of all sums of ages in my MonthController.

Can I use this feature to access and walk through all nested LogControllers in my MonthController? and if so, then how should I replace that {{render "log" log}} tag with a proper one?


#10

Cheers all, don’t mean to necro an older thread, but this use case came up in a project of ours back in January with our initial adoption of ember, and I’ve been casually following it since. With @mmun’s original rejected PR, I was hoping for an official solution to be introduced into the proper Ember APIs - but I have not seen anything on the topic yet. We’ve deployed a modified version of the @jayphelps implementation, but have run into a number of issues with controller state resets on api calls, and seemingly heavy, redundant controller recomputations.

I think there is enough buzz around this justifying the use-case. Do we have an alternative, canonical approach to its implementation, or have we resigned to the computed controller solution as good enough?


#11

We’ve deployed a modified version of the @jayphelps implementation, but have run into a number of issues with controller state resets on api calls, and seemingly heavy, redundant controller recomputations.

@eriknelson Mind creating an emberjs.jsbin.com demoing the issues you’ve been having with the content controller technique? I’ve been using it tons without noticing any issues. It would be cool to know what kind of modifications you needed to make.