Use Modifier Instead of Event Handler Properties
Always use the modifier for event handling instead of HTML event handler properties. The modifier provides better memory management, automatic cleanup, and clearer intent.
Why is Better:
- Automatic cleanup when element is removed (prevents memory leaks)
- Supports event options (
capture,passive,once) - More explicit and searchable in templates
Incorrect (HTML event properties):
glimmer-js
// app/components/button.gjs
import Component from '@glimmer/component';
import { action } from '@ember/object';
export default class Button extends Component {
@action
handleClick() {
console.log('clicked');
}
<template>
<button onclick={{this.handleClick}}>
Click Me
</button>
</template>
}Correct ( modifier):
glimmer-js
// app/components/button.gjs
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { on } from '@ember/modifier';
export default class Button extends Component {
@action
handleClick() {
console.log('clicked');
}
<template>
<button {{on "click" this.handleClick}}>
Click Me
</button>
</template>
}Event Options
The modifier supports standard event listener options:
glimmer-js
// app/components/scrollable.gjs
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { on } from '@ember/modifier';
export default class Scrollable extends Component {
@action
handleScroll(event) {
console.log('scrolled', event.target.scrollTop);
}
<template>
{{! passive: true improves scroll performance }}
<div {{on "scroll" this.handleScroll passive=true}}>
{{yield}}
</div>
</template>
}Available options:
capture- Use capture phase instead of bubble phaseonce- Remove listener after first invocationpassive- Indicates handler won't callpreventDefault()(better scroll performance)
Handling Multiple Events
glimmer-js
// app/components/input-field.gjs
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { on } from '@ember/modifier';
export default class InputField extends Component {
@action
handleFocus() {
console.log('focused');
}
@action
handleBlur() {
console.log('blurred');
}
@action
handleInput(event) {
this.args.onChange?.(event.target.value);
}
<template>
<input
type="text"
value={{@value}}
{{on "focus" this.handleFocus}}
{{on "blur" this.handleBlur}}
{{on "input" this.handleInput}}
/>
</template>
}Preventing Default and Stopping Propagation
Handle these in your action, not in the template:
glimmer-js
// app/components/form.gjs
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { on } from '@ember/modifier';
export default class Form extends Component {
@action
handleSubmit(event) {
event.preventDefault(); // Prevent page reload
event.stopPropagation(); // Stop event bubbling if needed
this.args.onSubmit?.(/* form data */);
}
<template>
<form {{on "submit" this.handleSubmit}}>
<button type="submit">Submit</button>
</form>
</template>
}Keyboard Events
glimmer-js
// app/components/keyboard-nav.gjs
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { on } from '@ember/modifier';
export default class KeyboardNav extends Component {
@action
handleKeyDown(event) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
this.args.onActivate?.();
}
if (event.key === 'Escape') {
this.args.onCancel?.();
}
}
<template>
<div
role="button"
tabindex="0"
{{on "keydown" this.handleKeyDown}}
>
{{yield}}
</div>
</template>
}Performance Tip: Event Delegation
For lists with many items, use event delegation on the parent:
glimmer-js
// app/components/todo-list.gjs
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { on } from '@ember/modifier';
export default class TodoList extends Component {
@action
handleClick(event) {
// Find which todo was clicked
const todoId = event.target.closest('[data-todo-id]')?.dataset.todoId;
if (todoId) {
this.args.onTodoClick?.(todoId);
}
}
<template>
{{! Single listener for all todos - better than one per item }}
<ul {{on "click" this.handleClick}}>
{{#each @todos as |todo|}}
<li data-todo-id={{todo.id}}>
{{todo.title}}
</li>
{{/each}}
</ul>
</template>
}Common Pitfalls
❌ Don't bind directly without @action:
glimmer-js
// This won't work - loses 'this' context
<button {{on "click" this.myMethod}}>Bad</button>✅ Use @action decorator:
glimmer-js
@action
myMethod() {
// 'this' is correctly bound
}
<button {{on "click" this.myMethod}}>Good</button>❌ Don't use string event handlers:
glimmer-js
{{! Security risk and doesn't work in strict mode }}
<button onclick="handleClick()">Bad</button>Always use the modifier for cleaner, safer, and more performant event handling in Ember applications.
References: