How to communicate to child components

In anticipation of the switch to using components everywhere in Ember 2.0, I am trying to architect my app to use components all the way down in the view layer.

I have a route which essentially contains a form which is organized in a tab view where each tab has a collection of fields. So I have a parent form component which contains a tabs component and then on each tab there are multiple field components.

Each field is passed an initial value in its attributes. When the user edits the value in a field, the new value is passed back up the stack via an action. The field component also maintains some internal state about whether it has been edited so that some different styling is applied to edited fields.

My difficulty now is with the implementation for a Reset button on the form which will cause all the changes to be thrown away and the fields all to be reset to their ā€˜uneditedā€™ state. One approach would be push the maintenance of the state of all the fields up the tree to the form so that it then gets communicated back down to all the fields via attributes on the components. In this way the Reset action will change the ā€˜editedā€™ state of all the fields it maintains and the field components would respond via the changes to the attribute values.

But it seems to me that the individual field components should be responsible for maintaining their own state and this should not be a concern of the parent form which only needs to keep track of the updated values as its actions are triggered by the fields.

But how else can the parent form communicate down to its child components? Some sort of global event bus, perhaps using Ember.Evented?

Any ideas about the ā€˜correctā€™ way to design this would be much appreciated Thanks

4 Likes

What Iā€™ve been using (and this is by no means standard) is a pattern like this:

// In parent component template
{{child-component register-as=someParentRelevantName}}

var ChildComponent = Ember.Component.extend({
  didInsertElement: function() {
    this.set("register-as', this);
  }
})

var ParentComponent = Ember.Component.extend({
   actions: {
    reset: function() {
      // This should probably at least be done with actions
      this.get('someParentRelevantName').reset()
    }
   }
 })

The reason I like this approach is that it localizes the ā€œbreakingā€ of component isolation. Thereā€™s a bit of annoying fiddly-ness in setting up these references, but I think that does a good job of keeping me honest and pressuring me to find another solution if I start to overuse it.

3 Likes

Maybe of interest

Yeah, I guess thatā€™s where I got the technique from.

Depending on your use case observing init is probably better than didInsertElement. The former makes it pretty difficult to start tying into another componentā€™s dom elements, which seems like a good barrier to keep in place.

Thanks for the ideas and pointers. Iā€™ll need to extend this to support a collection of child components which will each need to register to receive event notifications but this gets me going for now. Iā€™m thinking perhaps a ā€œregisterā€ action instead of a ā€œregister-asā€ property - the child components can send this action during initialization and the parent can store these references to the children in an array.

You can also use ember-component-inbound-actions.

When you really want something like a ā€œglobal event busā€, thatā€™s a Service. You can Ember.inject.service() on any components that need to access it, and then just access it directly. They will all share it.

When you have a parent component with some tightly-coupled children, you can yield values to the children that give them a way to interact with the parent. For example:

{{#my-super-form as |parent|}}
    {{some-child notify=parent}}
{{/my-parent-component}}

Then the child can decide to this.get('notify').send('somethingHappened'). If you want to be able to send actions from parent-to-child, you can have the children register themselves at didInsertElement.

Itā€™s also possible to yield functions and write child components that just call those functions:

{{#my-form as |submit cancel|}}
  {{#my-button action=submit}}Go{{/my-button}}
  {{#my-button action=cancel}}Nevermind{{/my-button}}
{{/my-form}}

Today this requires some small hacks. In 2.0, it will work this way out of the box ā€“ the parentā€™s template can just say {{yield (action "submit") (action "cancel")}}, and the children will receive functions they can call directly.

In general I would avoid trying to send actions downward. Remember, itā€™s actions up, data down. Design the flow so youā€™re just pushing new data down to the children and they will react appropriately. In your form-reset case, instead of storing the original values in the children and telling them to reset themselves, store the original values in the parent and share both the current values and the original values with the children, so they always know whether to show the has-been-edited style or not.

8 Likes

@ef4 what are the small hacks, and is there an addon so we can start using this functionality now?

I put together a gist:

https://gist.github.com/ef4/016664960593c9884b04

4 Likes

So that makes sense for a component that owns all of its content, but what about a component that is designed to be a container for other content? For example, I made this split view component which takes the following form:

{{#happy-split-container}}
  {{#happy-split-view}}
    <h2>some user supplied content</h2>
  {{/happy-split-view}}
  {{~ happy-splitter-bar ~}}
  {{#happy-split-view}}
    <h2>some other user supplied content</h2>
  {{/happy-split-view}}
{{/happy-split-container}}

That happy-splitter-bar in the middle needs to send an action to the parent happy-split-container; theyā€™re definitely tightly coupled components. However, the idea of sending an action is an implementation detail of the component. I donā€™t want to expose this to consumers of my component and force them to change their templates (and possibly get it wrong, thus breaking their usage of the component).

Iā€™m still trying to find a good solution for child to parent component communication in this scenario. I guess I could use the Service idea, but that seems like overkill to me. What I eventually settled on was this.get('parentView').send('dragSplitter'); (code in context), but that just feels clunky and potentially error prone (how does the child component know that its parent is of the expected type? validate on didInsertElement? ugh).

Iā€™m open to suggestions on how to make this child/parent action communication better, or even why my existing implementation might be not as bad as I think. Any help is appreciated.

1 Like

I am beginner in Ember, but I came up with a solution to let components send actions that bubble up through their wrapper components. I use this to communicate between components and catch actions on component level.

Template - aim is to let sq-form catch actions from sq-form-button and do necessary validations first.

{{#sq-form save='save'}}
  {{sq-form-button}}
{{/sq-form}}

bubbling.js - I created a Mixin that should be used on both components. This recursively walks up in the component hierarchy from down to up and fires the event if it is declared.

dispatchAction: function(name, ...contexts) {
   this.dispatch(name, true, ...contexts);
},
dispatch: function(name, bubble, ...contexts) {
    var parent = this.get('parentView');
    if ( bubble === true || typeof parent['dispatch'] === "function" ) {
	 parent.dispatch(name, false, ...contexts);
    } else if ( this._actions.hasOwnProperty(name) ) {
	 this.send(name, ...contexts);
    }
}

sq-form-button.js

actions: {
   click: function() {	
      this.dispatchAction('save');
   }
},

sq-form.js .

actions: {
   save: function() {
      // DO THE NECESSARY VALIDATIONS FIRST, THEN SEND "SAVE" if OK
      this.sendAction('save');
   }
},

What do you think? I mean it works for me, but I am not sure how ember friendly it is. My challenge now is to somehow enable sq-form to enable and disable the sq-form-button and I was thinking to build the a similar method with reversed bubbling, so actions bubble down, but I have feeling it is not ember friendly, I keep reading ā€˜Data down, actions upā€™.

If anyone is interested, here us a nice solution using a ā€œglobal event busā€

1 Like

The data down-action up is right for most of the cases. But how would you solve a sync issue between two components? Component A shows a list of items. When an item is clicked in component A it shows the item details in component B. However user is still free to scroll the list of items in component A. You want to offer in component B a way to scroll back the component A items to the select item. This a sync issue between two components. Itā€™s almost impossible to solve this only by data downā€¦ Something has to tell component A to scroll to a certain item. If you add animation with component A resizing and item positions changing you have to wait for the end of animation for calculating the correct scroll position, etc. I think component should have a way to register action down functions for this purposes.

You would have component A and component B send their actions to a shared parent or service that mutates the data such that both components will sync you could also use the global event bus or dispatch an event on the shared model object which would essentially be a local event bus.

@varblob I think that moving visualization events/states such as an end of animation event into the model object is conceptually not correct. For some other scenarios, I think your idea could be applicable. I prefer the first solution you described with a shell component that orchestrates the two child components. But there is so much you can do with just dataā€¦ How do you instruct a component to reload or refresh? You have to implement a semaphore mechanismā€¦ I really think that having component exposing methods and the possibility for a shell component to invoke these actions on a child component is inevitable. My second issue is with add-ons, there is no way we can have them know about a event bus unless itā€™s a concept built in Ember.

The local event dispatcher can sit in a separate event object you pass to the child if youā€™re worried about keeping separation of events from data. The choice to attach it to the model object is mostly out of laziness since itā€™s annoying have to pass in many parameters to child components especially if theyā€™re deeply nested. There are also other ways to achieve this by passing a register function down to the children for them to call on themselves or some part of themselves.

As to view states perhaps there is a slight misunderstanding. I define view model data as state information that will not be persisted. Normally this lives on the component directly and this is fine until you run into the situation where there is shared view state between multiple components. In these instances I choose to break out the view state into a separate ā€œmodelā€ object and share it between those components. Generally that view state doesnā€™t live on the same model object as the persisted state. I do however tend to wrap my persisted state in the view model object out of my aversion to passing many things into components but conceptually theyā€™re distinct.

Iā€™m a little fuzzy as to your addon statement what do you mean by there is no way to have the addon know? Do you mean that the addon has no way to subscribe to a service that is created by the application that uses it? If possible could you give a use case so I can understand more clearly.

I know this is not the ideal way to do it but this could be achieved by something like this

//Parent 
Ember.Component.extend({
  triggerChildActionProp : false,
  actions : {
    triggerActionOnChild () { this.toggleProperty('triggerChildActionProp'); }
  }
})

//Child
Ember.Component.extend({
  parentActionProp : false,
  parentActionTrigger : Ember.observer('parentActionProp', function () {
    //trigger child action here
  })
})

//template
{{child parentActionProp=triggerChildActionProp}}

This is makeshift way achieving parent to child action triggering. Worked for my use case. But yeah the ideal way would be to send only data down the component and change the underlying data passed to the child component instead of doing this.

Hey there, Iā€™ve put up a gist to demonstrate @ef4 suggestion:

store the original values in the parent and share both the current values and the original values with the children, so they always know whether to show the has-been-edited style or not.

Hope that will be helpful!

Just send values to the child to bind to. Data down, actions up.

Hi Ben_Glancy,

Can you give an example.

Thanks,

Darren