Best practice for setting up a nested component's data


#1

Looking specifically for answers geared to Ember 2.0

I think it is pretty clear that the standard way to go is that the route sets up data and passes it into the component for rendering. But what about the case where one component is setting up another? For example, I have an application with a survey. A section hasMany questions. An answer belongsTo a question and an instance of the survey.

Route is surveys/1/instances/1/sections/1 and in the route I have

setupController: function(controller, model) {
  controller.set('attrs.section', model);
  controller.set('attrs.instance', this.modelFor('instance')); //
}

In the template (soon to be component), I want to

{{#each question in attrs.section.questions}}
  {{app-question question=question answer=answer answerChanged='answerChanged'}}
{{/each}}

But I want to run some code to setup answer to pass in values to the app-question component. Namely, I want to look in the store and see if there is already an answer linked to that question and instance, and create it if not, so that it can be bound to an input in the app-question component.

If I were rendering this from a route, I would do it in the model hook, so what is the equivalent way to set up some data when rendering a nested component? didInsertElement? Pass in the question and the instance and do it in the .on('init') in the `{{app-question}} component?


#2

I’ll add that I know this could be done in .on('init') inside the child component, but that bothers me because the way I understand it is that components are supposed to be supplied data, not set up their own. Having a component always be supplied data allows it to be more re-usable I think


#3

I hope this question is clear. A more basic way to put it is, when we are rendering a collection of nested models in an {{each}} helper, what is the preferred way to set up the context?

When setting up the context in a route, we rely on the model and setupController hook. Inside of an {{each}} helper, we typically have the model passed down from above (i.e. {{#each comment in posts.comment}}{{app-comment comment=comment}}{{/each}}

But what is the recommended equivalent to setupController to set up the component with some information that is specific to both the surrounding context and the specific comment inside of the each?

I’ve been experimenting with the .on('init'), and sending actions back up to the route, but it is trickier to manage and ensure things are properly loaded this way, and I’m thinking there must be a more straightforward declarative way to do this.


#4

Hey @leejt489, since you’re focusing on future Ember here, you can use a new feature called Block Parameters. Here’s a JSBin that illustrates how to send data out from a parent component and pass it into a child: http://emberjs.jsbin.com/baferezinu/1/

Is that along the lines of what you’re looking for?


#5

@heycarsten Thanks for your reply. I understand how to pass the data down; that’s not a problem. It’s more that what if I want to execute some logic to set a property (e.g. commentTitle) on the child component that has to be computed from information in both the child and the parent. One solution is to pass in the parent and compute commentTitle in the child. But then say I want to use the child component in a different context with a different type of parent and in this context the commentTitle property is computed in a different way. So now it doesn’t make sense to store the logic to set commentTitle on the child.

Right now I am listening to .on('init') in the child and sending an action back up to the parent with a reference to the child component as an argument and using that to set up these properties and it works, but what I would really want is something where I could define computed properties that are bound within the each, something like:

eachComputedProperty: function(item) { //item is passed in
  var prop = item+1; //execute some logic here
  return prop;
}.property('childItems.@each')

and then

{{#each childItems as |item|}}
  {{eachComputedProperty}}
{{/each}}

…now that I think about it this way, it could probabaly be done with a custom helper


#6

@leejt489 if the way the commentTitle is computed depends on the parent component that wraps it, you could always override the _yield function to pass a pointer to either a). the parent component (achieving the same effect as passing the component itself as an attribute of the child component without having to actually pass it manually each time) or b). a function that can be used to set the commentTitle.

I did just this today. I looked through the Ember 1.10 source and found that the {{yield}} makes a call to the component’s private _yield function:

_yield: function(context, options, morph, blockArguments) {
        var view = options.data.view;
        var parentView = this._parentView;
        var template = get(this, 'template');

        if (template) {
            Ember.assert("A Component must have a parent view in order to yield.", parentView);

            view.appendChild(Ember.View, {
                isVirtual: true,
                tagName: '',
                template: template,
                _blockArguments: blockArguments,
                _contextView: parentView,
                _morph: morph,
                context: get(parentView, 'context'),
                controller: get(parentView, 'controller'),
                component: view //My custom property that gets set on each child view
            });
        }
    }

My child component can access the property in the template via {{_view._parentView.component}} or in the component via this._parentView.component. If you follow this approach, make sure you place test coverage around the yield code because I have seen this helper change. You can also add an Ember.assert to make sure the component property is available to the child component if its required, or have a default mechanism for setting the commentTitle if it’s not present