Visual Diagrams
Visual explanations of Aurelia 2's router architecture and concepts.
Table of Contents
1. Route Matching Pipeline
How the router resolves a URL to components:
┌──────────────────────────────────────────────────────────┐
│ User navigates to: /products/42/reviews │
└──────────────────┬───────────────────────────────────────┘
↓
┌──────────────────────┐
│ 1. Parse URL │
│ Path: /products/42/ │
│ reviews │
│ Fragment: #section2 │
│ Query: ?sort=date │
└──────────┬───────────┘
↓
┌──────────────────────┐
│ 2. Match Routes │
│ - Check path pattern │
│ - Extract params │
│ - Apply constraints │
└──────────┬───────────┘
↓
┌──────────────────────┐
│ 3. Build Route Tree │
│ Root │
│ └─ Products (:id) │
│ └─ Reviews │
└──────────┬───────────┘
↓
┌──────────────────────┐
│ 4. Execute Hooks │
│ - canLoad (guard) │
│ - loading (data) │
│ - canUnload (prev) │
└──────────┬───────────┘
↓
┌──────────────────────┐
│ 5. Render Components │
│ - Swap viewports │
│ - loaded hooks │
│ - Update title │
└──────────────────────┘
Route Configuration Match Example:
routes: [
{
path: 'products/:id', ✓ Matches /products/42
component: ProductDetail,
routes: [
{ path: 'reviews', ... } ✓ Matches /reviews
]
},
{
path: 'products/:id{{^\\d+$}}', ✓ Only if :id is numeric
component: ProductDetail
}
]Key Points:
Routes are matched top-to-bottom in configuration order
First matching route wins
Parameters are extracted during matching
Constraints (
{{regex}}) are validatedHierarchical routes build a route tree
Route matching documentation →
2. Navigation Flow
How different navigation methods work:
┌─────────────────────────────────────────────────────────────┐
│ NAVIGATION METHODS │
└─────────────────────────────────────────────────────────────┘
METHOD 1: href attribute (Declarative)
─────────────────────────────────────────
<a href="products/42"> ┌──────────────┐
─────────────────────────────>│ href handler │
└──────┬───────┘
↓
┌──────────────────┐
│ Parse URL string │
└──────────┬───────┘
↓
Navigate to URL
Context: Current route context by default
Use ../ to navigate to parent context
METHOD 2: load attribute (Structured)
──────────────────────────────────────────
<a load="route: products; ┌──────────────┐
params.bind: {id: 42}">───>│ load handler │
└──────┬───────┘
↓
┌──────────────────────┐
│ Build instruction │
│ from structured data │
└──────────┬───────────┘
↓
Navigate to route
Context: Current by default, can bind custom context
Active: Supports .active bindable for styling
METHOD 3: IRouter.load() (Programmatic)
────────────────────────────────────────────
router.load('products/42', { ┌──────────────┐
queryParams: { ... }, │ IRouter.load │
context: this └──────┬───────┘
}); ↓
┌──────────────────────┐
│ Full JavaScript API │
│ - Error handling │
│ - Async/await │
│ - Options object │
└──────────┬───────────┘
↓
Navigate to route
Context: Root by default (different from href/load!)
Returns: Promise<boolean> for success/failure
ALL METHODS CONVERGE
────────────────────────────────────────────
↓
┌────────────────────┐
│ Router Core Engine │
└──────────┬─────────┘
↓
┌────────────────────┐
│ Route Matching │
│ Hook Execution │
│ Component Loading │
└────────────────────┘Decision Guide:
Use
hreffor simple, static linksUse
loadwhen you need parameter binding or active stateUse
IRouter.load()for conditional/programmatic navigation
3. Lifecycle Hook Execution Order
Complete sequence when navigating from ComponentA to ComponentB:
┌────────────────────────────────────────────────────────┐
│ Navigation: /page-a → /page-b │
└────────────────────────────────────────────────────────┘
PHASE 1: CAN UNLOAD (Current Component)
════════════════════════════════════════
ComponentA (current)
↓
┌─────────────────────────────────┐
│ 1. canUnload() │ → Return false to cancel navigation
│ - Check unsaved changes │ Return true to allow
│ - User confirmation │
└─────────────────┬───────────────┘
↓
[Navigation Cancelled?] ─── No ──→ Continue
│
Yes
↓
Stay on page A
PHASE 2: CAN LOAD (Next Component)
══════════════════════════════════════
ComponentB (next)
↓
┌─────────────────────────────────┐
│ 2. Router hooks: canLoad() │ → Return false to block
│ - Authentication checks │ Return NavigationInstruction to redirect
│ - Authorization │ Return true to allow
└─────────────────┬───────────────┘
↓
┌─────────────────────────────────┐
│ 3. Component: canLoad() │ → Component-level validation
│ - Parameter validation │
│ - Conditional logic │
└─────────────────┬───────────────┘
↓
[Navigation Allowed?] ─── No ──→ Show fallback or redirect
│
Yes
↓
Continue to load
PHASE 3: UNLOADING (Current Component)
═══════════════════════════════════════
ComponentA (current)
↓
┌─────────────────────────────────┐
│ 4. Router hooks: unloading() │
│ - Global cleanup │
└─────────────────┬───────────────┘
↓
┌─────────────────────────────────┐
│ 5. Component: unloading() │
│ - Save drafts │
│ - Cleanup subscriptions │
│ - Log analytics │
└─────────────────┬───────────────┘
↓
┌─────────────────────────────────┐
│ 6. Component detached │ ← Standard Aurelia lifecycle
│ - DOM removal │
└─────────────────────────────────┘
PHASE 4: LOADING (Next Component)
══════════════════════════════════════
ComponentB (next)
↓
┌─────────────────────────────────┐
│ 7. Router hooks: loading() │
│ - Shared data loading │
└─────────────────┬───────────────┘
↓
┌─────────────────────────────────┐
│ 8. Component: loading() │
│ - Fetch component data │
│ - Initialize state │
│ - Show loading UI │
└─────────────────┬───────────────┘
↓
┌─────────────────────────────────┐
│ 9. Component attached │ ← Standard Aurelia lifecycle
│ - DOM insertion │
└─────────────────┬───────────────┘
↓
Swap viewport content
(ComponentA → ComponentB)
↓
┌─────────────────────────────────┐
│ 10. Component: loaded() │
│ - Post-render effects │
│ - Scroll to top │
│ - Track page view │
└─────────────────┬───────────────┘
↓
┌─────────────────────────────────┐
│ 11. Update browser history │
│ Update document title │
└─────────────────────────────────┘
↓
Navigation Complete
TIMING DIAGRAM (with async operations)
═══════════════════════════════════════════
Time ComponentA ComponentB
──── ────────── ──────────
0ms canUnload() ────────┐
│
100ms └─> [approved]
canLoad() ──────┐
│
200ms └─> [approved]
unloading() ───────┐
│
250ms └─> [cleanup done]
loading() ──────┐
│ ← async data fetch
400ms └─> [data loaded]
[detached]
[attached]
loaded() ───────┐
│
410ms └─> [done]
████████ (visible) ░░░░░░░░ (hidden)
░░░░░░░░ (hidden) ████████ (visible)Important Notes:
All hooks can be async (return Promise)
Router waits for each hook to complete before proceeding
Returning
falsefrom guard hooks stops navigationRouter hooks run before component hooks
unloadingandloadinghappen in parallel for performance
Lifecycle hooks documentation →
4. Component vs Router Hooks
Two ways to implement lifecycle logic:
┌─────────────────────────────────────────────────────────────┐
│ COMPONENT HOOKS (Local) │
├─────────────────────────────────────────────────────────────┤
│ │
│ export class ProductDetail implements IRouteViewModel { │
│ canLoad(params: Params): boolean { │
│ // 'this' refers to component instance │
│ return this.validateProduct(params.id); │
│ } │
│ } │
│ │
│ ✓ Use for component-specific logic │
│ ✓ Direct access to component state via 'this' │
│ ✓ Runs only for this component │
│ ✗ Cannot share logic across components │
└─────────────────────────────────────────────────────────────┘
↓↑
┌─────────────────────────────────────────────────────────────┐
│ ROUTER HOOKS (Shared/Global) │
├─────────────────────────────────────────────────────────────┤
│ │
│ @lifecycleHooks() │
│ export class AuthHook { │
│ canLoad( │
│ viewModel: IRouteViewModel, ← component instance │
│ params: Params, │
│ next: RouteNode │
│ ): boolean { │
│ // 'this' is the hook instance, not the component │
│ return this.authService.isAuthenticated(); │
│ } │
│ } │
│ │
│ // Register globally │
│ Aurelia.register(AuthHook); │
│ │
│ ✓ Share logic across all components │
│ ✓ Centralized cross-cutting concerns │
│ ✓ Access component via viewModel parameter │
│ ✗ Extra indirection to access component state │
└─────────────────────────────────────────────────────────────┘
EXECUTION ORDER (both registered)
══════════════════════════════════════════════════════════════
Navigation triggered
↓
┌─────────────────────┐
│ 1. Router Hooks │ ← Runs first (global checks)
│ canLoad() │
└──────────┬──────────┘
↓
[return false?] ─── Yes ──→ Navigation blocked
│
No
↓
┌─────────────────────┐
│ 2. Component Hook │ ← Runs second (local checks)
│ canLoad() │
└──────────┬──────────┘
↓
[return false?] ─── Yes ──→ Navigation blocked
│
No
↓
Navigation continues
COMMON PATTERNS
═══════════════════════════════════════════════════════════
Pattern 1: Authentication (Router Hook)
────────────────────────────────────────
@lifecycleHooks()
class AuthHook {
canLoad(...) {
if (!isLoggedIn) return 'login';
return true;
}
}
→ Applies to all routes
→ Centralized auth logic
Pattern 2: Data Loading (Component Hook)
─────────────────────────────────────────
class ProductDetail implements IRouteViewModel {
async loading(params: Params) {
this.product = await fetchProduct(params.id);
}
}
→ Component-specific data
→ Direct state access
Pattern 3: Mixed Approach (Both)
─────────────────────────────────────────
@lifecycleHooks()
class PermissionHook {
canLoad(vm, params, next) {
const requiredPermission = next.data?.permission;
return this.hasPermission(requiredPermission);
}
}
class AdminPanel implements IRouteViewModel {
canLoad(params) {
// Additional component-specific checks
return this.validateContext(params);
}
}
→ Global permission check first
→ Then component-specific validationDecision Guide:
Router hooks for: Authentication, authorization, logging, analytics
Component hooks for: Data fetching, validation, component state
Both when you need layered checks (global + local)
Router hooks → | Component hooks →
5. Viewport Hierarchy
How viewports nest and relate to each other:
SIMPLE (SINGLE VIEWPORT)
════════════════════════════════════
<my-app>
<nav>...</nav>
<au-viewport></au-viewport> ← Single viewport
</my-app>
Route: /products
↓
┌────────────────┐
│ <my-app> │
│ <nav> │
│ ┌──────────┐ │
│ │ Products │ │ ← Loaded into viewport
│ └──────────┘ │
│ </my-app> │
└────────────────┘
HIERARCHICAL (NESTED VIEWPORTS)
═══════════════════════════════════════════════
<my-app>
<au-viewport></au-viewport> ← Root viewport
↓
<products-page>
<au-viewport></au-viewport> ← Child viewport
↓
<product-detail>
</product-detail>
</products-page>
</my-app>
Route: /products/42/reviews
↓
┌─────────────────────────────────────┐
│ Root Component (my-app) │
│ ┌─────────────────────────────────┐ │
│ │ Products (viewport: default) │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ Product 42 (viewport: deflt)│ │ │
│ │ │ ┌─────────────────────────┐ │ │ │
│ │ │ │ Reviews (viewport: def) │ │ │ │
│ │ │ └─────────────────────────┘ │ │ │
│ │ └─────────────────────────────┘ │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────┘
Route Tree:
Root
└─ products
└─ 42 (product-detail)
└─ reviews
SIBLING VIEWPORTS (MULTIPLE VIEWPORTS)
═══════════════════════════════════════════════
<my-app>
<div class="layout">
<au-viewport name="left"></au-viewport>
<au-viewport name="right"></au-viewport>
</div>
</my-app>
Route: products@left+details/42@right
↓
┌───────────────────────────────────────┐
│ Root Component │
│ ┌───────────────┬─────────────────┐ │
│ │ Products │ Product Details │ │
│ │ (left) │ (right) │ │
│ │ │ ID: 42 │ │
│ │ - Item 1 │ │ │
│ │ - Item 2 │ Description... │ │
│ │ - Item 3 │ │ │
│ └───────────────┴─────────────────┘ │
└───────────────────────────────────────┘
Route Configuration:
routes: [
{ path: 'products', component: ProductList },
{ path: 'details/:id', component: ProductDetail }
]
Navigation:
<a href="products@left+details/42@right">Load both</a>
router.load([
{ component: ProductList, viewport: 'left' },
{ component: ProductDetail, params: { id: 42 }, viewport: 'right' }
]);
COMPLEX (NESTED + SIBLING)
═══════════════════════════════════════════════
<my-app>
<au-viewport></au-viewport> ← Root
↓
<dashboard>
<au-viewport name="main"></au-viewport>
<au-viewport name="sidebar"></au-viewport>
↓ ↓
<content> <sidebar-content>
<au-viewport></au-viewport> ← Nested in main
</content>
</dashboard>
</my-app>
Route: /dashboard/content@main+sidebar@sidebar/nested
↓
┌──────────────────────────────────────────┐
│ Root (my-app) │
│ ┌──────────────────────────────────────┐ │
│ │ Dashboard │ │
│ │ ┌─────────────────┬────────────────┐ │ │
│ │ │ Main │ Sidebar │ │ │
│ │ │ ┌─────────────┐ │ │ │ │
│ │ │ │ Nested Comp │ │ Sidebar Content│ │ │
│ │ │ └─────────────┘ │ │ │ │
│ │ └─────────────────┴────────────────┘ │ │
│ └──────────────────────────────────────┘ │
└──────────────────────────────────────────┘Key Concepts:
Default viewport:
<au-viewport></au-viewport>(no name)Named viewport:
<au-viewport name="aside"></au-viewport>Target viewport: Use
@viewportNamein navigationHierarchical: Nested components each have their own viewport
Sibling: Multiple viewports at the same level
6. History Strategy
How router interacts with browser history:
STRATEGY: 'push' (default)
══════════════════════════════════════════
User Journey:
/home → /about → /contact
Browser History Stack:
┌─────────────┐
│ /contact │ ← Current (length: 3)
├─────────────┤
│ /about │ [Back button goes here]
├─────────────┤
│ /home │
└─────────────┘
Code:
router.load('contact', { historyStrategy: 'push' });
✓ Each navigation adds new entry
✓ Back button works as expected
✓ Forward button available after going back
✗ History grows unbounded
STRATEGY: 'replace'
══════════════════════════════════════════
User Journey:
/home → /about → /contact (replace)
Browser History Stack:
┌─────────────┐
│ /contact │ ← Current (length: 2)
├─────────────┤
│ /home │ [Back button goes here]
└─────────────┘
↑
/about was replaced by /contact
Code:
router.load('contact', { historyStrategy: 'replace' });
✓ No history pollution
✓ Good for redirects/corrections
✓ Prevents "back" to intermediate states
✗ Can't navigate back to replaced pages
STRATEGY: 'none'
══════════════════════════════════════════
User Journey:
/home → /about → /contact (none)
Browser History Stack:
┌─────────────┐
│ /home │ ← Current (length: 1)
└─────────────┘
URL bar shows: /contact
But history still has: /home
Code:
router.load('contact', { historyStrategy: 'none' });
✓ No history interaction at all
✓ Good for modal-style navigation
✗ Back button goes to previous app page, not /about
✗ URL and history out of sync
COMPARISON
══════════════════════════════════════════════════════════
Use Case | Strategy
─────────────────────────────────────────────────────────
Normal navigation | 'push'
Login redirect | 'replace'
Fixing invalid route | 'replace'
Multi-step form (same logical page)| 'replace'
Modal / overlay content | 'none'
Wizard steps (want back to work) | 'push'
Correcting user typos in URL | 'replace'
REAL-WORLD EXAMPLE: Login Flow
═══════════════════════════════════
// User tries to access protected route
canLoad() {
if (!isLoggedIn) {
// Redirect to login WITH replace
// So after login, "back" doesn't go to login page
router.load('login', { historyStrategy: 'replace' });
return false;
}
}
// After successful login
login() {
authenticate();
// Navigate to dashboard WITH replace
// So "back" from dashboard doesn't go to login
router.load('dashboard', { historyStrategy: 'replace' });
}
History progression:
1. User at /home
2. Tries /admin → redirected to /login (replace)
History: [/home, /login]
3. After login → /admin (replace)
History: [/home, /admin]
4. Back button → goes to /home (skips /login)
REAL-WORLD EXAMPLE: Wizard
═══════════════════════════════════
// Multi-step form
wizard.nextStep() {
currentStep++;
// Use push so back button works
router.load(`wizard/step${currentStep}`, {
historyStrategy: 'push'
});
}
History: /wizard/step1 → /wizard/step2 → /wizard/step3
Back button goes through steps correctly
REAL-WORLD EXAMPLE: Search Filters
══════════════════════════════════════
// User adjusts filters
applyFilters() {
// Use replace to update URL without history spam
router.load('search', {
queryParams: { ...filters },
historyStrategy: 'replace'
});
}
Without replace:
/search → /search?cat=A → /search?cat=A&sort=price
→ /search?cat=A&sort=price&page=2
→ /search?cat=A&sort=price&page=3
[User hits back 4 times to go back!]
With replace:
/search → /search?cat=A&sort=price&page=3
[User hits back once to go back!]Decision Guide:
push: Normal navigation, want history
replace: Redirects, corrections, interim states
none: Modals, overlays, no history needed
History strategy documentation →
7. Transition Plans
What happens when navigating to the same component with different parameters:
Scenario: Navigate from /users/1 to /users/2
(Same component, different parameter)
TRANSITION PLAN: 'replace' (default)
════════════════════════════════════════
/users/1 (ComponentA, id=1)
↓
router.load('/users/2')
↓
┌──────────────────────────────┐
│ 1. Unload current instance │
│ - unloading() called │
│ - detached() called │
│ - Component destroyed │
└────────────┬─────────────────┘
↓
┌──────────────────────────────┐
│ 2. Create new instance │
│ - New component instance │
│ - canLoad() called │
│ - loading() called │
│ - attached() called │
│ - loaded() called │
└────────────┬─────────────────┘
↓
/users/2 (ComponentA, id=2) ← Different instance
Timeline:
ComponentA(id=1) ComponentA(id=2)
unloading()
detached()
[destroyed]
canLoad()
loading()
attached()
loaded()
✓ Clean slate, no stale state
✓ Simple mental model
✗ Slower (full recreation)
✗ Loses component state
✗ Re-runs constructor, bound, etc.
TRANSITION PLAN: 'invoke-lifecycles'
════════════════════════════════════════
/users/1 (ComponentA, id=1)
↓
router.load('/users/2')
↓
┌──────────────────────────────┐
│ 1. Keep existing instance │
│ - Same component object │
│ - No destruction │
└────────────┬─────────────────┘
↓
┌──────────────────────────────┐
│ 2. Re-invoke hooks │
│ - canLoad() called │
│ - loading() called │
│ - loaded() called │
│ (NO attach/detach) │
└────────────┬─────────────────┘
↓
/users/2 (ComponentA, id=2) ← Same instance!
Timeline:
ComponentA(id=1)
canLoad(id=2)
loading(id=2)
loaded(id=2)
ComponentA(id=2)
✓ Faster (reuses instance)
✓ Can preserve component state
✓ Smoother transitions/animations
✗ Must handle state updates correctly
✗ Potential for stale data bugs
COMPARISON
══════════════════════════════════════════════════════════
Aspect | replace | invoke-lifecycles
────────────────────────────────────────────────────────────────
Instance | New | Reused
Speed | Slower | Faster
State | Fresh | Preserved*
Lifecycle hooks | All | Subset
DOM | Removed/readded | Stays
Use for | Default behavior | Param-only changes
* Preserved state can be a pro or con depending on use case
CONFIGURATION
═══════════════════════════════════════════════════════════
Global configuration:
@route({
transitionPlan: 'invoke-lifecycles', ← All routes
routes: [...]
})
Per-route configuration:
{
path: 'users/:id',
component: UserDetail,
transitionPlan: 'invoke-lifecycles' ← Just this route
}
Per-navigation override:
router.load('users/2', {
transitionPlan: 'invoke-lifecycles' ← Just this navigation
});
REAL-WORLD EXAMPLE: User Profile Tabs
═══════════════════════════════════════════════════════════
Component:
class UserProfile implements IRouteViewModel {
userId: string;
userData: User;
selectedTab = 'overview'; ← Component state
loading(params: Params) {
if (this.userId !== params.id) {
// Different user - fetch new data
this.userId = params.id;
this.userData = await fetchUser(params.id);
}
// Update tab from URL
this.selectedTab = params.tab || 'overview';
}
}
Routes:
{
path: 'users/:id/:tab?',
component: UserProfile,
transitionPlan: 'invoke-lifecycles' ← Preserve state
}
Navigation:
/users/123/overview → /users/123/posts
└─ Same user, keep loaded data, just update tab
/users/123/posts → /users/456/posts
└─ Different user, fetch new data in loading()
WHEN TO USE EACH
═══════════════════════════════════════════════════════════
Use 'replace' when:
✓ You want clean state each time
✓ Component has complex initialization
✓ Different params mean completely different data
✓ You don't trust yourself to handle reuse correctly
Use 'invoke-lifecycles' when:
✓ Only parameters change (same logical entity)
✓ You want to preserve UI state (scroll, selections)
✓ Performance matters (frequent navigation)
✓ You have good loading() logic that handles updates
COMMON PITFALL
═══════════════════════════════════════════════════════════
// ✗ BAD: Doesn't update when params change
class ProductDetail implements IRouteViewModel {
product: Product;
constructor() {
this.product = fetchProduct(params.id); ← params not available!
}
}
// ✓ GOOD: Updates on every navigation
class ProductDetail implements IRouteViewModel {
product: Product;
loading(params: Params) {
this.product = await fetchProduct(params.id); ← Correct!
}
}Rule of Thumb:
Default (
replace): Safe, always worksinvoke-lifecycles: Optimize when parameters drive content, not fundamentally different pages
Transition plans documentation →
8. Route Parameter Flow
How parameters flow from URL to component:
URL: /products/42/reviews?sort=date&page=2#reviews-section
\_______/\__/\______/\__________________/\_____________/
│ │ │ │ │
path param path query fragment
PARSING
═══════════════════════════════════════════════════════════
Router processes URL:
┌────────────────────────────────┐
│ Path segments: [products, 42, │
│ reviews] │
│ Path params: {id: '42'} │
│ Query params: {sort: 'date', │
│ page: '2'} │
│ Fragment: 'reviews-section'│
└────────────────────────────────┘
ROUTE MATCHING
═══════════════════════════════════════════════════════════
Configuration:
{
path: 'products/:id',
component: ProductDetail,
routes: [
{ path: 'reviews', component: Reviews }
]
}
Match result:
┌─────────────────────────────────────────┐
│ Route Tree: │
│ products (:id = '42') │
│ └─ reviews │
│ │
│ Params object: │
│ { id: '42' } │
│ │
│ Query object: │
│ { sort: 'date', page: '2' } │
└─────────────────────────────────────────┘
ACCESS IN COMPONENT
═══════════════════════════════════════════════════════════
Method 1: Lifecycle hooks
──────────────────────────────────────────
class ProductDetail implements IRouteViewModel {
productId: string;
canLoad(params: Params, next: RouteNode) {
// Path parameters
this.productId = params.id; // '42'
// Query parameters
const sort = next.queryParams.get('sort'); // 'date'
const page = next.queryParams.get('page'); // '2'
// Fragment
const fragment = next.fragment; // 'reviews-section'
return true;
}
}
Method 2: ICurrentRoute
──────────────────────────────────────────
import { ICurrentRoute } from '@aurelia/router';
import { resolve } from '@aurelia/kernel';
class ProductDetail {
private readonly currentRoute = resolve(ICurrentRoute);
attached() {
// Current path
console.log(this.currentRoute.path); // 'products/42/reviews'
// Parameters (includes all from parent routes)
const params = this.currentRoute.parameterInformation[0].params;
console.log(params.get('id')); // '42'
// Query string (need to parse)
const url = this.currentRoute.url;
const queryString = url.split('?')[1]; // 'sort=date&page=2'
}
}
Method 3: getRouteParameters (aggregates hierarchy)
────────────────────────────────────────────────────
import { IRouteContext } from '@aurelia/router';
import { resolve } from '@aurelia/kernel';
class NestedComponent {
private readonly context = resolve(IRouteContext);
attached() {
// Get all params from entire route hierarchy
const allParams = this.context.getRouteParameters<{
companyId: string; // From /companies/:companyId
projectId: string; // From /projects/:projectId
userId: string; // From /users/:userId
}>({
includeQueryParams: true // Also include ?foo=bar
});
console.log(allParams.companyId); // Nearest definition wins
}
}
PARAMETER TYPES
═══════════════════════════════════════════════════════════
All parameters are strings!
─────────────────────────────────────────
URL: /products/42?count=10&active=true
params.id // '42' (string, not number!)
params.count // '10' (string, not number!)
params.active // 'true' (string, not boolean!)
Always convert:
const id = Number(params.id);
const count = parseInt(params.count, 10);
const active = params.active === 'true';
PARAMETER BINDING WITH load
═══════════════════════════════════════════════════════════
Template:
<a load="route: products; params.bind: {id: productId}">
View Product
</a>
Component:
productId = 42;
Generated URL:
/products/42
With multiple params:
<a load="route: items;
params.bind: {
id: itemId,
category: itemCategory,
extra: 'value'
}">
View Item
</a>
Route: /items/:id/:category?
Generated: /items/42/electronics?extra=value
│ │ └─ query (not in path)
│ └─ matches :category
└─ matches :id
PROGRAMMATIC WITH OPTIONS
═══════════════════════════════════════════════════════════
router.load('products/42', {
queryParams: {
sort: 'price',
page: 1
},
fragment: 'reviews'
});
Generated URL:
/products/42?sort=price&page=1#reviews
Or with structured instruction:
router.load({
component: 'products',
params: { id: 42 },
children: [
{ component: 'reviews' }
]
}, {
queryParams: { sort: 'date' }
});
Generated URL:
/products/42/reviews?sort=date
PARAMETER CONSTRAINTS
═══════════════════════════════════════════════════════════
Validate during routing:
{
path: 'products/:id{{^\\d+$}}', // Only digits
component: ProductDetail
}
URL: /products/42 ✓ Matches
URL: /products/abc ✗ Doesn't match, goes to fallback
Custom validation in component:
canLoad(params: Params) {
const id = Number(params.id);
if (!Number.isInteger(id) || id <= 0) {
return 'not-found'; // Redirect to 404
}
return true;
}Key Points:
All parameters are strings
Path params come from URL segments
Query params come from
?key=valueAccess via lifecycle hooks or ICurrentRoute
Always validate and convert types
Path parameters → | Query parameters →
Summary
These diagrams cover the core architectural concepts of Aurelia 2's router:
Route Matching - How URLs become components
Navigation - Three ways to navigate and their differences
Lifecycle - Complete hook execution sequence
Hooks - Component vs Router hooks
Viewports - Nested and sibling viewport patterns
History - Push vs Replace vs None strategies
Transitions - Replace vs invoke-lifecycles behavior
Parameters - How data flows from URL to component
For more details, see the complete Router Documentation.
Last updated
Was this helpful?