Research
Security News
Threat Actor Exposes Playbook for Exploiting npm to Build Blockchain-Powered Botnets
A threat actor's playbook for exploiting the npm ecosystem was exposed on the dark web, detailing how to build a blockchain-powered botnet.
ember-modifier
Advanced tools
The ember-modifier package provides a way to create reusable, composable, and encapsulated behavior for DOM elements in Ember.js applications. It allows developers to define custom modifiers that can be applied to elements in templates, enabling a clean separation of concerns and enhancing code maintainability.
Creating a Simple Modifier
This feature demonstrates how to create a simple modifier that focuses an input element when it is inserted into the DOM. The `focusInput` modifier is defined and then applied to an input element in a template.
```javascript
// app/modifiers/focus-input.js
import { modifier } from 'ember-modifier';
export default modifier(function focusInput(element) {
element.focus();
});
// Usage in a template
<input {{focus-input}} />
```
Modifier with Arguments
This feature shows how to create a modifier that accepts arguments. The `setAttribute` modifier takes an attribute name and value as arguments and sets the attribute on the element.
```javascript
// app/modifiers/set-attribute.js
import { modifier } from 'ember-modifier';
export default modifier(function setAttribute(element, [attribute, value]) {
element.setAttribute(attribute, value);
});
// Usage in a template
<div {{set-attribute 'data-test' 'example'}}></div>
```
Modifier with Cleanup
This feature illustrates how to create a modifier that adds an event listener to an element and cleans up the listener when the element is removed from the DOM. The `addEventListener` modifier adds a specified event listener and returns a cleanup function to remove the listener.
```javascript
// app/modifiers/add-event-listener.js
import { modifier } from 'ember-modifier';
export default modifier(function addEventListener(element, [event, handler]) {
element.addEventListener(event, handler);
return () => {
element.removeEventListener(event, handler);
};
});
// Usage in a template
<button {{add-event-listener 'click' this.handleClick}}>Click me</button>
```
The ember-on-modifier package provides a simple way to add event listeners to elements in Ember.js templates using the `{{on}}` modifier. It is similar to the `addEventListener` feature of ember-modifier but is more focused on handling events in a declarative manner. While ember-on-modifier is great for straightforward event handling, ember-modifier offers more flexibility for creating complex and reusable behaviors.
This addon is the next iteration of both ember-class-based-modifier and ember-functional-modifiers. Some breaking changes to the APIs have been made, for a list of difference, see the API differences section.
Huge thanks to @sukima and @spencer516 for their contributions! This project is based on their work, and wouldn't have been possible without them.
This addon provides an API for authoring element modifiers in Ember. It mirrors Ember's helper API, with a form for writing simple functional modifiers, and form for writing more complicated class modifiers.
ember install ember-modifier
This addon does not provide any modifiers out of the box; instead, this library allows you to write your own. There are two ways to write modifiers:
import Modifier, { modifier } from 'ember-modifier';
These are analogous to Ember's Helper APIs, helper
and Helper
.
modifier
is an API for writing simple modifiers. For instance, you could
implement Ember's built-in {{on}}
modifier like so with modifier
:
// /app/modifiers/on.js
import { modifier } from 'ember-modifier';
export default modifier((element, [eventName, handler]) => {
element.addEventListener(eventName, handler);
return () => {
element.removeEventListener(eventName, handler);
}
});
Functional modifiers consist of a function that receives:
element
modifier((element, positional, named) => { /* */ });
This function runs the first time when the element the modifier was applied to is inserted into the DOM, and it autotracks while running. Any values that it accesses will be tracked, including the arguments it receives, and if any of them changes, the function will run again.
The modifier can also optionally return a destructor. The destructor function will be run just before the next update, and when the element is being removed entirely. It should generally clean up the changes that the modifier made in the first place.
To create a modifier (and a corresponding integration test), run:
ember g modifier scroll-top
For example, if you wanted to implement your own scrollTop
modifier (similar
to this),
you may do something like this:
// app/modifiers/scroll-top.js
import { modifier } from 'ember-modifier';
export default modifier((element, [scrollPosition]) => {
element.scrollTop = scrollPosition;
})
<div class="scroll-container" {{scroll-top @scrollPosition}}>
{{yield}}
</div>
If the functionality you add in the modifier needs to be torn down when the element is removed, you can return a function for the teardown method.
For example, if you wanted to have your elements dance randomly on the page
using setInterval
, but you wanted to make sure that was canceled when the
element was removed, you could do:
// app/modifiers/move-randomly.js
import { modifier } from 'ember-modifier';
const { random, round } = Math;
export default makeFunctionalModifier(element => {
const id = setInterval(() => {
const top = round(random() * 500);
const left = round(random() * 500);
element.style.transform = `translate(${left}px, ${top}px)`;
}, 1000);
return () => clearInterval(id);
});
<button {{move-randomly}}>
{{yield}}
</button>
Sometimes you may need to do something more complicated than what can be handled by functional modifiers. For instance:
In these cases, you can use a class modifier instead. Here's how you would
implement the {{on}}
modifier with a class:
import Modifier from 'ember-modifier';
export default class OnModifier extends Modifier {
event = null;
handler = null;
// methods for reuse
addEventListener() {
let [event, handler] = this.args.positional;
// Store the current event and handler for when we need to remove them
this.event = event;
this.handler = handler;
this.element.addEventListener(event, handler);
}
removeEventListener() {
let [event, handler] = this;
if (event && handler) {
this.element.removeEventListener(event, handler);
this.event = null;
this.handler = null;
}
}
// lifecycle hooks
didReceiveArguments() {
this.removeEventListener();
this.addEventListener();
}
willRemove() {
this.removeEventListener();
}
}
This may seem more complicated than the functional version, but that complexity comes along with much more control.
As with functional modifiers, the lifecycle hooks of class modifiers are tracked. When they run, they any values they access will be added to the modifier, and the modifier will update if any of those values change.
To create a modifier (and a corresponding integration test), run:
ember g modifier scroll-top --class
For example, let's say you want to implement your own {{scroll-position}}
modifier (similar to this).
This modifier can be attached to any element and accepts a single positional
argument. When the element is inserted, and whenever the argument is updated, it
will set the element's scrollTop
property to the value of its argument.
// app/modifiers/scroll-position.js
import Modifier from 'ember-modifier';
export default class ScrollPositionModifier extends Modifier {
get scrollPosition() {
// get the first positional argument passed to the modifier
//
// {{scoll-position @someNumber relative=@someBoolean}}
// ~~~~~~~~~~~
//
return this.args.positional[0];
}
get isRelative() {
// get the named argument "relative" passed to the modifier
//
// {{scoll-position @someNumber relative=@someBoolean}}
// ~~~~~~~~~~~~
//
return this.args.named.relative
}
didReceiveArguments() {
if(this.isRelative) {
this.element.scrollTop += this.scrollPosition;
} else {
this.element.scrollTop = this.scrollPosition;
}
}
}
Usage:
{{!-- app/components/scroll-container.hbs --}}
<div
class="scroll-container"
style="width: 300px; heigh: 300px; overflow-y: scroll"
{{scroll-position this.scrollPosition relative=false}}
>
{{yield this.scrollToTop}}
</div>
// app/components/scroll-container.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class ScrollContainerComponent extends Component {
@tracked scrollPosition = 0;
@action scrollToTop() {
this.scrollPosition = 0;
}
}
{{!-- app/templates/application.hbs --}}
<ScrollContainer as |scroll|>
A lot of content...
<button {{on "click" scroll}}>Back To Top</button>
</ScrollContainer>
If the functionality you add in the modifier needs to be torn down when the
modifier is removed, you can use the willRemove
hook.
For example, if you want to have your elements dance randomly on the page using
setInterval
, but you wanted to make sure that was canceled when the modifier
was removed, you could do this:
// app/modifiers/move-randomly.js
import { action } from '@ember/object';
import Modifier from 'ember-modifier';
const { random, round } = Math;
const DEFAULT_DELAY = 1000;
export default class MoveRandomlyModifier extends Modifier {
setIntervalId = null;
get delay() {
// get the named argument "delay" passed to the modifier
//
// {{move-randomly delay=@someNumber}}
// ~~~~~~~~~~~
//
return this.args.named.delay || DEFAULT_DELAY;
}
@action moveElement() {
let top = round(random() * 500);
let left = round(random() * 500);
this.element.style.transform = `translate(${left}px, ${top}px)`;
}
didReceiveArguments() {
if (this.setIntervalId !== null) {
clearInterval(this.setIntervalId);
}
this.setIntervalId = setInterval(this.moveElement, this.delay);
}
willRemove() {
clearInterval(this.setIntervalId);
this.setIntervalId = null;
}
}
Usage:
<div {{move-randomly}}>
Catch me if you can!
</div>
You can also use services into your modifier, just like any other class in Ember.
For example, suppose you wanted to track click events with ember-metrics
:
// app/modifiers/track-click.js
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import Modifier from 'ember-modifier';
export default class TrackClickModifier extends Modifier {
@service metrics;
get eventName() {
// get the first positional argument passed to the modifier
//
// {{track-click "like-button-click" page="some page" title="some title"}}
// ~~~~~~~~~~~~~~~~~~~
//
return this.args.positional[0];
}
get options() {
// get the named arguments passed to the modifier
//
// {{track-click "like-button-click" page="some page" title="some title"}}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
return this.args.named;
}
@action onClick() {
this.metrics.trackEvent(this.eventName, this.options);
}
didInstall() {
this.element.addEventListener('click', this.onClick, true);
}
willRemove() {
this.element.removeEventListener('click', this.onClick, true);
}
}
Usage:
<button {{track-click "like-button-click" page="some page" title="some title"}}>
Click Me!
</button>
element
args
: { positional: Array, named: Object }
args.positional
is an array of positional arguments, and args.named
is an object containing the named arguments.isDestroying
true
if the modifier is in the process of being destroyed, or has already been destroyed.isDestroyed
true
if the modifier has already been destroyed.constructor(owner, args)
super(...arguments)
before performing other initialization. The element
is not yet available at this point (i.e. its value is null
during construction).didReceiveArguments()
didUpdateArguments()
didReceiveArguments
.didInstall()
didReceiveArguments
.willRemove()
willDestroy()
willRemove
. The element
is no longer available at this point (i.e. its value is null
during teardown).Install | Update | Remove | this.element | this.args | |
---|---|---|---|---|---|
constructor() | (1) | ❌ | ❌ | ❌ | after super() |
didUpdateArguments() | ❌ | (1) | ❌ | ✔️ | ✔️ |
didReceiveArguments() | (2) | (2) | ❌ | ✔️ | ✔️ |
didInstall() | (3) | ❌ | ❌ | ✔️ | ✔️ |
willRemove() | ❌ | ❌ | (1) | ✔️ | ✔️ |
willDestroy() | ❌ | ❌ | (2) | ❌ | ✔️ |
Using the class API, you can use .ts
instead of .js
and it'll just work, as long as you do runtime checks to narrow the types of your args when you access them.
// app/modifiers/scroll-position.ts
import Modifier from 'ember-modifier';
export default class ScrollPositionModifier extends Modifier {
// ...
}
But to avoid writing runtime checks, you can extend Modifier
with your own args, similar to the way you would define your args for a Glimmer Component.
// app/modifiers/scroll-position.ts
import Modifier from 'ember-modifier';
interface ScrollPositionModifierArgs {
positional: [number],
named: {
relative: boolean
}
}
export default class ScrollPositionModifier extends Modifier<ScrollPositionModifierArgs> {
get scrollPosition(): number {
// get the first positional argument passed to the modifier
//
// {{scoll-position @someNumber relative=@someBoolean}}
// ~~~~~~~~~~~
//
return this.args.positional[0];
}
get isRelative(): boolean {
// get the named argument "relative" passed to the modifier
//
// {{scoll-position @someNumber relative=@someBoolean}}
// ~~~~~~~~~~~~
//
return this.args.named.relative
}
didReceiveArguments() {
if(this.isRelative) {
this.element.scrollTop += this.scrollPosition;
} else {
this.element.scrollTop = this.scrollPosition;
}
}
}
See this pull request comment for a full discussion about using TypeScript with your Modifiers.
ember-modifier
makeFunctionalModifier
to modifier
, and to a named export instead of the defaultisRemoving
flag from modifier destructors. In cases where fine-grained control over the lifecycle is needed, class modifiers should be used instead.ember-modifier
ember-modifier
.Modifier.modifier()
function.this.args
.didInsertElement
to didInstall
and willDestroyElement
to willRemove
. This is to emphasize that when the modifier is installed or removed, the underlying element may not be freshly inserted or about to go away. Therefore, it is important to perform clean-up work in the willRemove
to reverse any modifications you made to the element.didReceiveArguments
fires before didInstall
, and didUpdateArguments
fires before didReceiveArguments
, mirroring the classic component life-cycle hooks ordering.willDestroy
, isDestroying
and isDestroyed
with the same semantics as Ember objects and Glimmer components.See the Contributing guide for details.
This project is licensed under the MIT License.
FAQs
A library for writing Ember modifiers
The npm package ember-modifier receives a total of 107,942 weekly downloads. As such, ember-modifier popularity was classified as popular.
We found that ember-modifier demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 4 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
A threat actor's playbook for exploiting the npm ecosystem was exposed on the dark web, detailing how to build a blockchain-powered botnet.
Security News
NVD’s backlog surpasses 20,000 CVEs as analysis slows and NIST announces new system updates to address ongoing delays.
Security News
Research
A malicious npm package disguised as a WhatsApp client is exploiting authentication flows with a remote kill switch to exfiltrate data and destroy files.