# Child routing playbook

Child routing lets each routed component own its own navigation tree. Use it to build dashboards with nested layouts, multi-step forms, or resource detail pages that include tabs or auxiliary panels. This guide walks through the patterns you will use most often.

## 1. Define parent and child routes

Every routed component can declare a `routes` array inside the `@route` decorator. Parent components stay slim—most of the structure lives in the child components.

```typescript
import { route } from '@aurelia/router';
import { UsersPage } from './users/users-page';
import { ReportsPage } from './reports/reports-page';

@route({
  routes: [
    { path: '', component: UsersPage, title: 'Users' },
    { path: 'reports', component: ReportsPage, title: 'Reports' },
  ]
})
export class AdminLayout {}
```

Each child component can keep nesting:

```typescript
import { route } from '@aurelia/router';
import { UserOverview } from './user-overview';
import { UserSettings } from './user-settings';

@route({
  routes: [
    { path: ':id', component: UserOverview, title: 'Overview' },
    { path: ':id/settings', component: UserSettings, title: 'Settings' },
  ]
})
export class UsersPage {}
```

When the router loads `AdminLayout`, it automatically instantiates the nested layout components and surfaces their routes inside the `<au-viewport>` declared in each template.

## 2. Render child viewports in parent templates

Every component that declares child routes must include at least one `<au-viewport>` in its view:

```html
<!-- admin-layout.html -->
<nav>
  <a load="">Users</a>
  <a load="reports">Reports</a>
</nav>

<au-viewport></au-viewport>
```

```html
<!-- users-page.html -->
<section class="users">
  <aside>
    <user-list></user-list>
  </aside>
  <main>
    <au-viewport></au-viewport>
  </main>
</section>
```

You can name child viewports to run siblings in parallel:

```html
<au-viewport name="main"></au-viewport>
<au-viewport name="details"></au-viewport>
```

Then target them with multi-viewport instructions such as `href="orders@main+profile@details"` or `router.load([{ component: Orders, viewport: 'main' }, { component: Profile, viewport: 'details' }])`. See [Viewports](https://docs.aurelia.io/getting-to-know-aurelia/aurelia-router/viewports#sibling-viewports) for more combinations.

## 3. Share layout data across child routes

Load shared data once in the parent and expose it through a service that both parent and children resolve from DI.

```typescript
import { singleton } from '@aurelia/kernel';

type AdminSummary = {
  totalUsers: number;
  activeUsers: number;
};

@singleton()
export class AdminStatsStore {
  private readonly fallback: AdminSummary = { totalUsers: 0, activeUsers: 0 };
  private _value: AdminSummary | null = null;

  set(summary: AdminSummary) {
    this._value = summary;
  }

  get value() {
    return this._value ?? this.fallback;
  }
}
```

```typescript
import { IRouteViewModel, Params } from '@aurelia/router';
import { resolve } from '@aurelia/kernel';

export class AdminLayout implements IRouteViewModel {
  private readonly store = resolve(AdminStatsStore);

  async loading(_params: Params) {
    const summary = await fetch('/api/admin/summary').then(res => res.json());
    this.store.set(summary);
  }
}
```

```typescript
import { resolve } from '@aurelia/kernel';

export class UsersPage {
  private readonly store = resolve(AdminStatsStore);

  get stats() {
    return this.store.value;
  }
}
```

Because the store is a singleton, each child route can read the latest summary without manually passing data down the tree.

## 4. Navigate within the current hierarchy

Relative navigation keeps nested layouts decoupled from the app root. Always resolve `IRouteContext` (or pass `context` through `router.load`) when a child needs to target a sibling or parent.

```typescript
import { IRouter, IRouteContext } from '@aurelia/router';
import { resolve } from '@aurelia/kernel';

export class UserTabs {
  private readonly router = resolve(IRouter);
  private readonly context = resolve(IRouteContext);

  openSettings(id: string) {
    return this.router.load(`${id}/settings`, {
      context: this.context,
    });
  }
}
```

You can achieve the same thing in templates:

```html
<a href="../">Back to list</a>
<a href="../${user.id}/settings">Settings</a>
```

The `../` prefix climbs up one routing context before evaluating the rest of the path.

## 5. Combine child routes with parameters

Child routes can declare their own parameters and still reuse parent parameters. The router merges them automatically when you call `IRouteContext.getRouteParameters({ includeQueryParams: true })` or receive the `Params` argument in lifecycle hooks. See the [Route parameters guide](https://docs.aurelia.io/getting-to-know-aurelia/aurelia-router/fundamentals/route-parameters) for a complete walkthrough.

## 6. Lazy-load nested modules

You can reference dynamic imports inside any `component` slot. The router will `await` the module before instantiating the component.

```typescript
@route({
  routes: [
    {
      path: 'reports',
      component: () => import('./reports/reports-index').then(m => m.ReportsIndex),
      title: 'Reports',
    },
  ],
})
export class AdminLayout {}
```

This works at every level of the tree, so you pay the cost only when users actually navigate there.

## 7. Test nested layouts in isolation

Create the parent component via the testing harness, call `router.load` with a path that exercises the child routes, and then assert against the rendered DOM. Because every child route uses a real component (not a string lookup), you get high-confidence integration coverage:

```typescript
import { createFixture } from '@aurelia/testing';
import { RouterConfiguration, IRouter } from '@aurelia/router';

const { appHost, container, startPromise, stop } = createFixture(
  '<au-viewport></au-viewport>',
  AdminLayout,
  [RouterConfiguration],
);
await startPromise;

const router = container.get(IRouter);
await router.load('reports/daily');
expect(appHost.querySelector('reports-daily')).not.toBeNull();

await stop(true);
```

## Scenario recipes

### Tabs inside a detail page

**Outcome:** `/users/:id` loads a layout with tabs (`overview`, `activity`, `settings`) without re-rendering the outer chrome.

1. Parent layout defines `routes` for each tab and keeps the `<au-viewport>` inside the main column.
2. Tabs use `href="tabName"` so navigation stays relative to the current user context.
3. Store the selected tab in a store or read it from `ICurrentRoute.fragment` if you also want anchor links.

Validation checklist:

* Navigating from `overview` to `settings` preserves the `:id` value.
* Browser back button cycles tabs without losing the parent layout.
* A deep link to `/users/42/settings` opens the settings tab immediately.

### Protected admin area with nested guards

**Outcome:** Block access to admin child routes unless the parent layout validates the session, while letting each child enforce its own role.

1. Implement `canLoad` on the parent layout to check authentication. Return `'login'` to redirect unauthorized users.
2. Register additional router hooks (or per-view-model `canLoad`) on children for permissions such as `reports:read`.
3. Use `IRouterEvents` to show a toast whenever a guard cancels navigation.

Validation checklist:

* Visiting `/admin/reports` while logged out redirects to `/login`.
* Visiting `/admin/users` with insufficient role triggers the child guard and surfaces an error message.
* Successful navigation still shows the admin shell.

### Multi-pane dashboards

**Outcome:** A dashboard shows a list in the left viewport and detail in the right viewport, both driven by routing.

1. Parent template declares `<au-viewport name="list">` and `<au-viewport name="detail">`.
2. Route instructions load both panes simultaneously, e.g. `router.load([{ component: ReportsList, viewport: 'list' }, { component: ReportsDetail, params: { id }, viewport: 'detail' }])`.
3. Child components navigate using `context: resolve(IRouteContext)` to avoid resetting the other viewport.

Validation checklist:

* Loading `/dashboard` shows default list + placeholder detail.
* Clicking a row updates only the detail viewport.
* Sharing `/dashboard@detail=report/weekly` opens the same detail for other users.

## Related resources

* [Configuring routes](https://docs.aurelia.io/getting-to-know-aurelia/aurelia-router/fundamentals/configuring-routes)
* [Viewports](https://docs.aurelia.io/getting-to-know-aurelia/aurelia-router/fundamentals/viewports)
* [Routing lifecycle](https://docs.aurelia.io/getting-to-know-aurelia/aurelia-router/lifecycle-and-events/routing-lifecycle)
* [Route parameters](https://docs.aurelia.io/getting-to-know-aurelia/aurelia-router/fundamentals/route-parameters)
* [Navigating](https://docs.aurelia.io/getting-to-know-aurelia/navigation/navigating#navigate-in-current-and-ancestor-routing-context)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.aurelia.io/getting-to-know-aurelia/aurelia-router/fundamentals/child-routing.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
