Sending actions down to components

I know, I know “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. In other words DDAUBSADTM (Data down, actions up, but sometimes actions down too maybe?)

I’ve run into two of these situations recently:

  • I wanted to create a dashboard with a number of graph components on the screen. Each of these graph components can export its contents to a PNG file. I wanted to give the user the ability to select a number of these graph components and then perform a bulk operation to export them all as PNGs. This required being able to call the “export” action on a number of components from the top down.
  • I wanted to create a component that wraps an iframe. I wanted to be able to both receive messages from that iframe and pass them up to my application as actions (which is standard) and also send messages to the iframe. This required being able to call an action on the component containing the iframe itself (that would in turn send a message to the iframe using the postMessage API).

The Demo

To get an idea of the former situation, I’ve put together a demo application that offers similar mocked functionality. You can see the demo running on AWS, and you can look at the completed code on Github

The Problem

So the problem here is twofold:

  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:

import Service from '@ember/service';
import {computed} from '@ember/object';

export default Service.extend({
  modules: null,
  // ...
  register(component) {
    let modules = this.get('modules');
    if(! modules.includes(component)) {
      console.log(`Registering component ${component.get('elementId')}`);
      modules.addObject(component);
    } else {
      console.error(`Couldn't register component ${component.get('elementId')} to module ${this.get('elementId')}`);
    }
  },
  unregister(component) {
    let modules = this.get('modules');
    if(modules.includes(component)) {
      console.log(`Unregistering component ${component.get('elementId')}`);
      modules.removeObject(component);
    } else {
      console.error(`Couldn't unregister component ${component.get('elementId')} from module ${this.get('elementId')}`);
    }
  },
  init() {
    this._super(...arguments);
    this.set('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.get('dashboard').register(this);
  },
  willRemoveElement() {
    this._super(...arguments);

    // Unregister with service
    this.get('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.get('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.