Building Bridges to the DOM with Modifiers

"Those who do not remember the past, are condemned to repeat it.

- George Santayana

What?

  • Similar to Handlebars helpers
  • Functions or classes
  • Can be used in templates directly using {{double-curlies}}

{{action}}

{{bind-attr}}

Don't lifecycle hooks solve the same problem?

Better solution for DOM manipulation

  • Allow targeting specific elements more easily
  • Allow related code to live in the same place.

  • Make sharing code between components much easier
  • Work with template only components
  • Work with tagless components and Glimmer components

#1

Allow Targeting specific elements more easily.

Lifecycle hooks only allow you to work with the component's root element.

didInsertElement() {
  this.element
    .querySelector('button')
    .addEventListener('click', this.handleClick);
}

#2

Allow related code to live in the same place.

class HelloWorld extends Component {
  addTooltipListener() {
    // save the element so we can remove the listener later
    this._tooltip = this.element.querySelector('.tooltip');

    if (this._tooltip) {
      this._tooltip.addEventListener(
        'mouseover',
        this.toggleTooltip
      );
    }
  }

  removeTooltipListener() {
    if (this._tooltip) {
      this._tooltip.removeEventListener(
        'mouseover',
        this.toggleTooltip
      );
    }
  }

  didInsertElement() {
    this.element
      .querySelector('button')
      .addEventListener('click', this.handleClick);

    this.addTooltipListener();
  }

  didUpdate() {
    this.removeTooltipListener();
    this.addTooltipListener();
  }

  willDestroyElement() {
    this.element
      .querySelector('button')
      .removeEventListener('click', this.handleClick);

    this.removeTooltipListener();
  }

  // ...
}

Modifiers have their own setup and teardown logic, completely

self-contained.

Run on the insertion & destruction of the element they are modifying, not the component,

#3

Make sharing code between components much easier

Modifications are applied where they happen

#4

Work with template-only components

You MUST create a component class to do even simple DOM manipulation

#5

Work with tag-less components & Glimmer components

tagName: ''

Tag-less Components

tagName: ''

Life-cycle Hooks

Tag-less Components

tagName: ''

Life-cycle Hooks

this.element

Tag-less Components

this.element

Glimmer Components

this.element

Glimmer Components

didInsertElement

didRender

Component Class Definition

DOM Manipulation

Work without component API

How other framework do it?

React => useLayoutEffect

Modifier syntax

<div {{action this.handleClick}}></div>

<div onclick={{action this.handleClick}}></div>
<!-- MODIFIER -->
<div {{action this.handleClick}}></div>

<!-- HELPER -->
<div onclick={{action this.handleClick}}></div>

Same syntax as helper

Helper => Attribute

Modifier => Element

Modifiers run :

  • whenever the element is inserted or destroyed

  • whenever any of arguments to them change.

User defined modifiers

Modifier Manager

Low level API

Class Based Modifiers

  • Fully featured
  • Instance & State
  • Ability to control each lifecycle event
export default class DarkMode extends Modifier {
  @service userSettings;

  didInsert(element, [darkModeClass]) {
    if (this.userSettings.darkModeEnabled) {
      this._previousDarkModeClass = darkModeClass;
      element.classList.add(darkModeClass);
    }
  }

  willDestroy(element) {
    element.classList.remove(this._previousDarkModeClass);
  }

  didUpdate() {
    this.willDestroy(...arguments);
    this.didInsert(...arguments);
  }
}
<!-- usage -->
<div {{dark-mode 'ui-dark'}}></div>

Functional Modifiers

  • Functional API
  • Same as `useLayoutEffect`
  • Single function returns a cleanup function
function darkMode(userSettings, element, [darkModeClass]) {
  if (userSettings.darkModeEnabled) {
    element.classList.add(darkModeClass);

    return () => {
      element.classList.remove(darkModeClass);
    };
  }
}

export default modifier(
  { services: ['userSettings'] },
  darkMode
);

With Decorators

@modifier
function darkMode(
  @service userSettings,
  element,
  [darkModeClass]
) {
  if (userSettings.darkModeEnabled) {
    element.classList.add(darkModeClass);

    return () => {
      element.classList.remove(darkModeClass);
    }
  }
}