By TEN BITCOMB

Element Modifiers In Ember.js And How To Write Your Own

You may have been enjoying the use of modifiers like {{action}} and {{on "eventname"}}, but have you considered writing your own modifiers?

What is an element modifier?

A modifier in Ember is really somewhere between a component and a helper function. It can enact upon an element in which it was instantiated with, it has a state, and it has lifecycle hooks.

Sounds a lot like a component, right? You might be wondering why you should ever bother making modifiers if components fit the bill.

The thing that makes modifiers different is that they can be used on arbitrary DOM elements. A component can't really be instantiated on any given element, although it can be given a different template conditionally. A modifier doesn't have a template, but it can be given an element with which it can modify.

The Built-in Modifiers

Ye Olde Action Modifier

We've all used {{action}}, the oldest modifier and the only one we had for years(to my knowledge). All this modifier does is listen for a click-event and subsequently call an action or method in the current context.

This modifier may one day disappear all-together, and you will see why in the next section. It's no longer encouraged to call actions using strings, like {{action "myAction"}}, and only being able to listen to click events is pretty limiting. Yes, we can listen to other events in our component code, but it should be just as easy to hook up our elements to our component actions through different events without having to use lifecycle hooks or the DOM API directly.

{{on}}

As of v3.12, Ember now comes with a new and powerful modifier for event listening.

Gone are the days of just listening to click events on individual elements using the old {{action}} modifier. Now we can listen to any DOM event from any element within a template.

Take for consideration this component:

// my-component.js
import Component from '@ember/component';
import { action } from '@ember/object';
export default class MyComponent extends Component {
  @action
  hoverAction() {
    alert('It\'s not only for clicks!');
  }
}
{{!-- my-component.hbs --}}
<button {{on "mouseover" this.hoverAction}}>Hover Over Me</button>

As you can tell, this allows us to listen to an event of our choosing and call an action. No setup inside of a lifecycle hook is required.

Render Modifiers

With the package @ember/render-modifiers, we can now use lifecycle hooks on our elements both within and outside of components. They are especially useful for components that are tagless.

Let's say you've got a component that has a didInsertElement hook that puts the focus on a child element. Now, you can do the same thing using {{did-insert}} within the template, and use other modifiers to hook into the lifecycle like {{will-destroy}}.

<ul class="my-menu"
    role="menu"
    tabindex="-1"
    {{did-insert this.focusMenu}}
    {{will-destroy this.focusOpenButton}}>
  {{#each items as |item|}}
    <li tabindex="-1" {{on "click" this.selectItem item}}>{{item.name}}</li>
  {{/each}}
</ul>

As there is an RFC for render modifiers, eventually they will ship with Ember and we won't need to install a package anymore.

Writing Custom Modifiers

The idea of writing a modifier for Ember probably seems foreign, especially since there's no guide that I'm aware of for doing so. Even the Octane guide has literally anything to say about modifiers, despite the fact that they are already a part of Ember. Though I presume it will at some point, given the placeholder page.

Before you can try writing your own modifier, you must be using a version of Ember greater or equal to v3.8, which gives us public access to the setModifierManager API that we can use to hook-up our own modifiers.

Here's an example that shows the anatomy of a modifier manager using only this API:

// app/modifier-managers/my-custom-modifier.js
import { setModifierManager } from '@ember/modifier';

export default setModifierManager(
  () => ({
    /**
     * Returns a new instance of the modifier, or even just a plain
     * JavaScript object representing a state.
     * 
     * @param {Function|undefined} factory - A factory for the modifier.  This is the result of `factoryFor('modifier:<modifier name>')`, which will return a factory if it resolves to a module located in `app/modifiers`.  To create an instance of that modifier, you can call `factory.create(args)`.  If no modifier class is resolved, this will be undefined.
     * @param {Object} args - Arguments passed to the modifier in the template
     * @param {Object} [args.named] - Named arguments
     * @returns {Object|undefined} - The return value is passed to other hooks as the state.  This can be an instance created by the factory or even just a plain JavaScript object.
     */
    createModifier(factory, args) {
         
    },

    /**
     * Performs when the element has been inserted.
     * Works like didInsertElement().
     * If you need to interact with the element in other hooks, you
     * should assign it to the `instance` object.
     * 
     * @param {*} instance - The instance or state returned from createModifier().
     * @param {HTMLElement} element - The element the modifier applies to.
     * @param {Object} args - Arguments passed to the modifier in the template
     * @param {Array} [args.positional] - Positional arguments
     * @param {Object} [args.named] - Named arguments
     */
    installModifier(state, element, args) {

    },

    /**
     * Is called when arguments to the modifier change.
     * Think of this like didUpdate().
     * 
     * @param {*} instance - The instance or state returned from createModifier().
     * @param {Object} args - Arguments passed to the modifier in the template
     * @param {Array} [args.positional] - Positional arguments
     * @param {Object} [args.named] - Named arguments
     */
    updateModifier(instance, args) {

    },

    /**
     * This is the willDestroy() of the modifier manager.
     * Perform cleanup here for when the modifier is no longer needed.
     * 
     * @param {*} instance - The instance or state returned from createModifier().
     * @param {Object} args - Arguments passed to the modifier in the template
     * @param {Array} [args.positional] - Positional arguments
     * @param {Object} [args.named] - Named arguments
     */
    destroyModifier(instance, args) {

    }
  }),
  /**
   * The base class that applications would extend from.
   * I don't completely understand what this is for.
   */
  class MyCustomModifierManager {}
)

The setModifierManager function is considered a low-level API, intended only for internal use and by add-ons. That's something that never stops me. At this time, it's the only public API for setting up modifiers in Ember, so it's up to you whether you think it's time to write your own modifiers this way.

As of this writing, there is an RFC open that describes a higher-level API for writing element modifiers, which looks very similar to how you would write a component, but I have no idea if we'll be seeing this feature in Ember any time soon. Until then, using a modifier-manager alone will work just fine, and this seems to be what's done in other modifier add-ons.

Here's an example of a dirt simple modifier for adding a tooltip, implemented only as a modifier-manager in the app/modifiers folder:

// app/modifiers/tooltip.js
import { setModifierManager } from '@ember/modifier';

export default setModifierManager(
  () => ({
    createModifier() {
      // Here, we'll just use a plain old object to hold
      // the state of the element.
      return {
        isVisible: false,
        element: null,
        timeout: null
      };
    },

    installModifier(state, element, { named: { text }}) {
      state.element = element;
      element.setAttribute('data-tooltip', text);
      element.addEventListener('mouseover', () => {
        // show tooltip after 1 second
        state.timeout = setTimeout(() => {
          if (!state.isVisible) {
            state.isVisible = true;
            element.classList.add('tooltip-visible');
          }
        }, 1000);
      });
      element.addEventListener('mouseleave', () => {
        state.isVisible = false;
        element.classList.remove('tooltip-visible');
        clearTimeout(state.timeout);
        state.timeout = null;
      });
    },

    updateModifier({ element }, { named: { text }}) {
      element.setAttribute('data-tooltip', text);
    },

    destroyModifier() {
      // We don't need to do anything here, but a function
      // still has to be here so we'll leave it blank.
    }
  }),
  class TooltipModifierManager {}
)
Then you can something like this:
<span {{tooltip text="Modifiers are cool"}}>hover here</span>

That's all you need to make your own modifiers!

Functional and Class-based Modifiers

There's a package called ember-modifier that provides what some might find to be nicer high-level APIs for creating modifiers.

Here's an example of a functional modifier:

// app/modifiers/move-randomly.js
import { modifier } from 'ember-modifier';

const { random, round } = Math;

export default makeFunctionalModifier(element => {
  const id = setInterval(() => {
    const top = round(random() * 500);
    const left = round(random() * 500);
    element.style.transform = `translate(${left}px, ${top}px)`;
  }, 1000);
  // The return value of the function is called on destroyModifier.
  return () => clearInterval(id);
});
Here's an example of a class-based modifier:
import Modifier from 'ember-modifier';

export default class OnModifier extends Modifier {
  event = null;
  handler = null;

  // methods for reuse
  addEventListener() {
    let [event, handler] = this.args.positional;

    // Store the current event and handler for when we need to remove them
    this.event = event;
    this.handler = handler;

    this.element.addEventListener(event, handler);
  }

  removeEventListener() {
    let [event, handler] = this;

    if (event && handler) {
      this.element.removeEventListener(event, handler);

      this.event = null;
      this.handler = null;
    }
  }

  // lifecycle hooks
  didReceiveArguments() {
    this.removeEventListener();
    this.addEventListener();
  }

  willRemove() {
    this.removeEventListener();
  }
}

Modifiers Moving Forward

I see exposing the APIs for modifiers as a big step forward for Ember because they help relieve components of low-level responsibilities and, like native decorators, can fill the space once taken by mixins.