Ember Actions — Event Handling and User Interactions
In this tutorial, you will learn about Ember Actions. We cover key concepts, practical examples, and best practices to help you master this topic.
Ember actions handle user interactions in templates. The {{on}} modifier binds DOM events to component methods. The @action decorator defines methods that maintain the correct this context. Actions can be passed between components through args.
What You'll Learn
You will learn how to handle DOM events with {{on}}, define actions with @action, pass actions between components, handle form submissions, and prevent event bubbling.
Why It Matters
Proper event handling makes applications interactive. Ember's action system ensures consistent this context, prevents memory leaks through modifier cleanup, and keeps event logic in dedicated methods.
Real-World Use
A data entry form handles input changes, form submission, field validation, and autocomplete. Each event is handled by a dedicated action method, making the code organized and testable.
flowchart LR
A[DOM Event] --> B[{{on}} modifier]
B --> C[@action method]
C --> D[Update tracked state]
C --> E[Call parent action]
E --> F[Parent component/route]
Basic Event Handling with {{on}}
Use the {{on}} modifier to bind DOM events.
{{! app/components/click-counter.hbs }}
<button type="button" {{on "click" this.increment}}>
Clicked {{this.count}} times
</button>
<button type="button" {{on "click" this.reset}}>
Reset
</button>
// app/components/click-counter.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class ClickCounterComponent extends Component {
@tracked count = 0;
@action
increment() {
this.count++;
}
@action
reset() {
this.count = 0;
}
}
Passing Actions to Child Components
Parent components pass action functions as arguments.
// app/components/parent-component.js
import Component from '@glimmer/component';
import { action } from '@ember/object';
export default class ParentComponent extends Component {
@action
handleChildClick(data) {
console.log('Received from child:', data);
}
@action
handleChildSubmit(formData) {
console.log('Form submitted:', formData);
}
}
{{! Child component invocation }}
<ChildButton @onClick={{this.handleChildClick}} />
<ChildForm @onSubmit={{this.handleChildSubmit}} />
// app/components/child-button.js
import Component from '@glimmer/component';
import { action } from '@ember/object';
export default class ChildButtonComponent extends Component {
@action
onClick() {
if (this.args.onClick) {
this.args.onClick({ timestamp: Date.now() });
}
}
}
Form Handling
// app/components/login-form.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class LoginFormComponent extends Component {
@tracked email = '';
@tracked password = '';
@tracked error = '';
@tracked isSubmitting = false;
get isFormValid() {
return this.email.length > 0 && this.password.length >= 6;
}
@action
updateEmail(event) {
this.email = event.target.value;
this.error = '';
}
@action
updatePassword(event) {
this.password = event.target.value;
this.error = '';
}
@action
async handleSubmit(event) {
event.preventDefault();
if (!this.isFormValid) {
this.error = 'Please fill in all fields';
return;
}
this.isSubmitting = true;
this.error = '';
try {
await this.args.onSubmit({
email: this.email,
password: this.password
});
} catch (error) {
this.error = error.message || 'Login failed';
} finally {
this.isSubmitting = false;
}
}
}
{{! app/components/login-form.hbs }}
<form {{on "submit" this.handleSubmit}}>
{{#if this.error}}
<div class="error" role="alert">{{this.error}}</div>
{{/if}}
<div class="form-field">
<label for="email">Email</label>
<input
id="email"
type="email"
value={{this.email}}
{{on "input" this.updateEmail}}
disabled={{this.isSubmitting}}
/>
</div>
<div class="form-field">
<label for="password">Password</label>
<input
id="password"
type="password"
value={{this.password}}
{{on "input" this.updatePassword}}
disabled={{this.isSubmitting}}
/>
</div>
<button type="submit" disabled={{or (not this.isFormValid) this.isSubmitting}}>
{{if this.isSubmitting "Logging in..." "Login"}}
</button>
</form>
Event Modifiers and Options
The {{on}} modifier supports event options.
{{! Passive event (for scroll performance) }}
<div {{on "scroll" this.handleScroll passive=true}}>
{{! Capture phase }}
<div {{on "click" this.handleOuterClick capture=true}}>
{{! Once — fires only once }}
<button {{on "click" this.handleFirstClick once=true}}>
{{! Prevent default automatically }}
<form {{on "submit" this.handleSubmit preventDefault=true}}>
Keyboard Events
// app/components/search-input.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class SearchInputComponent extends Component {
@tracked query = '';
@tracked selectedIndex = -1;
@action
handleInput(event) {
this.query = event.target.value;
this.selectedIndex = -1;
this.args.onQueryChange?.(this.query);
}
@action
handleKeydown(event) {
if (event.key === 'Escape') {
this.query = '';
this.args.onQueryChange?.('');
event.target.blur();
}
if (event.key === 'ArrowDown') {
event.preventDefault();
this.selectedIndex++;
this.args.onHighlightChange?.(this.selectedIndex);
}
if (event.key === 'ArrowUp') {
event.preventDefault();
this.selectedIndex = Math.max(-1, this.selectedIndex - 1);
this.args.onHighlightChange?.(this.selectedIndex);
}
if (event.key === 'Enter' && this.selectedIndex >= 0) {
event.preventDefault();
this.args.onSelect?.(this.selectedIndex);
}
}
@action
handleFocus() {
this.args.onFocus?.();
}
@action
handleBlur() {
// Delay to allow click on suggestion
setTimeout(() => {
this.args.onBlur?.();
}, 200);
}
}
Multiple Event Handlers
Multiple {{on}} modifiers can target the same event.
<button
type="button"
{{on "click" this.logClick}}
{{on "click" this.trackAnalytics}}
{{on "click" this.handleAction}}
>
Click me
</button>
Preventing Default Behavior
// app/components/link-button.js
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
export default class LinkButtonComponent extends Component {
@service router;
@action
handleClick(event) {
event.preventDefault();
event.stopPropagation();
this.router.transitionTo(this.args.route, this.args.model);
}
}
Action Modifier for Custom Events
The {{on}} modifier works with custom DOM events too.
<video
{{on "play" this.onPlay}}
{{on "pause" this.onPause}}
{{on "timeupdate" this.onTimeUpdate}}
{{on "ended" this.onEnded}}
controls
>
<source src={{@src}} type="video/mp4" />
</video>
Common Mistakes
- Forgetting
@actiondecorator on methods. Without@action,thismay be wrong when the method is called asynchronously. - Calling
event.preventDefault()manually instead of usingpreventDefault=true. ThepreventDefaultoption is cleaner and less error-prone. - Not cleaning up event listeners added outside the template.
{{on}}cleans up automatically. ManualaddEventListenerrequires manualremoveEventListenerinwillDestroy. - Passing action results instead of action references.
{{on "click" this.handleClick()}}calls the function immediately. Usethis.handleClickwithout parentheses. - Creating new functions in the template.
{{on "click" (fn this.handleClick @id)}}is fine.{{on "click" (fn (action this.handleClick) @id)}}with deprecatedactionhelper is not.
Practice Questions
- How do you bind a click event to a component action?
- What does the
@actiondecorator do? - How do you pass an action from parent to child?
- What event options does
{{on}}support? - Challenge: Create an autocomplete search component with: text input, dropdown suggestions, keyboard navigation (arrow up/down, enter to select, escape to close), debounced API calls, and blur handling. Use
{{on}}for all event bindings. Pass actions for selection and query change.
FAQ
{{< faq "How do I pass the event object to an action?" "The event is the first argument automatically. `{{on \"click\" this.handle}}` receives the event." >}}Mini Project
Create a complex form with: (1) Input validation on blur and on input. (2) Character count for textarea. (3) Tags input (type comma-separated values). (4) Auto-save on Ctrl+S keyboard shortcut. (5) Dirty state tracking with confirmation on navigation away. Use {{on}} for all event bindings. Use @action for all handlers.
What's Next
Now that you understand actions, learn Ember Testing for testing Ember applications. Then explore Ember Octane for modern Ember features.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro