Getting started
Introduction
Ember.js is a framework for building ambitious web applications with convention over configuration. Modern Ember (Octane v4+) uses Glimmer components, tracked properties, and native JavaScript classes.
Installation
npm install -g ember-cli
ember new my-app
cd my-app
ember serve
Quick Example
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { on } from '@ember/modifier';
export default class Counter extends Component {
@tracked count = 0;
increment = () => {
this.count++;
};
<template>
<p>Count: {{this.count}}</p>
<button {{on "click" this.increment}}>
+1
</button>
</template>
}
This uses the modern template tag format (.gjs files).
Ember CLI
Common Commands
ember serve
Start dev server (localhost:4200)
ember generate component my-component
Generate new component
ember generate route about
Generate new route
ember generate service shopping-cart
Generate new service
ember generate model post
Generate Ember Data model
ember build --environment=production
Build for production
ember test
Run test suite
ember test --server
Run tests in watch mode
Generators
| Command | Description |
|---|---|
ember g component NAME |
Glimmer component |
ember g route NAME |
Route + template |
ember g service NAME |
Service class |
ember g model NAME |
Ember Data model |
ember g helper NAME |
Template helper |
ember g modifier NAME |
Custom modifier |
ember g adapter NAME |
Data adapter |
ember g serializer NAME |
Data serializer |
ember g util NAME |
Utility module |
Components (Glimmer)
Template Tag Format
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { on } from '@ember/modifier';
export default class UserCard extends Component {
@tracked showDetails = false;
toggleDetails = () => {
this.showDetails = !this.showDetails;
};
<template>
<div class="card">
<h3>{{@name}}</h3>
<button {{on "click" this.toggleDetails}}>
Toggle
</button>
{{#if this.showDetails}}
<p>{{@bio}}</p>
{{/if}}
</div>
</template>
}
Use .gjs extension for template tag components.
Component Arguments
<UserCard @name="Tom" @bio="Developer" />
// Access in JavaScript
export default class UserCard extends Component {
get displayName() {
return this.args.name.toUpperCase();
}
}
<p></p>
<p></p>
Arguments use @ prefix in templates, this.args in JavaScript.
Tracked Properties
import { tracked } from "@glimmer/tracking";
export default class Counter extends Component {
@tracked count = 0;
@tracked items = [];
increment = () => {
this.count++;
};
addItem = (item) => {
// Reassign for reactivity
this.items = [...this.items, item];
};
}
@tracked makes properties reactive. Reassign arrays/objects for nested changes.
Actions with fn Helper
import { fn } from "@ember/helper";
<button >
+1
</button>
<button >
-1
</button>
change = (delta) => {
this.count += delta;
};
Use fn helper to pass arguments to actions.
Lifecycle Hooks
import { modifier } from "ember-modifier";
export default class MyComponent extends Component {
constructor() {
super(...arguments);
// Component created
}
willDestroy() {
super.willDestroy();
// Cleanup before removal
}
}
Glimmer components have minimal lifecycle hooks. Use modifiers for DOM operations.
Block Content (Yield)
<div class="card">
<h3></h3>
<div class="card-body">
</div>
</div>
<Card @title="Welcome">
<p>This content goes into </p>
</Card>
Templates (Handlebars)
Conditionals
<p>Loading...</p>
<p>Error occurred</p>
<p></p>
<button>Click me</button>
Loops
<li>: </li>
<p>No items found</p>
<div></div>
Let Blocks
<p>Hello, !</p>
<p> is </p>
Built-in Helpers
| Helper | Description |
|---|---|
{{if condition value}} |
Inline conditional |
{{unless condition value}} |
Inline negated conditional |
{{concat "a" "b"}} |
Concatenate strings |
{{get object "key"}} |
Dynamic property access |
{{array "a" "b"}} |
Create array |
{{hash key=value}} |
Create object |
{{fn callback arg}} |
Partial application |
Inline Conditionals
<div class=>
</div>
<button disabled=>
Submit
</button>
Routing
Router Configuration
// app/router.js
import EmberRouter from "@ember/routing/router";
import config from "./config/environment";
export default class Router extends EmberRouter {
location = config.locationType;
rootURL = config.rootURL;
}
Router.map(function () {
this.route("about");
this.route("contact", { path: "/contact-us" });
this.route("posts", function () {
this.route("new");
this.route("edit", { path: "/:post_id/edit" });
});
this.route("post", { path: "/posts/:post_id" });
this.route("not-found", { path: "/*path" });
});
Route Handlers
import Route from "@ember/routing/route";
import { service } from "@ember/service";
export default class PostsRoute extends Route {
@service store;
model() {
return this.store.findAll("post");
}
}
// Dynamic segment
export default class PostRoute extends Route {
@service store;
model(params) {
return this.store.findRecord("post", params.post_id);
}
}
Query Parameters
import Controller from "@ember/controller";
import { tracked } from "@glimmer/tracking";
export default class PostsController extends Controller {
queryParams = ["category", "page"];
@tracked category = "all";
@tracked page = 1;
}
// Refresh model on query param change
export default class PostsRoute extends Route {
queryParams = {
category: { refreshModel: true },
};
model(params) {
return this.store.query("post", {
category: params.category,
});
}
}
LinkTo Component
<LinkTo @route="about">About</LinkTo>
<LinkTo @route="post" @model=>
</LinkTo>
<LinkTo @route="posts" @query=>
Tech Posts
</LinkTo>
Programmatic Transitions
import Component from "@glimmer/component";
import { service } from "@ember/service";
export default class LoginForm extends Component {
@service router;
handleSubmit = async () => {
await this.login();
this.router.transitionTo("dashboard");
};
}
// With params
this.router.transitionTo("post", postId);
Services
Creating Services
import Service from "@ember/service";
import { tracked } from "@glimmer/tracking";
export default class ShoppingCartService extends Service {
@tracked items = [];
add(item) {
this.items = [...this.items, item];
}
remove(item) {
this.items = this.items.filter((i) => i !== item);
}
get total() {
return this.items.reduce((sum, item) => {
return sum + item.price;
}, 0);
}
get count() {
return this.items.length;
}
}
Injecting Services
import Component from "@glimmer/component";
import { service } from "@ember/service";
export default class CartSummary extends Component {
@service shoppingCart;
checkout = () => {
console.log(this.shoppingCart.total);
};
}
// Custom name
@service('shopping-cart') cart;
Common Service Patterns
// Session management
@service session;
@service currentUser;
// Data
@service store;
// Routing
@service router;
// Media queries
@service media;
Ember Data
Models
import Model, { attr, belongsTo, hasMany } from "@ember-data/model";
export default class PostModel extends Model {
@attr("string") title;
@attr("string") body;
@attr("date") publishedAt;
@belongsTo("user") author;
@hasMany("comment") comments;
}
Attribute Types
| Type | Description |
|---|---|
'string' |
String values |
'number' |
Numeric values |
'boolean' |
True/false |
'date' |
Date objects |
undefined |
Any type |
Store Methods
import { service } from "@ember/service";
export default class PostsRoute extends Route {
@service store;
async model() {
// Find all records
return this.store.findAll("post");
}
}
// Find single record
this.store.findRecord("post", 1);
// Query with filters
this.store.query("post", { filter: { published: true } });
// Peek cached record
this.store.peekRecord("post", 1);
// Create new record
let post = this.store.createRecord("post", {
title: "New Post",
});
await post.save();
// Delete record
await post.destroyRecord();
Relationships
// belongsTo
let author = await post.author;
// hasMany
let comments = await post.comments;
// Set belongsTo
post.author = user;
// Add to hasMany
post.comments.push(comment);
Modifiers
Built-in Modifiers
<button >
Save
</button>
<input />
Custom Modifiers
import { modifier } from "ember-modifier";
export default modifier((element) => {
element.focus();
});
<input />
Modifiers with Arguments
import { modifier } from "ember-modifier";
export default modifier((element, [color]) => {
element.style.color = color;
});
<p >Red text</p>
Cleanup in Modifiers
import { modifier } from "ember-modifier";
export default modifier((element) => {
const handler = () => console.log("Clicked");
element.addEventListener("click", handler);
return () => {
element.removeEventListener("click", handler);
};
});
Testing
Component Tests
import { module, test } from "qunit";
import { setupRenderingTest } from "ember-qunit";
import { render, click } from "@ember/test-helpers";
import { hbs } from "ember-cli-htmlbars";
module("Component | user-card", function (hooks) {
setupRenderingTest(hooks);
test("renders user name", async function (assert) {
this.set("user", { name: "Tom" });
await render(hbs`
<UserCard @user={{this.user}} />
`);
assert.dom("[data-test-name]").hasText("Tom");
});
test("handles click", async function (assert) {
await render(hbs`<UserCard />`);
await click("[data-test-button]");
assert.dom("[data-test-details]").exists();
});
});
Acceptance Tests
import { module, test } from "qunit";
import { setupApplicationTest } from "ember-qunit";
import { visit, currentURL, fillIn, click } from "@ember/test-helpers";
module("Acceptance | login", function (hooks) {
setupApplicationTest(hooks);
test("user can login", async function (assert) {
await visit("/login");
await fillIn("[data-test-email]", "user@example.com");
await fillIn("[data-test-password]", "password");
await click("[data-test-submit]");
assert.strictEqual(currentURL(), "/dashboard");
});
});
Test Helpers
| Helper | Description |
|---|---|
render(template) |
Render component |
click(selector) |
Click element |
fillIn(selector, text) |
Fill input field |
visit(url) |
Navigate to URL |
currentURL() |
Get current URL |
find(selector) |
Query DOM element |
findAll(selector) |
Query all elements |
waitFor(selector) |
Wait for element |
Integration Test Example
import { module, test } from "qunit";
import { setupRenderingTest } from "ember-qunit";
import { render } from "@ember/test-helpers";
import { hbs } from "ember-cli-htmlbars";
module("Integration | Helper | format-date", function (hooks) {
setupRenderingTest(hooks);
test("formats date", async function (assert) {
this.set("date", new Date("2026-02-06"));
await render(hbs`{{format-date this.date}}`);
assert.dom().hasText("Feb 6, 2026");
});
});
Decorators
@tracked
import { tracked } from "@glimmer/tracking";
export default class Counter extends Component {
@tracked count = 0;
@tracked items = [];
}
Makes properties reactive in templates.
@service
import { service } from "@ember/service";
export default class MyComponent extends Component {
@service router;
@service("shopping-cart") cart;
}
Injects service dependency.
@action
import { action } from "@ember/object";
export default class MyComponent extends Component {
@action
handleClick() {
// 'this' is bound
}
}
Binds method to component instance (or use arrow functions).
Gotchas
Template Tag Files
Use .gjs extension for template tag format, not .js.
// ✅ my-component.gjs
export default class MyComponent extends Component {
<template>...</template>
}
// ❌ my-component.js
// Won't work with <template>
Immutable Arguments
Arguments passed to components are immutable.
// ❌ Can't mutate args
this.args.name = "New Name";
// ✅ Create tracked local copy
@tracked localName = this.args.name;
this.localName = 'New Name';
Tracked Mutations
@tracked doesn't track nested mutations. Reassign the entire array/object.
// ❌ Won't update template
this.items.push(item);
// ✅ Reassign for reactivity
this.items = [...this.items, item];
Action Binding
Use arrow functions to preserve this context.
// ✅ Arrow function
increment = () => {
this.count++;
};
// ❌ Regular function loses 'this'
increment() {
this.count++; // Error
}
Model Promises
Route model() hooks must return or await promises.
// ✅ Return promise
model() {
return this.store.findAll('post');
}
// ✅ Or await
async model() {
const posts = await this.store.findAll('post');
return posts;
}
Classic vs Octane
Classic Ember patterns are deprecated. Use Glimmer components, not @ember/component.
// ❌ Classic Component
import Component from "@ember/component";
// ✅ Glimmer Component
import Component from "@glimmer/component";
Also see
- Ember.js Official Guides - Comprehensive framework documentation
- Ember.js Homepage - Official website and downloads
- Ember CLI Guides - Command-line tool documentation
- Ember Data Guide - Data management layer
- Ember Inspector - Browser DevTools extension
- Glimmer Components - Modern component architecture