Prerequisites:
Before moving on to our topic, let's recap some basic stuff about ember modifiers from my previous blogs.
-
Explanation about ember modifiers and how they are used to achieve reusable DOM behavior are in the following post.
-
Each ember modifier should be managed by an ember modifier manager. To explore more, check out my other blog.
If you're familiar with these topics, feel free to skip this part.
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
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:
- createModifier
- installModifier
- updateModifier
- 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:
- MODIFIER_ELEMENTS - to map the element with the modifier.
- MODIFIER_TEARDOWNS - to map the teardown logic with the modifier
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.
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.
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.
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.
- createModifier - simply returns the modifier instance.
- 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 inMODIFIER_TEARDOWNS
WeakMap. - 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. - 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
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
.
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.
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 thecreateModifier
method of theFunctionalModifierManager
which will provide the instance of the modifier function. Also, this method returns an instance of theCustomModifierState
which has information about the element, delegate, instance, and args. -
install()
- triggers theinstallModifier
method of theFunctionalModifierManager
. -
update()
- triggers theupdateModifier
method of theFunctionalModifierManager
. -
getDestructor()
- returns the state in which we can access thedestroy
method to trigger thedestroyModifier
method of theFunctionalModifierManager
.
Combining these two classes, our custom-modifier-manager.js
file would look like
File: custom-modifier-manager.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 fileindex.js
1. Import necessary files
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.
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.
var componentInstance = new EmberComponent();
var inputElement = document.createElement("input");
componentInstance.render(inputElement);
4. Instantiate CustomModifierManager
var FUNC_CUSTOM_MODIFIER = new CustomModifierManager();
5. Get the state using FUNC_CUSTOM_MODIFIER.create()
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
andmodifierClass
together. - args -
{}
This will return an instance of the customModifierState
.
6. Install the modifier on the element using FUNC_CUSTOM_MODIFIER.install()
FUNC_CUSTOM_MODIFIER.install(funcModifierState);
Finally, our index.js
file will look like
File: index.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
FUNC_CUSTOM_MODIFIER.update(funcModifierState);
destroy
We can access the destroy()
method through the getDestructor()
method which returns the CustomModifierState containing the destroy() method.
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
<!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
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! 🤩
Hope you enjoyed getting to know the behind the scenes of ember modifiers. See you in my next blog! 👋
Top comments (0)