Deeply nested controllers

What is the best way to nest controllers greater than one level deep.

Say I have a list of projects and each project has a list of todos.

How would one set it up so that the list of projects, each project, each list of todos and each todo is represented by a controller.

This is what I have so far but I feel there should be a more efficient/simpler solution.

//controllers/projects.js
export default Ember.ArrayController.extend({
  itemController: 'projects/project'
});

//controllers/projects/project.js
export default Ember.ObjectController.extend({

  todos: function() {
    return this.container.lookupFactory('controller:todos').create({
      parentController: this,
      contentBinding: 'parentController.model.todos',
    });
  }.property()
  
})

//controllers/projects/project/todos.js
export default Ember.ArrayController.extend({
  itemController: 'projects/project/todos/todo'
});

//controllers/projects/project/todos/todo.js
export default Ember.ObjectController.extend({
})

//models/project.js    
export default DS.Model.extend({
  name: DS.attr('string'),
  todos: DS.hasMany('todo'),
});    

//models/todo.js
export default DS.Model.extend({
  description: DS.attr('string')
});

I almost wrote a post asking this exact same thing last week.

First, depending on your use case, it might be worthwhile to look at this post. I suggested a solution that looks very similar to yours…and it got way poo-pooed. But the alternative proposed was a much better solution and has worked for 99% of my use cases.

However, I ran into this again where I didn’t really have the ability to handle this in the router (as what I build is really meant to be a component; but there’s no ItemComponent or ArrayComponent). I found a solution that’s a hack; but it works for me. (Can’t wait to see if there’s other solutions).

Essentially, rather than making the itemController an instance of an ObjectController, I make it an ArrayController…and use one of the private hooks for setting up the content to change the content to the related hasMany array. (that probably doesn’t make sense…Hard to describe)…

Here’s kinda what that would look like in your situation.

// Controllers/projects/projects.js
export default Ember.ArrayController.extend({
  itemController: 'projects/project/todos'
});

// Controllers/projects/project/todos.js
export default Ember.ArrayController.extend({
  project: null,
  itemController: 'projects/project/todo',
  _setupContent: function() {
    var content = this.get('content');

    if(content && Ember.isArray(content)) {
      var project = content;
      content = project.get('todos');
      this.set('project', project);
      this.set('content', content);
    } else {
      this._super.apply(this, arguments);
    }
  }
});

The net effect is that you’d no longer need the projects/project controller. But this also means that your project is no longer wrapped in a controller (which sorta brings us back to this issue all over again).

Anyway, food for thought.

@Spencer_Price thanks for the post. I setup the controllers in the router when possible. In this case I need each project to be a object controller and each todo list to be an array controller. I hope someone comes up with a elegant solution to this.

I totally agree.

Well, you can take the basic idea around what you’re doing and roll it into a computed property utility or something. Like:

// utils/subArrayController.js
export default function subArrayController(modelProp, controller) {
  return function() {
    return this.container.lookupFactory('controller:'+controller).create({
      container: this.container, 
      model: this.get(modelProp),
      parentController: this
    });
  }.property(modelProp);
}

So your Project controller would then look like:

// controllers/projects/project.js
import subArrayController from '../utils/subArrayController';
export default Ember.ObjectController.extend({
  _todos: subArrayController('todos', 'projects/project/todos')
});

Still not ideal…but…perhaps a bit more reusable?

Hopefully others will chime in…