Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/angular/navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,9 @@ To get started with standalone components [visit Angular's official docs](https:

## Live Example

If you would prefer to get hands on with the concepts and code described above, please checkout our [live example](https://stackblitz.com/edit/ionic-angular-routing?file=src/app/app-routing.module.ts) of the topics above on StackBlitz.
import NavigationPlayground from '@site/static/usage/v9/navigation/index.md';

<NavigationPlayground defaultFramework="angular" />

## Linear Routing versus Non-Linear Routing

Expand Down
70 changes: 30 additions & 40 deletions docs/react/navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,21 +57,19 @@ Inside the Dashboard page, we define more routes related to this specific sectio
**DashboardPage.tsx**

```tsx
const DashboardPage: React.FC = () => {
return (
<IonPage>
<IonRouterOutlet>
<Route index element={<UsersListPage />} />
<Route path="users/:id" element={<UserDetailPage />} />
</IonRouterOutlet>
</IonPage>
);
};
const DashboardPage: React.FC = () => (
<IonRouterOutlet ionPage>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why ionPage is now required. We have never recommended adding this as a property and it is not added to any of the playgrounds.

Copy link
Copy Markdown
Member Author

@ShaneK ShaneK Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's never been recommended because the docs have been known to be wrongly recommending wrapping IonRouterOutlet in IonPages for several years and nobody has fixed it (note the age of the issue this resolves).

Using the ionPage prop on the router outlet directly benefits mostly nested routes (it's required by them, technically). When you have an outlet containing routes and one of those routes renders another outlet, like in a tab layout, the inner outlet needs to know about and be able to participate in the outer outlet's page transitions. Setting the prop makes the inner outlet itself the animatable page element, so the outer StackManager can transition it properly.

It's generally best to do this everywhere so you don't have to remember to do it in the special nested routing edge cases, and that will also be the case in RR6.

<Route index element={<UsersListPage />} />
<Route path="users/:id" element={<UserDetailPage />} />
</IonRouterOutlet>
);
```

Since the parent route already matches `/dashboard/*`, the child routes use **relative paths**. The `index` route matches the parent path (`/dashboard`) and `"users/:id"` resolves to `/dashboard/users/:id`. Absolute paths (e.g., `path="/dashboard/users/:id"`) still work if you prefer explicit full paths.

These routes are grouped in an `IonRouterOutlet`.
Note the `ionPage` prop on `IonRouterOutlet`. When a component serves as a nested outlet rendered directly by a `Route` in a parent outlet, the inner `IonRouterOutlet` must include the `ionPage` prop. Without it, router outlets can overlap during navigation and cause broken transitions. Wrapping the outlet in an `IonPage` is not needed and should be avoided in this case.

These routes are grouped in an `IonRouterOutlet`, let's discuss that next.

## Components

Expand All @@ -92,35 +90,27 @@ We can define a fallback route by placing a `Route` component with a `path` of `
**DashboardPage.tsx**

```tsx
const DashboardPage: React.FC = () => {
return (
<IonPage>
<IonRouterOutlet>
<Route index element={<UsersListPage />} />
<Route path="users/:id" element={<UserDetailPage />} />
<Route path="*" element={<Navigate to="/dashboard" replace />} />
</IonRouterOutlet>
</IonPage>
);
};
const DashboardPage: React.FC = () => (
<IonRouterOutlet ionPage>
<Route index element={<UsersListPage />} />
<Route path="users/:id" element={<UserDetailPage />} />
<Route path="*" element={<Navigate to="/dashboard" replace />} />
</IonRouterOutlet>
);
```

Here, we see that in the event a location does not match the first two `Route`s the `IonRouterOutlet` will redirect the Ionic React app to the `/dashboard` path.

You can alternatively supply a component to render instead of providing a redirect.

```tsx
const DashboardPage: React.FC = () => {
return (
<IonPage>
<IonRouterOutlet>
<Route index element={<UsersListPage />} />
<Route path="users/:id" element={<UserDetailPage />} />
<Route path="*" element={<NotFoundPage />} />
</IonRouterOutlet>
</IonPage>
);
};
const DashboardPage: React.FC = () => (
<IonRouterOutlet ionPage>
<Route index element={<UsersListPage />} />
<Route path="users/:id" element={<UserDetailPage />} />
<Route path="*" element={<NotFoundPage />} />
</IonRouterOutlet>
);
```

### IonPage
Expand Down Expand Up @@ -353,12 +343,10 @@ const App: React.FC = () => (
);

const DashboardRouterOutlet: React.FC = () => (
<IonPage>
<IonRouterOutlet>
<Route index element={<DashboardMainPage />} />
<Route path="stats" element={<DashboardStatsPage />} />
</IonRouterOutlet>
</IonPage>
<IonRouterOutlet ionPage>
<Route index element={<DashboardMainPage />} />
<Route path="stats" element={<DashboardStatsPage />} />
</IonRouterOutlet>
);
```

Expand Down Expand Up @@ -511,7 +499,9 @@ The example below shows how the Spotify app reuses the same album component to s

## Live Example

If you would prefer to get hands on with the concepts and code described above, please checkout our [live example](https://stackblitz.com/edit/ionic-react-routing?file=src/index.tsx) of the topics above on StackBlitz.
import NavigationPlayground from '@site/static/usage/v9/navigation/index.md';

<NavigationPlayground defaultFramework="react" />

### IonRouterOutlet in a Tabs View

Expand Down
22 changes: 20 additions & 2 deletions src/components/global/Playground/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ export default function Playground({
showConsole,
includeIonContent = true,
version,
defaultFramework,
}: {
code: { [key in UsageTarget]?: MdxContent | UsageTargetOptions };
title?: string;
Expand All @@ -154,6 +155,11 @@ export default function Playground({
* This will also load assets for StackBlitz from the specified version directory.
*/
version: string;
/**
* The framework to select by default when no user preference is stored.
* If not specified, defaults to Angular when available, then the first available framework.
*/
defaultFramework?: UsageTarget;
}) {
if (!code || Object.keys(code).length === 0) {
console.warn('No code usage examples provided for this Playground example.');
Expand Down Expand Up @@ -207,6 +213,13 @@ export default function Playground({
};

const getDefaultUsageTarget = () => {
/**
* If a default framework was specified and code exists for it, use that.
*/
if (defaultFramework && code[defaultFramework] !== undefined) {
return defaultFramework;
}

/**
* If there is a saved target from previously clicking the
* framework buttons, and there is code for it, use that.
Expand Down Expand Up @@ -431,10 +444,15 @@ export default function Playground({

/**
* Load the stored mode and/or usage target, if present
* from previously being toggled.
* from previously being toggled. Skip the usage target
* reset when defaultFramework is set, since the initial
* value is already correct and user tab clicks should
* be preserved.
*/
setIonicMode(getDefaultMode());
setUsageTarget(getDefaultUsageTarget());
if (!defaultFramework) {
setUsageTarget(getDefaultUsageTarget());
}

/**
* If the iframes weren't already loaded, load them now.
Expand Down
5 changes: 5 additions & 0 deletions static/usage/v9/navigation/angular/app_component_html.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
```html
<ion-app>
<ion-router-outlet></ion-router-outlet>
</ion-app>
```
11 changes: 11 additions & 0 deletions static/usage/v9/navigation/angular/app_component_ts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
```ts
import { Component } from '@angular/core';
import { IonApp, IonRouterOutlet } from '@ionic/angular/standalone';

@Component({
selector: 'app-root',
templateUrl: 'app.component.html',
imports: [IonApp, IonRouterOutlet],
})
export class AppComponent {}
```
35 changes: 35 additions & 0 deletions static/usage/v9/navigation/angular/app_routes_ts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
```ts
import { Routes } from '@angular/router';
import { ExampleComponent } from './example.component';

export const routes: Routes = [
{
path: 'example',
component: ExampleComponent,
children: [
{
path: 'dashboard',
loadComponent: () => import('./dashboard/dashboard-page.component').then((m) => m.DashboardPageComponent),
},
{
path: 'dashboard/:id',
loadComponent: () => import('./item-detail/item-detail-page.component').then((m) => m.ItemDetailPageComponent),
},
{
path: 'settings',
loadComponent: () => import('./settings/settings-page.component').then((m) => m.SettingsPageComponent),
},
{
path: '',
redirectTo: '/example/dashboard',
pathMatch: 'full',
},
],
},
{
path: '',
redirectTo: '/example/dashboard',
pathMatch: 'full',
},
];
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
```html
<ion-header>
<ion-toolbar>
<ion-title>Dashboard</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
@for (item of items; track item.id) {
<ion-item [routerLink]="['/example/dashboard', item.id]">
<ion-label>{{ item.name }}</ion-label>
</ion-item>
}
</ion-list>
</ion-content>
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
```ts
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';
import {
IonContent,
IonHeader,
IonItem,
IonLabel,
IonList,
IonTitle,
IonToolbar,
IonRouterLink,
} from '@ionic/angular/standalone';

@Component({
selector: 'app-dashboard-page',
templateUrl: 'dashboard-page.component.html',
imports: [IonContent, IonHeader, IonItem, IonLabel, IonList, IonTitle, IonToolbar, IonRouterLink, RouterLink],
})
export class DashboardPageComponent {
items = [
{ id: '1', name: 'Item One' },
{ id: '2', name: 'Item Two' },
{ id: '3', name: 'Item Three' },
];
}
```
14 changes: 14 additions & 0 deletions static/usage/v9/navigation/angular/example_component_html.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
```html
<ion-tabs>
<ion-tab-bar slot="bottom">
<ion-tab-button tab="dashboard" href="/example/dashboard">
<ion-icon name="grid-outline"></ion-icon>
<ion-label>Dashboard</ion-label>
</ion-tab-button>
<ion-tab-button tab="settings" href="/example/settings">
<ion-icon name="settings-outline"></ion-icon>
<ion-label>Settings</ion-label>
</ion-tab-button>
</ion-tab-bar>
</ion-tabs>
```
17 changes: 17 additions & 0 deletions static/usage/v9/navigation/angular/example_component_ts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
```ts
import { Component } from '@angular/core';
import { IonIcon, IonLabel, IonTabBar, IonTabButton, IonTabs } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons';
import { gridOutline, settingsOutline } from 'ionicons/icons';

@Component({
selector: 'app-example',
templateUrl: 'example.component.html',
imports: [IonIcon, IonLabel, IonTabBar, IonTabButton, IonTabs],
})
export class ExampleComponent {
constructor() {
addIcons({ gridOutline, settingsOutline });
}
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
```html
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button defaultHref="/example/dashboard"></ion-back-button>
</ion-buttons>
<ion-title>Item {{ id }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">You navigated to item {{ id }}.</ion-content>
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
```ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { IonBackButton, IonButtons, IonContent, IonHeader, IonTitle, IonToolbar } from '@ionic/angular/standalone';

@Component({
selector: 'app-item-detail-page',
templateUrl: 'item-detail-page.component.html',
imports: [IonBackButton, IonButtons, IonContent, IonHeader, IonTitle, IonToolbar],
})
export class ItemDetailPageComponent implements OnInit {
id = '';

constructor(private route: ActivatedRoute) {}

ngOnInit() {
this.id = this.route.snapshot.paramMap.get('id') ?? '';
}
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
```html
<ion-header>
<ion-toolbar>
<ion-title>Settings</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">Settings content</ion-content>
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
```ts
import { Component } from '@angular/core';
import { IonContent, IonHeader, IonTitle, IonToolbar } from '@ionic/angular/standalone';

@Component({
selector: 'app-settings-page',
templateUrl: 'settings-page.component.html',
imports: [IonContent, IonHeader, IonTitle, IonToolbar],
})
export class SettingsPageComponent {}
```
Loading
Loading