Sending actions down to components

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

Dashboards

Sketch of dashboard problem

Imagine you have a dashboard with a number of graph modules. Each graph module is a component. Each graph module can export its canvas to a PNG file. What if we want to be able to select a number of these graph modules 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!

iFrames

Sketch of iframe problem

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!

A Demo

To get an idea of the “dashboard” 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

Demo animation

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

Sketch of 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.