5.8 KiB
5.8 KiB
title, impact, impactDescription, tags
| title | impact | impactDescription | tags |
|---|---|---|---|
| Use {{on}} Modifier Instead of Event Handler Properties | MEDIUM | Better performance and clearer event handling | performance, events, modifiers, best-practices |
Use {{on}} Modifier Instead of Event Handler Properties
Always use the {{on}} modifier for event handling instead of HTML event handler properties. The {{on}} modifier provides better memory management, automatic cleanup, and clearer intent.
Why {{on}} 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):
// 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 ({{on}} modifier):
// 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 {{on}} modifier supports standard event listener options:
// 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
// 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:
// 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
// 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:
// 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:
// This won't work - loses 'this' context
<button {{on "click" this.myMethod}}>Bad</button>
✅ Use @action decorator:
@action
myMethod() {
// 'this' is correctly bound
}
<button {{on "click" this.myMethod}}>Good</button>
❌ Don't use string event handlers:
{{! Security risk and doesn't work in strict mode }}
<button onclick="handleClick()">Bad</button>
Always use the {{on}} modifier for cleaner, safer, and more performant event handling in Ember applications.
References: