Element Modifier Manager (3.8)

Learn about the low-level API that enables Modifiers, one of Ember's most exciting new features.

Summary


The Element Modifier Manager feature is a new low-level API that landed in Ember 3.8. It enables the community to start creating custom Modifiers without committing to a final authorship API.

Modifiers are part of Ember's templating syntax, and they look like this:

<button {{effect 'fade-in'}}>Save</button>

Here, effect is a Modifier, and the string fade-in is a positional argument. Modifiers can also accept named arguments:

<button {{effect 'fade-in' duration=100}}>Save</button>

Modifiers run in what's called element space, which distinguishes them from helpers that are being assigned to attributes:

<button
  {{effect 'fade-in'}} {{! running in element space }}
  onclick={{action 'save'}} {{! attribute assignment }}
>
  Save
</button>

We've had Modifiers in Ember since the beginning: the action modifier is one we still use to this day:

<button {{action 'save'}}>Save</button>

With the Modifier Manager feature, we now have an official way to define our own modifiers.


The actual Modifier Manager API is looks like this:

// modifiers/hide.js
import { setModifierManager } from "@ember/modifier";

export default setModifierManager(
  owner => ({
    createModifier(factory, args) {},

    installModifier(instance, element, args) {},

    updateModifier(instance, args) {},

    destroyModifier(instance, args) {}
  }),

  class HideModifier {
  }
);

It is intentionally verbose, because it is lower level than strictly necessary to allow addon authors to experiment with different authorship APIs.

Here we're creating a hypothetical hide modifier, which could be used like this:

<p {{hide}}>Lorem ipsum</p>

The different hooks correspond to the lifecycle of the modifier:

  • create gives us an opportunity to create an instance of our Modifier class
  • install happens when the underlying element is first rendered to the DOM, and our modifier is installed
  • update occurs whenever any of our modifier's arguments are updated, and
  • destroy happens when the underlying element is unrendered from the DOM.

Here's a simple implementation of our hide modifier:

import { setModifierManager } from "@ember/modifier";

export default setModifierManager(
  owner => ({
    createModifier(factory, args) {
      return new factory.class();
    },

    installModifier(instance, element, args) {
      instance.element = element;

      instance.modify(args);
    },

    updateModifier(instance, args) {
      instance.modify(args);
    },

    destroyModifier(instance, args) {
      console.log("destroy");
    }
  }),

  class HideModifier {
    modify(args) {
      let delay = args.named.delay;
      this.element.style.opacity = 0;

      setTimeout(() => {
        this.element.style.opacity = 1;
      }, 1000 * delay);
    }
  }
);

It accepts a named delay parameter, and will re-run whenever the parameter changes. That means we can pass in dynamic data, like this:

<p {{hide delay=count}}>Lorem ipsum</p>

and as the count variable changes, our modifer stays up to date.

There are two popular addons that use this lower-level API to provide a nicer, higher-level API for the rest of us working in apps and addons. They are Ember Functional Modifiers and Ember Class-based Modifiers.

Here's an example implementation of our hide modifer using Ember Functional Modifiers:

import makeFunctionalModifier from "ember-functional-modifiers";

export default makeFunctionalModifier((element, args, named) => {
  let delay = named.delay;
  element.style.opacity = 0;

  setTimeout(() => {
    element.style.opacity = 1;
  }, 1000 * delay);
});

While it's good to understand why the Modifier Manager API was merged and what role it plays in the future of Ember's design, you should use one of these other two libraries rather than the low-level API as you start using Modifiers in your own apps today.

Questions?

Send us a tweet:

Or ask us in Inside EmberMap, our private Slack workspace for subscribers.