DEV Community

Cover image for How do Ember Modifiers get to be managed internally?
Raja SK
Raja SK

Posted on • Edited on

How do Ember Modifiers get to be managed internally?

To know about ember modifiers and how they are used to achieve reusable DOM behaviors in ember, feel free to check my previous blog which compares the reusable DOM behavior in ember and react.

Now let's get into our topic: How do Ember modifiers get to be managed internally?

The answer is through Element modifier manager!

In ember, an element modifier manager is an object which is responsible for coordinating the life cycle events that occur when invoking, installing, and updating element modifiers.

Each element modifier needs a modifier manager that could be set by setModifierManager() API, which is a low-level API provided by the ember specifically for addon developers.

setModifierManager takes two parameters.

  1. A function that takes an Owner and returns an instance of a manager.
  2. The base class that applications would extend from.

When do we need this setModifierManagaer API?

By default, the modifier manager would already be assigned to a super-class provided by the framework or an addon.
But, while developing some addons like ember-render-modifiers, we need setModifierManager() API to create a custom modifier manager.

Modifier Lifecycle

  1. createModifier()
  2. installModifier()
  3. updateModifier()
  4. destroyModifier()

Before diving deeper into the modifier lifecycle, we need to know that in every modifier, there should be

  1. Installation logic
  2. Teardown logic

Installation logic is a piece of code that has to be executed when the element is installed to the DOM(the main modifier logic).

Teardown logic is a piece of code that has to be executed when the element is removed from the DOM.

createModifier

At first, to create an instance of a modifier, ember will invoke the modifier manager's createModifier method.

createModifier(factory, args) {
  return factory.create(args);
}
Enter fullscreen mode Exit fullscreen mode

This method is responsible for returning an instantiated modifier which will be passed as an argument to the other three lifecycle methods.


A small tip 💡 on how args is constructed inside ember.

args object will have two properties.

  1. positional (will be an array)
  2. named (will be an object)

Let's say, we have canShow = true and pass this to a tool-tip modifier

{{tool-tip canShow text="This is a tooltip"}}
Enter fullscreen mode Exit fullscreen mode

The args object will be constructed as

args: {
  positional: [true],
  named: {
    text: "This is a tooltip"
  }
}
Enter fullscreen mode Exit fullscreen mode

installModifier

After the modifier instance is created, the installModifier method is responsible for giving access to the underlying element and the arguments to the modifier instance.

  • Here, we need the installation logic.
installModifier(instance, element, args) {
  // installation logic ...
}
Enter fullscreen mode Exit fullscreen mode

The first argument instance is the result of createModifier. The second is the element on which the modifier is defined. The third is the snapshot of the args which we discussed earlier.

updateModifier

When any of the arguments passed to the modifier changes, ember invokes the updateModifier() method to allow the manager to reflect those changes on the modifier instance, before re-rendering.

  • First, we have to do the teardown logic for removing the instance with the old values.
  • Then, we have to do the installation logic for installing the new one.
updateModifier(instance, args) {
  // teardown logic...
  // installation logic...
}
Enter fullscreen mode Exit fullscreen mode

Here, we didn't get element as an argument because we have already installed this modifier on the element through installModifier method.

destroyModifier

When the element on which the modifier is defined is going to be destroyed (removed from the DOM), ember invokes this destroyModifier() method to perform the cleanup.

  • Teardown logic alone does the job here.
destroyModifier(instance, args) {
  // teardown logic...
}
Enter fullscreen mode Exit fullscreen mode

How do ember know which ember version does this modifier manager targets?

Through capabilities property. It should be the result of the capabilities() function provided by ember.

  • The first and the mandatory argument to the capabilities() function is the ${major}.${minor} format (eg 3.6), matching the minimum Ember version this manager is targeting.
  • It also accepts another argument, which is an object containing optional features.

This allows Ember to introduce new capabilities and make improvements to this setModifierManager API without breaking the existing code.

Now, we have managed to create a skeleton of setModifierManager API.

import { setModifierManager, capabilities } from '@ember/modifier';

export default setModifierManager(
  () => ({
    capabilities: capabilities('3.6'),

    createModifier(factory, args) {
      return factory.create(args.named);
    },

    installModifier(instance, element, args) {
      // installation logic...
    },

    updateModifier(instance,args) {
      // teardown logic...
      // installation logic...
    }

    destroyModifier(instance, args) {
      // teardown logic...
    }
  }), class BaseClass {}
);

Enter fullscreen mode Exit fullscreen mode

Let's create our custom tooltip modifier manager.

In this tooltip modifier, we need to do two processes

  1. Add tooltip when the cursor moves into the element (mouseover event).
  2. Remove tooltip when the cursor moves out of the element (mouseleave event).

Add Tooltip

addTooltip(event) {
  let element = event.target;
  let text = element.getAttribute('data-tooltip');
  let tooltipContent = document.getElementById('tooltip-content');
  if (!tooltipContent) {
    let tooltipElement = document.createElement('span');
    tooltipElement.setAttribute('id', 'tooltip-content');
    tooltipElement.innerHTML = text;
    element.appendChild(tooltipElement);
  }
}
Enter fullscreen mode Exit fullscreen mode

Here, we are getting the tooltip text from data-tooltip attribute of the element and we have created an element with id tooltip-content and appended it to the element.

Remove Tooltip

removeTooltip(event) {
  let element = event.target;
  let tooltipContent = document.getElementById('tooltip-content');
  if(tooltipContent) {
    element.removeChild(tooltipContent);
  }
}
Enter fullscreen mode Exit fullscreen mode

Here, we are removing the element with the id tooltip-content from the element.

Now, we need to incorporate the two logics.

Installation logic

Here, it is nothing but adding the event listeners - mouseover and mouseleave.

installationLogic(element) {
  element.addEventListener('mouseover', this.addTooltip);
  element.addEventListener('mouseleave', this.removeTooltip);
}
Enter fullscreen mode Exit fullscreen mode

Teardown Logic

In this case, we have to remove the added event listeners in the teardown logic.

tearDownLogic(element) {
  element.removeEventListener('mouseover', this.addTooltip);
  element.removeEventListener('mouseleave', this.removeTooltip);
}
Enter fullscreen mode Exit fullscreen mode

Now we have to define the lifecycle methods.

  • In the createModifier method, we have to return the state which would be passed as the instance argument for the other three lifecycle methods. Here, we just need to return null for the element property.
createModifier() {
  return {
    element: null,
  };
}
Enter fullscreen mode Exit fullscreen mode
  • Next, we have to perform the installation logic on the installModifier method. Also, we can store the text on the element's data-tooltip attribute.
installModifier(instance, element, args) {
  let { named: { text }} = args;
  element.setAttribute('data-tooltip', text);
  instance.element = element;
  this.installationLogic(element);
}
Enter fullscreen mode Exit fullscreen mode
  • updateModifier gets triggered only when the args change. So, here we need to do the teardown logic to destroy the old values and perform the installation logic to install a new one.
updateModifier(state, args) {
  let { element } = state;
  let { named: { text }} = args;
  element.setAttribute('data-tooltip', text);
  this.tearDownLogic(element);
  this.installationLogic(element);
}
Enter fullscreen mode Exit fullscreen mode
  • At last, we need to define the destroyModifier method in which the teardown logic must be called to remove the modifier from the element.
destroyModifier(state) {
  let { element } = state;
  this.tearDownLogic(element);
}
Enter fullscreen mode Exit fullscreen mode

We can set capabilities as capabilities('3.13').
Now, combining all these, we can get our tooltip custom modifier manager.

File: app/modifiers/tooltip.js

import { setModifierManager, capabilities } from '@ember/modifier';

export default setModifierManager(
  () => ({

    addTooltip(event) {
      let element = event.target;
      let text = element.getAttribute('data-tooltip');
      let tooltipContent = document.getElementById('tooltip-content');
      if (!tooltipContent) {
        let tooltipElement = document.createElement('span');
        tooltipElement.setAttribute('id', 'tooltip-content');
        tooltipElement.innerHTML = text;
        element.appendChild(tooltipElement);
      }
    },

    removeTooltip(event) {
      let element = event.target;
      let tooltipContent = document.getElementById('tooltip-content');
      if(tooltipContent) {
        element.removeChild(tooltipContent);
      }
    },

    installationLogic(element) {
      element.addEventListener('mouseover', this.addTooltip);
      element.addEventListener('mouseleave', this.removeTooltip);
    },

    tearDownLogic(element) {
      element.removeEventListener('mouseover', this.addTooltip);
      element.removeEventListener('mouseleave', this.removeTooltip);
    },

    /*--------- Modifier manager function starts here -----------*/

    capabilities: capabilities('3.13'),
    createModifier() {
      return {
        element: null,
      };
    },

    installModifier(state, element, args) {
      let { named: { text }} = args;
      element.setAttribute('data-tooltip', text);
      state.element = element;
      this.installationLogic(element);
    },

    updateModifier(state, args) {
      let { element } = state;
      let { named: { text }} = args;
      element.setAttribute('data-tooltip', text);
      this.tearDownLogic(element);
      this.installationLogic(element);
    },

    destroyModifier(state) {
      let { element } = state;
      this.tearDownLogic(element);
    }
  }),
  class TooltipModifierManager {}
)
Enter fullscreen mode Exit fullscreen mode

The class TooltipModifierManager is added because haven't defined a base class, we would run into an error. Rather we have defined it empty.

That's all. Our tooltip modifier is ready to be invoked as

<span {{tooltip text=this.tooltipText}}>Tooltip</span>
Enter fullscreen mode Exit fullscreen mode

For reference, check out this twiddle that I've created.

Also, feel free to drop off your queries (if any) in the discussion.


Up next ⚠️

Wanna know how exciting it could be to replicate ember modifiers in vanilla JS? Wait! We can work along in my next blog! 👋

Top comments (3)

Collapse
 
abhilashlr profile image
abhilashlr

Well explained 👍

Collapse
 
nightire profile image
Albert Yu

Great work!

The twiddle line is not correct, mind to fix it?

Collapse
 
_raja_sk_ profile image
Raja SK

My bad🤦. Updated the twiddle link. Thanks for pointing out!