Currently, there are several awkward points to Ember.js controllers. Namely:
- Passing controllers around to view's not instantiated by the routing system is hard.
- Non-singleton controllers are very difficult to manage (e.g. #each where each view has it's own controller).
- Sharing data between controllers is difficult.
Consider the following observations:
- Controllers should not reference each other.
- Each controller corresponds to a single view or parent-child hierarchy of views.
Based on the the above two points, there is no need for controllers to be "injected" at application initialization time. As it stands, the controller is only set once in the connectOutlets
method and it's view
property is only set to a single view. There is no need for the controller to exist outside of the lifecycle of a view.
A trivial implementation of this would be to just have the view's controller
property do the instantiation:
controller: Ember.computed(function(key, value) {
var parentView, controllerClass;
if (arguments.length === 2) {
return value;
} else if (controllerClass = get(this, 'controllerClass')) {
return controllerClass.create({view: this, router: get(this, 'router')});
} else {
parentView = get(this, 'parentView');
return parentView ? get(parentView, 'controller') : null;
}
}).property().cacheable(),
The controller could also be destroyed with the view.
Although controllers should not reference each other, the underlying data is shared amongst controllers. Examples of such data:
- The current user
- Application settings
- The root model object in a hierarchy of routes/controllers (e.g. '/post/:post_id', '/post/:post_id/comments', '/post/:post_id/edit', '/post/:post_id/contributors/:contributor_id')
In the last example, all of those routes could potentially point to different controllers, and all of the controllers could presumably need access to the same underlying post model.
The solution here could be to inject the model into the controllers.
A trivial implementation of this would be to just set the model on an object that is used as an injection context inside of connectOutlets
:
post: Em.Route.extend({
connectOutlets: function(router, post) {
set(router.injectionContext, 'post', post)
[...]
},
})
Then in the controller:
App.PostsController = Em.Controller.extend({
postBinding: 'router.injectionContext.post'
[...]
});
App.CommentController = Em.Controller.extend({
postBinding: 'router.injectionContext.post'
[...]
});
I agree that having the view contain the controller creation logic is slightly weird. Another alternative would be for the view to request a controller from the router. E.g. the controller computed property would call
router.createController(this)
or something.