Send actions to components


#1

Up until Ember 1.12 I have been using the following for sending actions to components: (taken from: http://blog.planetargon.com/entries/2015/4/8/helpful-pattern-for-sending-actions-to-your-ember-components)

MyComponent = Ember.Component.extend({
  _register: function() {
    this.set('register-as', this);
  }).on('init')
});

Then, I can use it in templates:

{{my-component register-as=eventsListener}}

and somewhere in a controller for example…

this.get('eventsListener').send('myAction');

In Ember 1.13 I’m getting strange results. I couldn’t figured out exactly what’s happening, but eventsListener sometimes does get updated.

I also tried different “lifecycle hooks” like: ‘didInitAttrs’, etc… with same results. ‘didInsertElement’ does work, (with warning) but it feels very wrong.

What is a good way to achieve this? Is this a bad pattern?


#2

It is a bad pattern. However, avoiding it is difficult. Here are a few ideas:

  1. Upcoming solution (not yet released): contextual components. This should provide additional tools to allow the common concept of a master component with sub-components (think ul>li).
  2. Using an event bus. This example makes it global, but you could also imagine having the master component create a local bus and yield it to its children.
  3. Depending on your use case, if your event is based on some data change, simply passing additional attributes to the child so it can observe it is the right way to do it (and before someone suggests it, no, passing a dummy attribute and abusing notifyPropertyChange is not a good idea).

Otherwise, here is some tweaking on your solution. Drop the register-as and directly yield the parent component to its child, in the parent’s template:

`{{yield this}}`

Then, where you invoke the component, pass the parent around:

{{#parent-component as |parent|}}
    {{child-component parent=parent}}
{{/parent-component}}`

Then, in your child’s init, you’ll get a reference to the parent, which you can use to let it know about the child, for instance

init: function () {
    this._super();
    const parent = get(this, 'parent');
    Ember.assert('child-component needs a parent', parent);
    parent.registerMyChild(this);
}

I’d rather have the child invoke a method than set a property directly, at least that keeps some isolation.

Still not the most elegant solution around, but gets the job done quickly.


#3

Thanks a lot! I should have thought about parameterized blocks!


#4

btw, do you maybe also have insight as to why my original solution doesn’t work reliably after Ember 1.12?


#5

Specifics? No, that would need closer inspection. I suspect it has something to do with the fact that data bindings are asynchronous. So depending on the exact order in which things are called, it would be possible the binding is not in place soon enough.

Interestingly, 1.13 is the version that refactored the rendering engine to use Glimmer, so it’s likely it touched component instanciation in non-trivial ways.


#6

It fails because in Ember 1.13, _register became a function name that’s used internally, so when you use it in you code, all sorts of things break. (the problem I was seeing was that jquery event handlers weren’t working, because the component wasn’t getting registered with the list of views properly).

(in particular, https://github.com/emberjs/ember.js/commit/fef509a5f33c7a4cc708a95951073f5346e37a32 is the commit that did it)


#7

This way is a good way — I think there’s a way we can refactor further and clean it up a bit for more isolation:

So, the parent template would contain:

{{yield (action registerMyChild)}}

(Or if the registerMyChild was an actual action in the actions hash):

{{yield (action 'registerMyChild')}}

Then, in invoking the component:

{{#parent-component as |registerChild|}}
  {{child-component register=registerChild}}
{{/parent-component}}

And, in the child component:

didInitAttrs() {
  // could add an assert here to make sure register is available.
  this.attrs.register(this);
}

#8

Apologies for reviving an old post. I felt my $0.02 was still relevant even in the Ember 3.x days.

I have played with a few patterns for this situation. Although I agree with the common trope that this is a code smell there are times when it can not be avoided. I attempted to illustrate an example use case and after 4 paragraphs realized that the setup was far too long for a simple forum reply. Perhaps one day I’ll make a blog post about it, Till then just trust me that there are times when the need to send a child component an action is necessary and here are some ways I’ve solved that need (ordered worst to best):

Events via a Service (global name-spacing) :-1:

This pattern is simply the idea that both the parent and the child inject a common service. The service is an event bus (Ember.Evented). The child adds an event listener on the service and the parent triggers or calls methods on the service to trigger events that the child responds to.

This is the worst because it is essentially a global event bus and would require either crafty event name spacing or unique service objects per component! Plus you have to remember to clean up after events (memory leaks) and make the coupling almost impossible to reason about in a large app.

I do not recommend this pattern.

Events via isolated broker (Dispatcher pattern) :-1:

Loosing the global issues of services we could construct a special object that is an isolated event bus:

const MyPersonalEventBus = Ember.Object.extend(Ember.Evented);

Then we pass it down to the child component:

{{my-component eventDispatcher=myEventDispatcher}}

And then the child component adds event handlers and the parent triggers events.

This pattern removes the global name spacing problem but still has all the problems of events and the difficulty in reasoning about them. It isn’t very declarative and things happen by magic without a huge amount of cognitive overload to understand it all.

I do not recommend this pattern.

Callback handshaking (Closure actions with Interface handoff) :+1:

In this pattern the child holds the responsibility to provide the parent an interface to work with. The parent attaches an action to the child component which registers an interface with the parent. That interface has methods on which the parent can call.

An example of this might be a modal that needs some JS to call an .open() and .close() function in order to handle the opening and closing (assume again that this is an addon restriction and not that there is a more ember way to do this; remember sometimes we must pass actions down).

Child component

export default Component.extend({
  openModal() {
    // Do something here to open the modal
  },
  closeModal() {
    // Do something here to close the modal
  },
  didInsertElement() {
    this._super(...arguments);
    this.get('registerModalController')({
      open: () => this.openModal(),
      close: () => this.closeModal()
    });
  },
  willRemoveElement() {
    this._super(...arguments);
    this.get('registerModalController')(null);
  }
});

And in the parent template:

{{my-component registerModalController=(action (mut modalController))}}

The parent can freely call methods on the modalController object.

this.get('modalController').open();
this.get('modalController').close();

I have found this the best solution as it narrows the scope to the child component and the child component can control the interface that is exposed to the parent. Plus it make stack traces easy and you can see that the parent called a method on the child.

I recommend this pattern if and when you need it.