Skip to content

Commit fbf2799

Browse files
docs(react,angular): use ionPage prop for nested outlets and add navigation playgrounds (#4454)
* docs(react-router): working on v9 migration docs for react router * docs(react-router): update navigation guide and expand v5-to-v6 migration guide * chore(lint): running lint * fix(build): fixing build error * docs(react-router): fixing several issues * docs(react-router): converting playground example to rr6 * docs(react-router): updating dependencies, reverting phrasing change * docs(react): fix nested IonRouterOutlet examples and replace live example * fix(docs): use playground component for angular navigation * chore(lint): running lint * fix(angular): add missing RouterLink import to dashboard component * refactor: consolidate angular/react navigation playgrounds into single directory * feat(playground): add default framework setting, migration angular/react navigation live example to use one playground * fix(playground): allow playground swapping with default framework set * docs(navigation): combine playground code to one index file --------- Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
1 parent fa4901b commit fbf2799

20 files changed

+486
-43
lines changed

docs/angular/navigation.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,9 @@ To get started with standalone components [visit Angular's official docs](https:
201201

202202
## Live Example
203203

204-
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.
204+
import NavigationPlayground from '@site/static/usage/v9/navigation/index.md';
205+
206+
<NavigationPlayground defaultFramework="angular" />
205207

206208
## Linear Routing versus Non-Linear Routing
207209

docs/react/navigation.md

Lines changed: 30 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -57,21 +57,19 @@ Inside the Dashboard page, we define more routes related to this specific sectio
5757
**DashboardPage.tsx**
5858

5959
```tsx
60-
const DashboardPage: React.FC = () => {
61-
return (
62-
<IonPage>
63-
<IonRouterOutlet>
64-
<Route index element={<UsersListPage />} />
65-
<Route path="users/:id" element={<UserDetailPage />} />
66-
</IonRouterOutlet>
67-
</IonPage>
68-
);
69-
};
60+
const DashboardPage: React.FC = () => (
61+
<IonRouterOutlet ionPage>
62+
<Route index element={<UsersListPage />} />
63+
<Route path="users/:id" element={<UserDetailPage />} />
64+
</IonRouterOutlet>
65+
);
7066
```
7167

7268
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.
7369

74-
These routes are grouped in an `IonRouterOutlet`.
70+
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.
71+
72+
These routes are grouped in an `IonRouterOutlet`, let's discuss that next.
7573

7674
## Components
7775

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

9492
```tsx
95-
const DashboardPage: React.FC = () => {
96-
return (
97-
<IonPage>
98-
<IonRouterOutlet>
99-
<Route index element={<UsersListPage />} />
100-
<Route path="users/:id" element={<UserDetailPage />} />
101-
<Route path="*" element={<Navigate to="/dashboard" replace />} />
102-
</IonRouterOutlet>
103-
</IonPage>
104-
);
105-
};
93+
const DashboardPage: React.FC = () => (
94+
<IonRouterOutlet ionPage>
95+
<Route index element={<UsersListPage />} />
96+
<Route path="users/:id" element={<UserDetailPage />} />
97+
<Route path="*" element={<Navigate to="/dashboard" replace />} />
98+
</IonRouterOutlet>
99+
);
106100
```
107101

108102
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.
109103

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

112106
```tsx
113-
const DashboardPage: React.FC = () => {
114-
return (
115-
<IonPage>
116-
<IonRouterOutlet>
117-
<Route index element={<UsersListPage />} />
118-
<Route path="users/:id" element={<UserDetailPage />} />
119-
<Route path="*" element={<NotFoundPage />} />
120-
</IonRouterOutlet>
121-
</IonPage>
122-
);
123-
};
107+
const DashboardPage: React.FC = () => (
108+
<IonRouterOutlet ionPage>
109+
<Route index element={<UsersListPage />} />
110+
<Route path="users/:id" element={<UserDetailPage />} />
111+
<Route path="*" element={<NotFoundPage />} />
112+
</IonRouterOutlet>
113+
);
124114
```
125115

126116
### IonPage
@@ -353,12 +343,10 @@ const App: React.FC = () => (
353343
);
354344

355345
const DashboardRouterOutlet: React.FC = () => (
356-
<IonPage>
357-
<IonRouterOutlet>
358-
<Route index element={<DashboardMainPage />} />
359-
<Route path="stats" element={<DashboardStatsPage />} />
360-
</IonRouterOutlet>
361-
</IonPage>
346+
<IonRouterOutlet ionPage>
347+
<Route index element={<DashboardMainPage />} />
348+
<Route path="stats" element={<DashboardStatsPage />} />
349+
</IonRouterOutlet>
362350
);
363351
```
364352

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

512500
## Live Example
513501

514-
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.
502+
import NavigationPlayground from '@site/static/usage/v9/navigation/index.md';
503+
504+
<NavigationPlayground defaultFramework="react" />
515505

516506
### IonRouterOutlet in a Tabs View
517507

src/components/global/Playground/index.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ export default function Playground({
134134
showConsole,
135135
includeIonContent = true,
136136
version,
137+
defaultFramework,
137138
}: {
138139
code: { [key in UsageTarget]?: MdxContent | UsageTargetOptions };
139140
title?: string;
@@ -154,6 +155,11 @@ export default function Playground({
154155
* This will also load assets for StackBlitz from the specified version directory.
155156
*/
156157
version: string;
158+
/**
159+
* The framework to select by default when no user preference is stored.
160+
* If not specified, defaults to Angular when available, then the first available framework.
161+
*/
162+
defaultFramework?: UsageTarget;
157163
}) {
158164
if (!code || Object.keys(code).length === 0) {
159165
console.warn('No code usage examples provided for this Playground example.');
@@ -207,6 +213,13 @@ export default function Playground({
207213
};
208214

209215
const getDefaultUsageTarget = () => {
216+
/**
217+
* If a default framework was specified and code exists for it, use that.
218+
*/
219+
if (defaultFramework && code[defaultFramework] !== undefined) {
220+
return defaultFramework;
221+
}
222+
210223
/**
211224
* If there is a saved target from previously clicking the
212225
* framework buttons, and there is code for it, use that.
@@ -431,10 +444,15 @@ export default function Playground({
431444

432445
/**
433446
* Load the stored mode and/or usage target, if present
434-
* from previously being toggled.
447+
* from previously being toggled. Skip the usage target
448+
* reset when defaultFramework is set, since the initial
449+
* value is already correct and user tab clicks should
450+
* be preserved.
435451
*/
436452
setIonicMode(getDefaultMode());
437-
setUsageTarget(getDefaultUsageTarget());
453+
if (!defaultFramework) {
454+
setUsageTarget(getDefaultUsageTarget());
455+
}
438456

439457
/**
440458
* If the iframes weren't already loaded, load them now.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
```html
2+
<ion-app>
3+
<ion-router-outlet></ion-router-outlet>
4+
</ion-app>
5+
```
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
```ts
2+
import { Component } from '@angular/core';
3+
import { IonApp, IonRouterOutlet } from '@ionic/angular/standalone';
4+
5+
@Component({
6+
selector: 'app-root',
7+
templateUrl: 'app.component.html',
8+
imports: [IonApp, IonRouterOutlet],
9+
})
10+
export class AppComponent {}
11+
```
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
```ts
2+
import { Routes } from '@angular/router';
3+
import { ExampleComponent } from './example.component';
4+
5+
export const routes: Routes = [
6+
{
7+
path: 'example',
8+
component: ExampleComponent,
9+
children: [
10+
{
11+
path: 'dashboard',
12+
loadComponent: () => import('./dashboard/dashboard-page.component').then((m) => m.DashboardPageComponent),
13+
},
14+
{
15+
path: 'dashboard/:id',
16+
loadComponent: () => import('./item-detail/item-detail-page.component').then((m) => m.ItemDetailPageComponent),
17+
},
18+
{
19+
path: 'settings',
20+
loadComponent: () => import('./settings/settings-page.component').then((m) => m.SettingsPageComponent),
21+
},
22+
{
23+
path: '',
24+
redirectTo: '/example/dashboard',
25+
pathMatch: 'full',
26+
},
27+
],
28+
},
29+
{
30+
path: '',
31+
redirectTo: '/example/dashboard',
32+
pathMatch: 'full',
33+
},
34+
];
35+
```
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
```html
2+
<ion-header>
3+
<ion-toolbar>
4+
<ion-title>Dashboard</ion-title>
5+
</ion-toolbar>
6+
</ion-header>
7+
<ion-content>
8+
<ion-list>
9+
@for (item of items; track item.id) {
10+
<ion-item [routerLink]="['/example/dashboard', item.id]">
11+
<ion-label>{{ item.name }}</ion-label>
12+
</ion-item>
13+
}
14+
</ion-list>
15+
</ion-content>
16+
```
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
```ts
2+
import { Component } from '@angular/core';
3+
import { RouterLink } from '@angular/router';
4+
import {
5+
IonContent,
6+
IonHeader,
7+
IonItem,
8+
IonLabel,
9+
IonList,
10+
IonTitle,
11+
IonToolbar,
12+
IonRouterLink,
13+
} from '@ionic/angular/standalone';
14+
15+
@Component({
16+
selector: 'app-dashboard-page',
17+
templateUrl: 'dashboard-page.component.html',
18+
imports: [IonContent, IonHeader, IonItem, IonLabel, IonList, IonTitle, IonToolbar, IonRouterLink, RouterLink],
19+
})
20+
export class DashboardPageComponent {
21+
items = [
22+
{ id: '1', name: 'Item One' },
23+
{ id: '2', name: 'Item Two' },
24+
{ id: '3', name: 'Item Three' },
25+
];
26+
}
27+
```
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
```html
2+
<ion-tabs>
3+
<ion-tab-bar slot="bottom">
4+
<ion-tab-button tab="dashboard" href="/example/dashboard">
5+
<ion-icon name="grid-outline"></ion-icon>
6+
<ion-label>Dashboard</ion-label>
7+
</ion-tab-button>
8+
<ion-tab-button tab="settings" href="/example/settings">
9+
<ion-icon name="settings-outline"></ion-icon>
10+
<ion-label>Settings</ion-label>
11+
</ion-tab-button>
12+
</ion-tab-bar>
13+
</ion-tabs>
14+
```
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
```ts
2+
import { Component } from '@angular/core';
3+
import { IonIcon, IonLabel, IonTabBar, IonTabButton, IonTabs } from '@ionic/angular/standalone';
4+
import { addIcons } from 'ionicons';
5+
import { gridOutline, settingsOutline } from 'ionicons/icons';
6+
7+
@Component({
8+
selector: 'app-example',
9+
templateUrl: 'example.component.html',
10+
imports: [IonIcon, IonLabel, IonTabBar, IonTabButton, IonTabs],
11+
})
12+
export class ExampleComponent {
13+
constructor() {
14+
addIcons({ gridOutline, settingsOutline });
15+
}
16+
}
17+
```

0 commit comments

Comments
 (0)