diff --git a/docs/angular/navigation.md b/docs/angular/navigation.md
index 85ef25c63ce..0354c7910d9 100644
--- a/docs/angular/navigation.md
+++ b/docs/angular/navigation.md
@@ -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';
+
+
## Linear Routing versus Non-Linear Routing
diff --git a/docs/react/navigation.md b/docs/react/navigation.md
index af1270a7298..0941f148f5e 100644
--- a/docs/react/navigation.md
+++ b/docs/react/navigation.md
@@ -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 (
-
-
- } />
- } />
-
-
- );
-};
+const DashboardPage: React.FC = () => (
+
+ } />
+ } />
+
+);
```
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
@@ -92,17 +90,13 @@ We can define a fallback route by placing a `Route` component with a `path` of `
**DashboardPage.tsx**
```tsx
-const DashboardPage: React.FC = () => {
- return (
-
-
- } />
- } />
- } />
-
-
- );
-};
+const DashboardPage: React.FC = () => (
+
+ } />
+ } />
+ } />
+
+);
```
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.
@@ -110,17 +104,13 @@ Here, we see that in the event a location does not match the first two `Route`s
You can alternatively supply a component to render instead of providing a redirect.
```tsx
-const DashboardPage: React.FC = () => {
- return (
-
-
- } />
- } />
- } />
-
-
- );
-};
+const DashboardPage: React.FC = () => (
+
+ } />
+ } />
+ } />
+
+);
```
### IonPage
@@ -353,12 +343,10 @@ const App: React.FC = () => (
);
const DashboardRouterOutlet: React.FC = () => (
-
-
- } />
- } />
-
-
+
+ } />
+ } />
+
);
```
@@ -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';
+
+
### IonRouterOutlet in a Tabs View
diff --git a/src/components/global/Playground/index.tsx b/src/components/global/Playground/index.tsx
index 3489ae7840b..69c1bd2b00e 100644
--- a/src/components/global/Playground/index.tsx
+++ b/src/components/global/Playground/index.tsx
@@ -134,6 +134,7 @@ export default function Playground({
showConsole,
includeIonContent = true,
version,
+ defaultFramework,
}: {
code: { [key in UsageTarget]?: MdxContent | UsageTargetOptions };
title?: string;
@@ -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.');
@@ -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.
@@ -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.
diff --git a/static/usage/v9/navigation/angular/app_component_html.md b/static/usage/v9/navigation/angular/app_component_html.md
new file mode 100644
index 00000000000..3b819dc640d
--- /dev/null
+++ b/static/usage/v9/navigation/angular/app_component_html.md
@@ -0,0 +1,5 @@
+```html
+
+
+
+```
diff --git a/static/usage/v9/navigation/angular/app_component_ts.md b/static/usage/v9/navigation/angular/app_component_ts.md
new file mode 100644
index 00000000000..bec445dc609
--- /dev/null
+++ b/static/usage/v9/navigation/angular/app_component_ts.md
@@ -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 {}
+```
diff --git a/static/usage/v9/navigation/angular/app_routes_ts.md b/static/usage/v9/navigation/angular/app_routes_ts.md
new file mode 100644
index 00000000000..79e8250c89d
--- /dev/null
+++ b/static/usage/v9/navigation/angular/app_routes_ts.md
@@ -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',
+ },
+];
+```
diff --git a/static/usage/v9/navigation/angular/dashboard_page_component_html.md b/static/usage/v9/navigation/angular/dashboard_page_component_html.md
new file mode 100644
index 00000000000..e20f2b19520
--- /dev/null
+++ b/static/usage/v9/navigation/angular/dashboard_page_component_html.md
@@ -0,0 +1,16 @@
+```html
+
+
+ Dashboard
+
+
+
+
+ @for (item of items; track item.id) {
+
+ {{ item.name }}
+
+ }
+
+
+```
diff --git a/static/usage/v9/navigation/angular/dashboard_page_component_ts.md b/static/usage/v9/navigation/angular/dashboard_page_component_ts.md
new file mode 100644
index 00000000000..502ca2fcc46
--- /dev/null
+++ b/static/usage/v9/navigation/angular/dashboard_page_component_ts.md
@@ -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' },
+ ];
+}
+```
diff --git a/static/usage/v9/navigation/angular/example_component_html.md b/static/usage/v9/navigation/angular/example_component_html.md
new file mode 100644
index 00000000000..dd7a83446e9
--- /dev/null
+++ b/static/usage/v9/navigation/angular/example_component_html.md
@@ -0,0 +1,14 @@
+```html
+
+
+
+
+ Dashboard
+
+
+
+ Settings
+
+
+
+```
diff --git a/static/usage/v9/navigation/angular/example_component_ts.md b/static/usage/v9/navigation/angular/example_component_ts.md
new file mode 100644
index 00000000000..025465afa1b
--- /dev/null
+++ b/static/usage/v9/navigation/angular/example_component_ts.md
@@ -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 });
+ }
+}
+```
diff --git a/static/usage/v9/navigation/angular/item_detail_page_component_html.md b/static/usage/v9/navigation/angular/item_detail_page_component_html.md
new file mode 100644
index 00000000000..581d088a361
--- /dev/null
+++ b/static/usage/v9/navigation/angular/item_detail_page_component_html.md
@@ -0,0 +1,11 @@
+```html
+
+
+
+
+
+ Item {{ id }}
+
+
+You navigated to item {{ id }}.
+```
diff --git a/static/usage/v9/navigation/angular/item_detail_page_component_ts.md b/static/usage/v9/navigation/angular/item_detail_page_component_ts.md
new file mode 100644
index 00000000000..6cba64ab184
--- /dev/null
+++ b/static/usage/v9/navigation/angular/item_detail_page_component_ts.md
@@ -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') ?? '';
+ }
+}
+```
diff --git a/static/usage/v9/navigation/angular/settings_page_component_html.md b/static/usage/v9/navigation/angular/settings_page_component_html.md
new file mode 100644
index 00000000000..bad2d5268d4
--- /dev/null
+++ b/static/usage/v9/navigation/angular/settings_page_component_html.md
@@ -0,0 +1,8 @@
+```html
+
+
+ Settings
+
+
+Settings content
+```
diff --git a/static/usage/v9/navigation/angular/settings_page_component_ts.md b/static/usage/v9/navigation/angular/settings_page_component_ts.md
new file mode 100644
index 00000000000..6ca4933f477
--- /dev/null
+++ b/static/usage/v9/navigation/angular/settings_page_component_ts.md
@@ -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 {}
+```
diff --git a/static/usage/v9/navigation/demo.html b/static/usage/v9/navigation/demo.html
new file mode 100644
index 00000000000..0388e0788dd
--- /dev/null
+++ b/static/usage/v9/navigation/demo.html
@@ -0,0 +1,95 @@
+
+
+
+
+
+ Navigation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Dashboard
+
+
+
+
+
+ Item One
+
+
+ Item Two
+
+
+ Item Three
+
+
+
+
+
+
+
+
+
+
+ Settings
+
+
+ Settings content
+
+
+
+
+
+ Dashboard
+
+
+
+ Settings
+
+
+
+
+
+
+
+
diff --git a/static/usage/v9/navigation/index.md b/static/usage/v9/navigation/index.md
new file mode 100644
index 00000000000..1495e9daf1a
--- /dev/null
+++ b/static/usage/v9/navigation/index.md
@@ -0,0 +1,55 @@
+import Playground from '@site/src/components/global/Playground';
+
+import angular_app_routes_ts from './angular/app_routes_ts.md';
+import angular_app_component_html from './angular/app_component_html.md';
+import angular_app_component_ts from './angular/app_component_ts.md';
+import angular_example_component_html from './angular/example_component_html.md';
+import angular_example_component_ts from './angular/example_component_ts.md';
+import angular_dashboard_page_component_html from './angular/dashboard_page_component_html.md';
+import angular_dashboard_page_component_ts from './angular/dashboard_page_component_ts.md';
+import angular_item_detail_page_component_html from './angular/item_detail_page_component_html.md';
+import angular_item_detail_page_component_ts from './angular/item_detail_page_component_ts.md';
+import angular_settings_page_component_html from './angular/settings_page_component_html.md';
+import angular_settings_page_component_ts from './angular/settings_page_component_ts.md';
+
+import react_main_tsx from './react/main_tsx.md';
+import react_dashboard_page_tsx from './react/dashboard_page_tsx.md';
+import react_item_detail_page_tsx from './react/item_detail_page_tsx.md';
+import react_settings_page_tsx from './react/settings_page_tsx.md';
+
+export default function NavigationPlayground({ defaultFramework = 'angular' }) {
+ return (
+
+ );
+}
diff --git a/static/usage/v9/navigation/react/dashboard_page_tsx.md b/static/usage/v9/navigation/react/dashboard_page_tsx.md
new file mode 100644
index 00000000000..6f84af69248
--- /dev/null
+++ b/static/usage/v9/navigation/react/dashboard_page_tsx.md
@@ -0,0 +1,31 @@
+```tsx
+import React from 'react';
+import { IonContent, IonHeader, IonItem, IonLabel, IonList, IonPage, IonTitle, IonToolbar } from '@ionic/react';
+
+const items = [
+ { id: '1', name: 'Item One' },
+ { id: '2', name: 'Item Two' },
+ { id: '3', name: 'Item Three' },
+];
+
+const DashboardPage: React.FC = () => (
+
+
+
+ Dashboard
+
+
+
+
+ {items.map((item) => (
+
+ {item.name}
+
+ ))}
+
+
+
+);
+
+export default DashboardPage;
+```
diff --git a/static/usage/v9/navigation/react/item_detail_page_tsx.md b/static/usage/v9/navigation/react/item_detail_page_tsx.md
new file mode 100644
index 00000000000..7918ed60813
--- /dev/null
+++ b/static/usage/v9/navigation/react/item_detail_page_tsx.md
@@ -0,0 +1,25 @@
+```tsx
+import React from 'react';
+import { IonBackButton, IonButtons, IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react';
+import { useParams } from 'react-router-dom';
+
+const ItemDetailPage: React.FC = () => {
+ const { id } = useParams<{ id: string }>();
+
+ return (
+
+
+
+
+
+
+ Item {id}
+
+
+ You navigated to item {id}.
+
+ );
+};
+
+export default ItemDetailPage;
+```
diff --git a/static/usage/v9/navigation/react/main_tsx.md b/static/usage/v9/navigation/react/main_tsx.md
new file mode 100644
index 00000000000..143a2f02021
--- /dev/null
+++ b/static/usage/v9/navigation/react/main_tsx.md
@@ -0,0 +1,35 @@
+```tsx
+import React from 'react';
+import { IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from '@ionic/react';
+import { IonReactRouter } from '@ionic/react-router';
+import { Route, Navigate } from 'react-router-dom';
+import { gridOutline, settingsOutline } from 'ionicons/icons';
+import DashboardPage from './pages/DashboardPage';
+import ItemDetailPage from './pages/ItemDetailPage';
+import SettingsPage from './pages/SettingsPage';
+
+const Example: React.FC = () => (
+
+
+
+ } />
+ } />
+ } />
+ } />
+
+
+
+
+ Dashboard
+
+
+
+ Settings
+
+
+
+
+);
+
+export default Example;
+```
diff --git a/static/usage/v9/navigation/react/settings_page_tsx.md b/static/usage/v9/navigation/react/settings_page_tsx.md
new file mode 100644
index 00000000000..5bdcec02a51
--- /dev/null
+++ b/static/usage/v9/navigation/react/settings_page_tsx.md
@@ -0,0 +1,17 @@
+```tsx
+import React from 'react';
+import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react';
+
+const SettingsPage: React.FC = () => (
+
+
+
+ Settings
+
+
+ Settings content
+
+);
+
+export default SettingsPage;
+```