The magic behind the ember modifiers
Tags: javascript, ember, webdev
Published: 2020-07-21 15:25:02

### Prerequisites:

> Before moving on to our topic, let's recap some basic stuff about ember modifiers from my previous blogs.

1. Explanation about ember modifiers and how they are used to achieve reusable DOM behavior are in the following post.
{% link https://dev.to/_raja_sk_/reusable-dom-behavior-in-react-vs-ember-4p04 %}

2. Each ember modifier should be managed by an ember modifier manager. To explore more, check out my other blog.
{% post https://dev.to/_raja_sk_/how-do-ember-modifiers-get-to-be-managed-internally-1i40 %}

If you're familiar with these topics, feel free to skip this part.
<hr>
Now, let's move on to our topic.

So far, IMO, ember modifiers are one of the extraordinary features of ember octane. While working with ember modifiers recently, I was questioning myself on:

## How does ember Modifiers do the magic on Reusable DOM behavior?

Come on! Let's solve the mystery together by replicating the ember modifiers through plain javascript.

Ember has both functional and class-based modifiers.

##Let's break down the functional modifiers.

### Create an `autofocus` modifier to just focus an element.

#### File: `modifiers/autofocus.js`
```js
export default function autofocus(element) {
  element.focus();
  return () => {
    console.log("destroy"); // Dummy log to simulate the teardown logic
  };
}
```
As we know, every ember modifiers will be managed by a modifier manager. Also, every modifier manager should have these four methods:
1. createModifier
2. installModifier
3. updateModifier
4. destroyModifier

### Create a functional modifier manager.

Now, we need to create a **functional modifier manager** class with the above-mentioned methods.

Before that, we need two WeakMaps here:
1. MODIFIER_ELEMENTS - to map the element with the modifier.
2. MODIFIER_TEARDOWNS - to map the teardown logic with the modifier

```js
const MODIFIER_ELEMENTS = new WeakMap();
const MODIFIER_TEARDOWNS = new WeakMap();
```

Teardown logic is nothing but the piece of code given in the `return` of modifier function. To set this in `MODIFIER_TEARDOWNS`, we need to have a `setup` method that runs the modifier and maps the returned value with the modifier.

```js
function setup(modifier, element, args) {
  const { positional, named } = args;
  const teardown = modifier(element, positional, named);
  MODIFIER_TEARDOWNS.set(modifier, teardown);
}
```

Some modifiers won't have any teardown logic. So we need a separate function `teardown` to check if the teardown present in `MODIFIER_TEARDOWNS` is a function and then access it.

```js
function teardown(modifier) {
  const teardown = MODIFIER_TEARDOWNS.get(modifier);
  if (teardown && typeof teardown === "function") {
    teardown();
  }
}
```
Now, Let's create the `FunctionalModifierManager` class with the above-mentioned lifecycle methods.

```js
class FunctionalModifierManager {
  createModifier(factory) {
    return (...args) => factory(...args);
  }
  installModifier(modifier, element, args) {
    MODIFIER_ELEMENTS.set(modifier, element);
    setup(modifier, element, args);
  }
  updateModifier(modifier, args) {
    teardown(modifier);
    const element = MODIFIER_ELEMENTS.get(modifier);
    setup(modifier, element, args);
  }
  destroyModifier(modifier) {
    teardown(modifier);
  }
}
```
Let's see what these four methods trying to do.
1. createModifier - simply returns the modifier instance.
2. installModifier - maps the element with the modifier in the `MODIFIER_ELEMENTS` WeakMap. Also calls the setup method to map the teardown logic with the modifier in `MODIFIER_TEARDOWNS` WeakMap.
3. updateModifier - runs the teardown logic once to remove the outdated modifier mapping and maps the element with the updated modifier in the `MODIFIER_ELEMENTS` WeakMap.
4. destroyModifier - runs the teardown logic to completely remove the mapping between the modifier and the element.

Combining the above snippets will form our `functional-modifier-manager.js` file.

#### File: `functional-modifier-manager.js`
```js
const MODIFIER_ELEMENTS = new WeakMap();
const MODIFIER_TEARDOWNS = new WeakMap();

function setup(modifier, element, args) {
  const { positional, named } = args;
  const teardown = modifier(element, positional, named);
  MODIFIER_TEARDOWNS.set(modifier, teardown);
}

function teardown(modifier) {
  const teardown = MODIFIER_TEARDOWNS.get(modifier);
  if (teardown && typeof teardown === "function") {
    teardown();
  }
}

export default class FunctionalModifierManager {
  createModifier(factory) {
    return (...args) => factory(...args);
  }
  installModifier(modifier, element, args) {
    MODIFIER_ELEMENTS.set(modifier, element);
    setup(modifier, element, args);
  }
  updateModifier(modifier, args) {
    teardown(modifier);
    const element = MODIFIER_ELEMENTS.get(modifier);
    setup(modifier, element, args);
  }
  destroyModifier(modifier) {
    teardown(modifier);
  }
}
```

## How do ember modifier and ember modifier manager communicate?
> **Custom Modifier Manager** acts as a middleman between the ember modifier and the ember modifier manager.

We need `CustomModifierState` to maintain the state for the `CustomModifierManager`.
```js
class CustomModifierState {
  constructor(element, delegate, modifier, args) {
    this.element = element;
    this.delegate = delegate;
    this.modifier = modifier;
    this.args = args;
  }

  destroy() {
    var { delegate, modifier, args } = this;
    delegate.destroyModifier(modifier, args);
  }
}
```
Let's decode the properties of this class.
* element - element on which the modifier is applied.
* delegate - an instance of the modifier manager (functional modifier manager).
* modifier - modifier definition (autofocus function).
* args - a snapshot of the arguments passed while invoking the modifier in `hbs`.
* destroy() - used to trigger `destroyModifier` method of the delegate(functional modifier manager).


Now, Let's create our `CustomModifierManager` class.
```js
class CustomModifierManager {
  create(element, definition, args) {
    var { delegate, ModifierClass } = definition;
    var instance = delegate.createModifier(ModifierClass, args);
    return new CustomModifierState(element, delegate, instance, args);
  }
  install(state) {
    var { delegate, modifier, element, args } = state;
    delegate.installModifier(modifier, element, args);
  }
  update(state) {
    var { delegate, modifier, args } = state;
    delegate.updateModifier(modifier, args);
  }
  getDestructor(state) {
    return state;
  }
```
Here,
* `create()` - triggers the `createModifier` method of the `FunctionalModifierManager` which will provide the instance of the modifier function. Also, this method returns an instance of the `CustomModifierState` which has information about the element, delegate, instance, and args.
* `install()` - triggers the `installModifier` method of the `FunctionalModifierManager`.
* `update()` - triggers the `updateModifier` method of the `FunctionalModifierManager`.
* `getDestructor()` - returns the state in which we can access the `destroy` method to trigger the `destroyModifier` method of the `FunctionalModifierManager`.

Combining these two classes, our `custom-modifier-manager.js` file would look like 

#### File: `custom-modifier-manager.js`
```js
class CustomModifierState {
  constructor(element, delegate, modifier, args) {
    this.element = element;
    this.delegate = delegate;
    this.modifier = modifier;
    this.args = args;
  }

  destroy() {
    var { delegate, modifier, args } = this;
    delegate.destroyModifier(modifier, args);
  }
}

export default class CustomModifierManager {
  create(element, definition, args) {
    var { delegate, ModifierClass } = definition;
    var instance = delegate.createModifier(ModifierClass, args);
    return new CustomModifierState(element, delegate, instance, args);
  }
  install(state) {
    var { delegate, modifier, element, args } = state;
    delegate.installModifier(modifier, element, args);
  }
  update(state) {
    var { delegate, modifier, args } = state;
    delegate.updateModifier(modifier, args);
  }
  getDestructor(state) {
    return state;
  }
```

# Implementing `index.js` file.

As of now, we have created the overall logic behind the ember functional modifiers. Let's move on to our main file`index.js`

### 1. Import necessary files
```js
import autofocus from "./modifiers/autofocus";
import FunctionalModifierManager from "./functional-modifier-manager";
import CustomModifierManager from "./custom-modifier-manager";
```

### 2. Ember component implementation
Let's assume this as an ember component with a render method which helps to render the elements in the DOM.
```js
class EmberComponent {
  render(...elements) {
    document.getElementById("app").innerHTML = "<div>Hello!</div>";
    document.getElementById("app").append(...elements);
  }
}
```

### 3. Instantiate the `EmberComponent` and call render with an input element.
```js
var componentInstance = new EmberComponent();
var inputElement = document.createElement("input");
componentInstance.render(inputElement);
```

### 4. Instantiate CustomModifierManager
```js
var FUNC_CUSTOM_MODIFIER = new CustomModifierManager();
```

### 5. Get the state using FUNC_CUSTOM_MODIFIER.create()
```js
var funcModifierState = FUNC_CUSTOM_MODIFIER.create(
  inputElement,
  {
    delegate: funcManager,
    ModifierClass: autofocus
  },
  {}
);
```
Here, create() method accepts three arguments.
* element - `inputElement` is the element on which modifier is applied.
* definition -  `delegate` and `modifierClass` together.
* args - `{}`

This will return an instance of the `customModifierState`.

### 6. Install the modifier on the element using FUNC_CUSTOM_MODIFIER.install()
```js
FUNC_CUSTOM_MODIFIER.install(funcModifierState);
```

Finally, our `index.js` file will look like

#### File: `index.js`
```js
import autofocus from "./modifiers/autofocus";
import FunctionalModifierManager from "./functional-modifier-manager";
import CustomModifierManager from "./custom-modifier-manager";

class EmberComponent {
  render(...elements) {
    document.getElementById("app").innerHTML = "<div>Hello!</div>";
    document.getElementById("app").append(...elements);
  }
}

var componentInstance = new EmberComponent();
var inputElement = document.createElement("input");
componentInstance.render(inputElement);

var FUNC_CUSTOM_MODIFIER = new CustomModifierManager();

var funcModifierState = FUNC_CUSTOM_MODIFIER.create(
  inputElement,
  {
    delegate: funcManager,
    ModifierClass: autofocus
  },
  {}
);

FUNC_CUSTOM_MODIFIER.install(funcModifierState);
```

Additionally, if we want to simulate the update and destroy,
### update
```js
FUNC_CUSTOM_MODIFIER.update(funcModifierState);
```
### destroy
We can access the `destroy()` method through the `getDestructor()` method which returns the CustomModifierState containing the destroy() method.
```js
FUNC_CUSTOM_MODIFIER.getDestructor(funcModifierState).destroy();
```

## We are only one step ahead to see the magic ✨ of ember modifiers.
Include the `index.js` script in `index.html` and see the magic.
#### File: `index.html`
```html
<!DOCTYPE html>
<html>
<head>
  <title>Parcel Sandbox</title>
  <meta charset="UTF-8" />
</head>
<body>
  <div id="app"></div>
  <script src="src/index.js">
  </script>
</body>
</html>
```

Hurray! The input element was focused automatically.

Apart from this, I've experimented with `on` modifier as a class-based-modifier through plain javascript and combined with the implementation we've done so far in [this codesandbox](https://codesandbox.io/s/ember-modifier-internals-2jq47)

Normally, we won't come across these implementations because Ember, as a framework, does its job in keeping those implementations away from us. But still, I find it interesting and useful to learn the internal working and redoing it with plain javascript. It was pretty exciting! 🤩

<hr>
> Hope you enjoyed getting to know the behind the scenes of ember modifiers. See you in my next blog! 👋