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)
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)
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)
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.