Sending Ember actions down to components

Data down, actions up is an often repeated mantra of the Ember community, and for good reason. Sometimes though, you need to call actions on components in a 'downward' direction. In this post I'll give you an approach for breaking this DDAU rule."

“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

Diagram sketch

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

Hand sketch of interface

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!

Take this real-world dashboard example:

Screenshot of the interface

The Challenges

There are two challenges:

  1. We need to be able to track/register a number of components
  2. 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:

service.js
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:

component.js
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:

controller.js
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.