Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
3 changes: 2 additions & 1 deletion docs/angular/navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ sidebar_label: Navigation/Routing
---

import useBaseUrl from '@docusaurus/useBaseUrl';
import NavigationPlayground from '@site/static/usage/v9/angular/navigation/index.md';
Comment thread
brandyscarney marked this conversation as resolved.
Outdated

<head>
<title>Angular Navigation: How Routing & Redirects Work in Angular Apps</title>
Expand Down Expand Up @@ -201,7 +202,7 @@ 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.
<NavigationPlayground />
Comment thread
brandyscarney marked this conversation as resolved.
Outdated

## Linear Routing versus Non-Linear Routing

Expand Down
67 changes: 28 additions & 39 deletions docs/react/navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ sidebar_label: Navigation/Routing
---

import useBaseUrl from '@docusaurus/useBaseUrl';
import NavigationPlayground from '@site/static/usage/v9/react/navigation/index.md';
Comment thread
brandyscarney marked this conversation as resolved.
Outdated

<head>
<title>React Navigation: Router Link Redirect to Navigate to Another Page</title>
Expand Down Expand Up @@ -57,20 +58,18 @@ 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.

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.

## IonRouterOutlet
Expand All @@ -90,35 +89,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 @@ -351,12 +342,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 @@ -509,7 +498,7 @@ 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.
<NavigationPlayground />
Comment thread
brandyscarney marked this conversation as resolved.
Outdated

### IonRouterOutlet in a Tabs View

Expand Down
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/angular/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/angular/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,26 @@
```ts
import { Component } from '@angular/core';
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],
Comment thread
brandyscarney marked this conversation as resolved.
Outdated
})
export class DashboardPageComponent {
items = [
{ id: '1', name: 'Item One' },
{ id: '2', name: 'Item Two' },
{ id: '3', name: 'Item Three' },
];
}
```
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/angular/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