Ember Nested Routes — Hierarchical Layouts and Data
In this tutorial, you will learn about Ember Nested Routes. We cover key concepts, practical examples, and best practices to help you master this topic.
Ember nested routes create hierarchical URL structures with shared parent layouts. Child routes render into the parent's {{outlet}}, enabling patterns like sidebar-main layouts, multi-step wizards, and sectioned admin panels with shared navigation.
What You'll Learn
You will learn how to nest routes, share layouts between routes, access parent model data from child routes, and build breadcrumb navigation.
Why It Matters
Nested routes eliminate redundant templates. Instead of repeating the sidebar on every settings page, the parent settings route renders it once. This keeps templates DRY and consistent.
Real-World Use
An admin panel uses nested routes: /admin/users, /admin/users/:id, /admin/users/:id/edit. The admin layout template provides the sidebar navigation once, and each child route renders only its unique content.
flowchart TD
A[/admin] --> B[admin.hbs]
B --> C[{{outlet}}]
C --> D[/admin/users]
C --> E[/admin/settings]
C --> F[/admin/reports]
D --> G[users.hbs]
D --> H[{{outlet}}]
H --> I[/admin/users/new]
H --> J[/admin/users/:id]
Defining Nested Routes
Use a callback function to define children within this.route().
// app/router.js
Router.map(function() {
this.route('admin', function() {
this.route('users');
this.route('users', function() {
this.route('user', { path: ':user_id' });
this.route('new');
});
this.route('settings');
this.route('reports');
});
});
Parent Route Template with Outlet
The parent template defines the shared layout and places {{outlet}} where child routes render.
{{! app/templates/admin.hbs }}
<div class="admin-layout">
<aside class="admin-sidebar">
<nav>
<LinkTo @route="admin.users">Users</LinkTo>
<LinkTo @route="admin.settings">Settings</LinkTo>
<LinkTo @route="admin.reports">Reports</LinkTo>
</nav>
</aside>
<main class="admin-content">
{{outlet}}
</main>
</div>
Child Route Handlers
Child route handlers live in nested folders matching the URL structure.
// app/routes/admin/users.js
import Route from '@ember/routing/route';
export default class AdminUsersRoute extends Route {
async model() {
return this.store.findAll('user');
}
}
{{! app/templates/admin/users.hbs }}
<h1>Users</h1>
<table>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{{#each this.model as |user|}}
<tr>
<td>{{user.name}}</td>
<td>{{user.email}}</td>
<td>
<LinkTo @route="admin.users.user" @model={{user.id}}>
View
</LinkTo>
</td>
</tr>
{{/each}}
</tbody>
</table>
<LinkTo @route="admin.users.new">Add User</LinkTo>
{{outlet}}
Accessing Parent Route Data
Child routes can access parent route data using modelFor.
// app/routes/admin/users/user.js
import Route from '@ember/routing/route';
export default class AdminUsersUserRoute extends Route {
model(params) {
// Access parent model
let users = this.modelFor('admin.users');
return users.findBy('id', params.user_id);
}
setupController(controller, model) {
super.setupController(controller, model);
// Access multiple parent levels
let adminModel = this.modelFor('admin');
controller.set('adminTitle', adminModel.title);
}
}
{{! app/templates/admin/users/user.hbs }}
<h2>User: {{this.model.name}}</h2>
<div class="user-detail">
<p><strong>Email:</strong> {{this.model.email}}</p>
<p><strong>Role:</strong> {{this.model.role}}</p>
<p><strong>Joined:</strong> {{this.model.createdAt}}</p>
</div>
<div class="user-actions">
<LinkTo @route="admin.users">Back to Users</LinkTo>
</div>
Multiple Outlets
Use named outlets to render child routes into specific locations.
Router.map(function() {
this.route('products', function() {
this.route('product', { path: ':product_id' });
this.route('compare');
});
});
{{! app/templates/products.hbs }}
<div class="products-layout">
<div class="products-list">
{{outlet "sidebar"}}
</div>
<div class="products-main">
{{outlet}}
</div>
<div class="products-compare">
{{outlet "compare"}}
</div>
</div>
Breadcrumb Navigation
Use nested route names to build breadcrumbs.
// app/controllers/admin/users/user.js
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
export default class AdminUsersUserController extends Controller {
@service router;
get breadcrumbs() {
return [
{ label: 'Home', route: 'index' },
{ label: 'Admin', route: 'admin' },
{ label: 'Users', route: 'admin.users' },
{ label: this.model.name, route: 'admin.users.user' }
];
}
}
{{! app/components/breadcrumbs.hbs }}
<nav class="breadcrumbs" aria-label="breadcrumb">
<ol>
{{#each @breadcrumbs as |crumb index|}}
<li>
{{#if (eq index (sub @breadcrumbs.length 1))}}
<span aria-current="page">{{crumb.label}}</span>
{{else}}
<LinkTo @route={{crumb.route}}>{{crumb.label}}</LinkTo>
{{/if}}
</li>
{{/each}}
</ol>
</nav>
Loading and Error Substates
Nested routes support loading and error templates per nesting level.
{{! app/templates/admin/loading.hbs }}
{{! Shows while ANY child of admin is loading }}
<div class="admin-loading">
<div class="spinner"></div>
<p>Loading admin section...</p>
</div>
{{! app/templates/admin/error.hbs }}
{{! Shows when ANY child of admin has an error }}
<div class="admin-error">
<h2>Admin Error</h2>
<p>There was a problem loading this section.</p>
<p>{{this.model.message}}</p>
<LinkTo @route="admin">Back to Admin Home</LinkTo>
</div>
Common Mistakes
- Too many nesting levels. Three levels maximum. Deeper nesting makes code hard to follow and hurts performance.
- Forgetting
{{outlet}}in every parent. Each nesting level needs its own{{outlet}}. Missing outlet means children never render. - Accessing
modelForwith wrong route name. Use the full route name as defined inrouter.js. Check for typos. - Loading all data in the parent model hook. Each route should load only its own data. Parent loads shared data, child loads specific data.
- Not handling loading states per level. A single loading template covers all children. Create granular loading templates for better UX.
Practice Questions
- How do you define a nested route in Ember?
- What is the purpose of
{{outlet}}in parent templates? - How do you access parent model data from a child route?
- Why should you limit nesting depth?
- Challenge: Create a nested route structure for a documentation site:
/docs,/docs/:section,/docs/:section/:page. Each level has its own layout. Breadcrumbs should show the full path. Use named outlets for sidebar and content.
FAQ
{{< faq "How do I render a child route in a different outlet?" "Use named outlets: `{{outlet \"sidebar\"}}` and specify the outlet name in `transitionTo` or `LinkTo`." >}}Mini Project
Build a documentation browser with three levels of nested routes. Level 1: documentation sections list. Level 2: section overview with page list in sidebar. Level 3: individual page content. Each level has loading and error substates. Breadcrumbs at the top show the current path. Use modelFor in level 3 to show section title.
What's Next
Now that you understand nested routes, learn Ember Templates for Handlebars syntax. Then explore Ember Components for reusable UI.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro