-
Notifications
You must be signed in to change notification settings - Fork 3.2k
docs(react,angular): use ionPage prop for nested outlets and add navigation playgrounds #4454
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 11 commits
2b442d1
8fcf1cf
bcf73fe
12981ad
1bd69b1
d0f6f2f
76552ed
d2401a0
5228288
30ac7db
686435b
12e3f60
58873e4
de72886
75c1dc0
91f7811
785d345
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ sidebar_label: Navigation/Routing | |
| --- | ||
|
|
||
| import useBaseUrl from '@docusaurus/useBaseUrl'; | ||
| import NavigationPlayground from '@site/static/usage/v9/react/navigation/index.md'; | ||
|
brandyscarney marked this conversation as resolved.
Outdated
|
||
|
|
||
| <head> | ||
| <title>React Navigation: Router Link Redirect to Navigate to Another Page</title> | ||
|
|
@@ -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> | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand why
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
@@ -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 | ||
|
|
@@ -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> | ||
| ); | ||
| ``` | ||
|
|
||
|
|
@@ -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 /> | ||
|
brandyscarney marked this conversation as resolved.
Outdated
|
||
|
|
||
| ### IonRouterOutlet in a Tabs View | ||
|
|
||
|
|
||
| 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> | ||
| ``` |
| 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 {} | ||
| ``` |
| 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], | ||
|
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> | ||
| ``` |
| 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 {} | ||
| ``` |
Uh oh!
There was an error while loading. Please reload this page.