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; +```