Routing and navigation

Move between screens with Angular's Router and <router-outlet>. Three things differ from the web: the URL lives in memory, you navigate from tap handlers instead of links, and the views of routes you leave stay alive.

Set up the router

Import provideRouter() from @blotch/angular-lynx instead of @angular/router. ng add @blotch/angular-lynx sets this up for you.

src/app/app.config.ts
import {
  type ApplicationConfig,
  provideZonelessChangeDetection,
} from '@angular/core';
import { provideRenderer, provideRouter } from '@blotch/angular-lynx'; 
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZonelessChangeDetection(),
    provideRenderer(),
    provideRouter(routes),
  ],
};
src/app/app.routes.ts
import type { Routes } from '@angular/router';
import { Home } from './home/home';
import { Settings } from './settings/settings';

export const routes: Routes = [
  { path: '', pathMatch: 'full', redirectTo: 'home' },
  { path: 'home', component: Home },
  { path: 'settings', component: Settings },
];
  • Give the empty path '' a route. The app always starts at /, and there's no URL bar to start anywhere else. Without an '' route, the first screen is blank.
  • provideRouter() takes the usual router features as extra arguments, such as withComponentInputBinding().
src/app/app.ts
import { Component, inject } from '@angular/core';
import { Router, RouterOutlet } from '@angular/router';
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';

@Component({
  selector: 'app-root',
  imports: [LYNX_ELEMENTS, RouterOutlet],
  template: `
    <view class="nav">
      <view (bindtap)="go('home')"><text>Home</text></view>
      <view (bindtap)="go('settings')"><text>Settings</text></view>
    </view>
    <router-outlet />
  `,
})
export class App {
  readonly #router = inject(Router);

  go(path: string): void {
    setTimeout(() => this.#router.navigateByUrl(path)); 
  }
}
  • Use (bindtap), not routerLink. RouterLink listens for click, which Lynx elements never fire. Call Router.navigate() or navigateByUrl() from a tap handler instead.
  • Wrap navigation in setTimeout. Navigating creates and destroys views, which can crash the app if it happens inside the tap handler. See the setTimeout rule.
  • Failed navigations don't throw. navigate() resolves to false, for example for a path with no route. Watch Router.events for NavigationError to find out why.

Go back

Use Location.back() from @angular/common:

src/app/details/details.ts
import { Location } from '@angular/common';
import { Component, inject } from '@angular/core';
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';

@Component({
  selector: 'app-details',
  imports: [LYNX_ELEMENTS],
  template: ` <view (bindtap)="back()"><text>Back</text></view> `,
})
export class Details {
  readonly #location = inject(Location);

  back(): void {
    setTimeout(() => this.#location.back()); 
  }
}
  • Location.back() and Location.forward() move through the router's in-memory history.
  • history.back() does nothing on a device, because there's no browser history.
  • The Android back button isn't connected to the router yet. In an app built with ng build --platform android, pressing it leaves the app instead of going back a screen. See Run on iOS and Android.

Read route parameters

Turn on withComponentInputBinding() to receive route parameters as inputs:

src/app/app.config.ts
import {
  type ApplicationConfig,
  provideZonelessChangeDetection,
} from '@angular/core';
import { withComponentInputBinding } from '@angular/router';
import { provideRenderer, provideRouter } from '@blotch/angular-lynx';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZonelessChangeDetection(),
    provideRenderer(),
    provideRouter(routes, withComponentInputBinding()),
  ],
};
src/app/profile/profile.ts
import { Component, input } from '@angular/core';
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';

@Component({
  selector: 'app-profile',
  imports: [LYNX_ELEMENTS],
  template: `<text>Profile {{ id() }}</text>`,
})
export class Profile {
  readonly id = input.required<string>(); // from the `profile/:id` route
}

Read parameters reactively, through an input or ActivatedRoute.paramMap, rather than once in ngOnInit. /profile/1 and /profile/2 share one component instance (see Good to know).

Lazy-loaded routes

loadComponent works:

src/app/app.routes.ts
import type { Routes } from '@angular/router';

export const routes: Routes = [
  { path: '', pathMatch: 'full', redirectTo: 'home' },
  {
    path: 'home',
    loadComponent: () => import('./home/home').then((m) => m.Home),
  },
];

It doesn't split your code, though. The build keeps every route's code in the bundle, so nothing downloads when the user navigates.

Differences from web Angular

FeatureWeb AngularAngularLynx
URLShown in the address barKept in memory, never shown
Starting URLAny URL the browser opensAlways /
LinksrouterLink(bindtap) with Router.navigate()
Going backBrowser back button, history.back()Location.back()
Navigating from a handlerCall it directlyWrap it in setTimeout
Lazy routesSeparate files, downloaded on demandPart of the bundle
Leaving a routeIts component is destroyedIts view is kept and reused

Good to know

AngularLynx keeps the view of every route you visit, instead of destroying it when you leave. Lynx doesn't free a destroyed view's native elements, so an app that recreated views on every navigation would run out of them (see element pool, and Renderer internals for the details). As a result:

  • ngOnDestroy and DestroyRef.onDestroy don't run when you leave a route, and ngOnInit doesn't run again when you come back.
  • Timers, subscriptions and effects keep running while a route is hidden. Pause them in <router-outlet (detach)="…"> and restart them in (attach).
  • Component state survives, such as signals and form values, until the app closes.