How do Ember Modifiers get to be managed internally?
Tags: javascript, ember
Published: 2020-07-14 11:35:05

> To know about ember modifiers and how they are used to achieve reusable DOM behaviors in ember, feel free to check my [previous blog](https://dev.to/_raja_sk_/reusable-dom-behavior-in-react-vs-ember-4p04) 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**](https://github.com/emberjs/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.

```js
createModifier(factory, args) {
  return factory.create(args);
}
```
This method is responsible for returning an instantiated modifier which will be passed as an argument to the other three lifecycle methods.
<hr>

> **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
```hbs
{{tool-tip canShow text="This is a tooltip"}}
```
The args object will be constructed as
```js
args: {
  positional: [true],
  named: {
    text: "This is a tooltip"
  }
}
```
<hr>

### `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**.

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

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.
```js
updateModifier(instance, args) {
  // teardown logic...
  // installation logic...
}
```
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.
```js
destroyModifier(instance, args) {
  // teardown logic...
}
```

### 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.
```js
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 {}
);

```

## 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
```js
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);
  }
}
```
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
```js
removeTooltip(event) {
  let element = event.target;
  let tooltipContent = document.getElementById('tooltip-content');
  if(tooltipContent) {
    element.removeChild(tooltipContent);
  }
}
```
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`.

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

### Teardown Logic

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

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

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.
```js
createModifier() {
  return {
    element: null,
  };
}
```
* 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.
```js
installModifier(instance, element, args) {
  let { named: { text }} = args;
  element.setAttribute('data-tooltip', text);
  instance.element = element;
  this.installationLogic(element);
}
```
* `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.
```js
updateModifier(state, args) {
  let { element } = state;
  let { named: { text }} = args;
  element.setAttribute('data-tooltip', text);
  this.tearDownLogic(element);
  this.installationLogic(element);
}
```
* At last, we need to define the `destroyModifier` method in which the teardown logic must be called to remove the modifier from the element.
```js
destroyModifier(state) {
  let { element } = state;
  this.tearDownLogic(element);
}
```

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`

```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 {}
)
```
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
```hbs
<span {{tooltip text=this.tooltipText}}>Tooltip</span>
```
For reference, check out [this twiddle](https://ember-twiddle.com/66678cd1a3168b8d2b9c7fb60a1baee7) that I've created.

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

<hr>
### 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! 👋