“Data down, actions up” is the mantra that Ember lives by (and for good reason) but there are sometimes occasions when you need to be able to call actions or functions on components in a “downward” direction.
iFrames

If you have an iFrame component, you might want to send and receive message via that iFrame using the postMessage
API. Receiving messages is easy as these get bubbled up to the parent components via actions. But how do we send actions down to our iFrame component so that they can be sent via the postMessage
API? Actions down!
Dashboards

Imagine you have a dashboard with a number of graphs. Each graph is a
component. Each graph component can export its canvas to a PNG file.
What if we want to be able to select a number of these graph components
at once and have them all export their canvas as a PNG? This is tricky.
We need to be able to monitor which components are selected and call the
“export” action on each of them. Actions down!

The Challenges
There are two challenges:
- We need to be able to track/register a number of components
- We need to be able to call actions on those components in bulk
The Solution
To get the functionality we require, we need to create a service that
acts as a registry for components that need to be tracked. This service
has a register
and unregister
function. Our
components will call these functions when they are rendered and
destroyed so that they are always accessible from elsewhere in the
application.
Once they are registered, we can then access them from our controller. In the demo application, I’ve added extra functionality to the service to allow these registered components to be selected and deselected. Our controller can then perform bulk operations on the selected components by calling their underlying actions.
Let’s look at the most important parts of our new dashboard service:
import Service from '@ember/service';
import {computed, set} from '@ember/object';
export default Service.extend({
modules: null,
// ...
register(component) {
if(! this.modules.includes(component)) {
console.log(`Registering component ${component.get('elementId')}`);
this.modules.addObject(component);
} else {
console.error(`Couldn't register component ${component.get('elementId')} to module ${this.get('elementId')}`);
}
},
unregister(component) {
if(this.modules.includes(component)) {
console.log(`Unregistering component ${component.get('elementId')}`);
this.modules.removeObject(component);
} else {
console.error(`Couldn't unregister component ${component.get('elementId')} from module ${this.get('elementId')}`);
}
},
init() {
this._super(...arguments);
set(this, 'modules', []);
}
});
and an example component that registers and unregisters itself:
import Component from '@ember/component';
import {inject as service} from '@ember/service';
import {computed} from '@ember/object';
export default Component.extend({
dashboard: service(),
// ...
didInsertElement() {
this._super(...arguments);
// Register with service
this.dashboard.register(this);
},
willRemoveElement() {
this._super(...arguments);
// Unregister with service
this.dashboard.unregister(this);
},
actions: {
export() {
// ...
}
}
});
and finally, how these bulk actions are called from our controller:
import Controller from '@ember/controller';
import {inject as service} from '@ember/service';
export default Controller.extend({
dashboard: service(),
actions: {
exportModules() {
this.dashboard.modules.forEach(component => {
component.send('export');
})
},
// ...
}
});
Of
course, these snippets are only part of the solution to our demo
application. To get a better sense of how it works, you should jump into
the repo and check it out in more detail.