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.
- A function that takes an Owner and returns an instance of a manager.
- 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
- createModifier()
- installModifier()
- updateModifier()
- destroyModifier()
Before diving deeper into the modifier lifecycle, we need to know that in every modifier, there should be
- Installation logic
- 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);
}
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.
- positional (will be an array)
- 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"}}
The args object will be constructed as
args: {
positional: [true],
named: {
text: "This is a tooltip"
}
}
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 ...
}
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...
}
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...
}
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 (eg3.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 {}
);
Let's create our custom tooltip modifier manager.
In this tooltip modifier, we need to do two processes
- Add tooltip when the cursor moves into the element (mouseover event).
- 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);
}
}
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);
}
}
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);
}
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);
}
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 returnnull
for theelement
property.
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'sdata-tooltip
attribute.
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.
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.
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
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
<span {{tooltip text=this.tooltipText}}>Tooltip</span>
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)
Well explained 👍
Great work!
The twiddle line is not correct, mind to fix it?
My bad🤦. Updated the twiddle link. Thanks for pointing out!