/ember-render-modifiers

Implements did-insert / did-update / will-destroy modifiers for emberjs/rfcs#415

Primary LanguageJavaScriptMIT LicenseMIT

@ember/render-modifiers

Provides element modifiers that can be used to hook into specific portions of the rendering lifecycle.

Compatibility

  • Ember.js v2.18 or above
  • Ember CLI v2.13 or above

Installation

ember install @ember/render-modifiers

Usage Examples

Example: Scrolling an element to a position

This sets the scroll position of an element, and updates it whenever the scroll position changes.

Before:

{{yield}}
export default Component.extend({
  classNames: ['scroll-container'],

  didRender() {
    this.element.scrollTop = this.scrollPosition;
  }
});

After:

<div
  {{did-insert this.setScrollPosition @scrollPosition}}
  {{did-update this.setScrollPosition @scrollPosition}}

  class="scroll-container"
>
  {{yield}}
</div>
export default class Component.extend({
  setScrollPosition(element, [scrollPosition]) {
    element.scrollTop = scrollPosition;
  }
})

Example: Adding a class to an element after render for CSS animations

This adds a CSS class to an alert element in a conditional whenever it renders to fade it in, which is a bit of an extra hoop. For CSS transitions to work, we need to append the element without the class, then add the class after it has been appended.

Before:

{{#if this.shouldShow}}
  <div class="alert">
    {{yield}}
  </div>
{{/if}}
export default Component.extend({
  didRender() {
    let alert = this.element.querySelector('.alert');

    if (alert) {
      alert.classList.add('fade-in');
    }
  }
});

After:

{{#if this.shouldShow}}
  <div {{did-insert this.fadeIn}} class="alert">
    {{yield}}
  </div>
{{/if}}
export default Component.extend({
  fadeIn(element) {
    element.classList.add('fade-in');
  }
});

Example: Resizing text area

One key thing to know about {{did-update}} is it will not rerun whenever the contents or attributes on the element change. For instance, {{did-update}} will not rerun when @type changes here:

<div {{did-update this.setupType}} class="{{@type}}"></div>

If {{did-update}} should rerun whenever a value changes, the value should be passed as a parameter to the modifier. For instance, a textarea which wants to resize itself to fit text whenever the text is modified could be setup like this:

<textarea {{did-update this.resizeArea @text}}>
  {{@text}}
</textarea>
export default Component.extend({
  resizeArea(element) {
    element.style.height = `${element.scrollHeight}px`;
  }
});

Example: ember-composability-tools style rendering

This is the type of rendering done by libraries like ember-leaflet, which use components to control the rendering of the library, but without any templates themselves. The underlying library for this is here. This is a simplified example of how you could accomplish this with Glimmer components and element modifiers.

Node component:

// components/node.js
export default Component.extend({
  init() {
    super(...arguments);
    this.children = new Set();

    this.parent.registerChild(this);
  }

  willDestroy() {
    super(...arguments);

    this.parent.unregisterChild(this);
  }

  registerChild(child) {
    this.children.add(child);
  }

  unregisterChild(child) {
    this.children.delete(child);
  }

  didInsertNode(element) {
    // library setup code goes here

    this.children.forEach(c => c.didInsertNode(element));
  }

  willDestroyNode(element) {
    // library teardown code goes here

    this.children.forEach(c => c.willDestroyNode(element));
  }
}
<!-- components/node.hbs -->
{{yield (component "node" parent=this)}}

Root component:

// components/root.js
import NodeComponent from './node.js';

export default NodeComponent.extend();
<!-- components/root.hbs -->
<div
  {{did-insert (action this.didInsertNode)}}
  {{will-destroy (action this.willDestroyNode)}}
>
  {{yield (component "node" parent=this)}}
</div>

Usage:

<Root as |node|>
  <node as |node|>
    <node />
  </node>
</Root>

License

This project is licensed under the MIT License.