0
votes

I've got a sticky situation that I keep on running into: The need for a new instance of a controller inside a handlebars template.

Here is a brief example of my situation. (Please excuse my use of coffeecript)

In Ember, I have a model:

App.Foo = DS.Model.extend

  attr: DS.attr()
  ...

Which I load from an endpoint etc.. And place into an array controller:

App.FooArray = Ember.ArrayController.extend

  ###*
   * Array of App.Foo
   * @type {Array}
   */
  content:

  method: ->
    ...

Finally, I have an 'instance' controller for this model, which implements further methods (i.e. this is not a singleton controller as would be found at the router level, but a decorator (or proxy) that augments the model with added methods and event handlers):

App.FooController = Ember.ObjectController.extend

  ###*
   * Content
   * @type {App.Foo}
   */
  content: null

  action: ->
    ...

In handlebars, I want to iterate over items in an App.FooArray:

{{#each myFooArray}}
  Hi! My attr is {{attr}}
{{/each}}

etc.. This works splendidly for parameters and such.

However, the trouble starts when I want to use actions (or other properties which would belong to a FooController)

{{#each myFooArray}}
   Hi! My attr is {{attr}} <a {{action 'action'}}>Action me!</a>
{{/each}}

Suddenly my actions are not working. That's because the action helper doesn't apply the action to 'this' but rather to a controller higher up, possibly even at the Route level!

So to work around this, I need to pass a target (i.e. a controller):

{{action 'action' target=**********}}

Well, the controller I want is an instance of App.FooController. Up until now, I've been instantiating controllers inside the model (yuck!):

App.Foo = DS.Model.extend

  attr: DS.attr()
  ...
  attrn: DS.attr()

  myController: Ember.computed (->
    App.FooController.create
      content: this
  )

and thus iterating as follows:

{{#each myFooArray}}
  Hi! My attr is {{attr}} <a {{action 'action' target=myController}}>Action me!</a>
{{/each}}

I know this is bad, but I can't think of a better way. Somebody, please help me see the light!

2
I think that using Ember.ObjectProxy isn't a good idea. Are you using the ObjectProxy just to don't have singletons controllers?Marcio Junior
Some models need a controller for every instance (for example the items in a newsfeed?) The controller will contain certain properties related to the object which may not necessarily need to be persisted to the store. Is it possible to have one single controller for many individual model items?Alexandros K
The model is where the state is hold. And the controller just provide additional methods like userIsAdmin etc. So it's a good pratice to not store data in the controller. The Ember.ObjectController is a controller that wrap a model, so in your template when you refenrence something like {{input type="text" value=name}}, it will first search the name property in the controller, if isn't found, it will delegate to the model. I think that is a good idea to use the Ember.ObjectController.Marcio Junior
Yes, you are right I have changed my question to use ObjectController. Even so, there is still the problem: how to get the controller in the #each helper?Alexandros K

2 Answers

0
votes

You can explicitly set the itemController in your each loop.

{{#each myFooArray itemController="foo"}}
  Hi! My attr is {{attr}}
{{/each}}
0
votes

This question poses an important and longstanding question about ArrayControllers, CollectionViews, Models and ObjectControllers.

At the time of writing, my knowledge of the inner workings of Ember was limited. However, I can rephrase my question more concisely as follows:

Given an ArrayController, CollectionView and instance controllers for a model, how can one leverage the itemControllerClass property of the ArrayController to iterate over its content and wrap each item in a unique (i.e. non-singleton) instance of itemController?

Turns out this problem is longstanding and the solution echoes @jeremy-green but I will expand on things here.

First off though: the Ember support thread that encapsulates the problem: https://github.com/emberjs/ember.js/issues/1637

I think the discussion there points very clearly to the need for non-singleton controllers in certain situations.

As well, here is the documentation for ArrayController that indicates the presence of an 'itemController' property on the ArrayController: http://emberjs.com/api/classes/Ember.ArrayController.html#property_itemController

Looking further into the docs, you will also note the presence of an 'lookupItemController' function: http://emberjs.com/api/classes/Ember.ArrayController.html#method_lookupItemController

These functions are for the express purpose of returning the content as an array of Controllers but how?

Well the first requirement is to use the ArrayController directly as the content in a loop. Unfortunately this is where things start to fall apart.

You may think it would be simply the case that a CollectionView can be used:

{{view myCollectionView controllerBinding=myArrayController}}

or

{{view myCollectionView contentBinding=myArrayController}}

But unfortunately this is not the case. More so in the situation where you are rendering a Controller on another Controller's route. CollectionView does not preserve the relationship between the ArrayController and its 'itemControllerClass`

The only way to make this work, as @jeremy-green points out:

{{#each myFooArray itemController="foo"}}
  Hi! My attr is {{attr}}
{{/each}}

A more complete example would be:

<ol class="foo-item-list">
  {{#each controllers.foo_items}}
    <li>{{ view "foo" }}</li>
  {{/each}}
</ol>

Wherein App.FooItemsController has either the property itemController or lookupItemController defined.

Unfortunately in this situation we lose the benefits of using the tagName or emptyView properties of CollectionView. Hopefully if an 'ArrayView' is ever created, it will bring the best of both worlds to this situation!