Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
27 changes: 27 additions & 0 deletions BREAKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ This is a comprehensive list of the breaking changes introduced in the major ver
- [Input](#version-9x-input)
- [Legacy Picker](#version-9x-legacy-picker)
- [Modal](#version-9x-modal)
- [Nav](#version-9x-nav)
- [Router Outlet](#version-9x-router-outlet)
- [Searchbar](#version-9x-searchbar)
- [Select](#version-9x-select)
Expand Down Expand Up @@ -98,6 +99,32 @@ Sheet modals that relied on the handle being inert should set `handleBehavior="n
<ion-modal handle-behavior="none"></ion-modal>
```

<h4 id="version-9x-nav">Nav</h4>

`ion-nav` no longer integrates with `ion-router`. It is now a standalone imperative stack navigation component, driven only through its own API (`root`, `push`, `pop`, `setRoot`, etc.) and `ion-nav-link`.

The following behaviors have been removed:

- The router no longer discovers or drives an `ion-nav`. Placing an `ion-nav` inside an `ion-router` no longer turns it into a routed outlet.
- Navigating an `ion-nav` (via `push`, `pop`, `ion-nav-link`, or the swipe-to-go-back gesture) no longer updates the URL, and the router's navigation guards no longer run for `ion-nav` transitions.
- The internal `setRouteId()` and `getRouteId()` methods and the `updateURL` nav option have been removed.

Apps that relied on `ion-nav` to update the URL (for example, pushing components and expecting the browser URL to change) should use `ion-router-outlet` for URL-based routing:

```diff
- <ion-router>
- <ion-nav root="page-one"></ion-nav>
- </ion-router>
+ <ion-router-outlet></ion-router-outlet>
```

An `ion-nav` can still be used inside a routed page for local, URL-less stack navigation:

```html
<!-- Inside a routed page, an ion-nav manages a local, URL-less stack -->
<ion-nav root="page-one"></ion-nav>
```

<h4 id="version-9x-router-outlet">Router Outlet</h4>

`ion-router-outlet` now exposes a `swipeGesture` property that controls the swipe-to-go-back gesture per outlet. This property defaults to `true` in `"ios"` mode and `false` in `"md"` mode.
Expand Down
17 changes: 2 additions & 15 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2115,10 +2115,6 @@ export namespace Components {
* @param view The view to get.
*/
"getPrevious": (view?: ViewController) => Promise<ViewController | undefined>;
/**
* Called by <ion-router> to retrieve the current component.
*/
"getRouteId": () => Promise<RouteID | undefined>;
/**
* Inserts a component into the navigation stack at the specified index. This is useful to add a component at any point in the navigation stack.
* @param insertIndex The index to insert the component at in the stack.
Expand Down Expand Up @@ -2194,15 +2190,6 @@ export namespace Components {
* @param done The transition complete function.
*/
"setRoot": <T extends NavComponent>(component: T, componentProps?: ComponentProps<T> | null, opts?: NavOptions | null, done?: TransitionDoneFn) => Promise<boolean>;
/**
* Called by the router to update the view.
* @param id The component tag.
* @param params The component params.
* @param direction A direction hint.
* @param animation an AnimationBuilder.
* @return the status.
*/
"setRouteId": (id: string, params: ComponentProps | undefined, direction: RouterDirection, animation?: AnimationBuilder) => Promise<RouteWrite>;
/**
* If the nav component should allow for swipe-to-go-back.
*/
Expand Down Expand Up @@ -2724,7 +2711,7 @@ export namespace Components {
*/
"beforeLeave"?: NavigationHookCallback;
/**
* Name of the component to load/select in the navigation outlet (`ion-tabs`, `ion-nav`) when the route matches. The value of this property is not always the tagname of the component to load, in `ion-tabs` it actually refers to the name of the `ion-tab` to select.
* Name of the component to load/select in the navigation outlet (`ion-tabs`, `ion-router-outlet`) when the route matches. The value of this property is not always the tagname of the component to load, in `ion-tabs` it actually refers to the name of the `ion-tab` to select.
*/
"component": string;
/**
Expand Down Expand Up @@ -7897,7 +7884,7 @@ declare namespace LocalJSX {
*/
"beforeLeave"?: NavigationHookCallback;
/**
* Name of the component to load/select in the navigation outlet (`ion-tabs`, `ion-nav`) when the route matches. The value of this property is not always the tagname of the component to load, in `ion-tabs` it actually refers to the name of the `ion-tab` to select.
* Name of the component to load/select in the navigation outlet (`ion-tabs`, `ion-router-outlet`) when the route matches. The value of this property is not always the tagname of the component to load, in `ion-tabs` it actually refers to the name of the `ion-tab` to select.
*/
"component": string;
/**
Expand Down
1 change: 0 additions & 1 deletion core/src/components/nav/nav-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ export interface RouterOutletOptions {

export interface NavOptions extends RouterOutletOptions {
progressAnimation?: boolean;
updateURL?: boolean;
delegate?: FrameworkDelegate;
viewIsReady?: (enteringEl: HTMLElement) => Promise<any>;
}
Expand Down
143 changes: 5 additions & 138 deletions core/src/components/nav/nav.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { EventEmitter } from '@stencil/core';
import { Build, Component, Element, Event, Method, Prop, Watch, h } from '@stencil/core';
import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Component, Element, Event, Method, Prop, Watch, h } from '@stencil/core';
import { getTimeGivenProgression } from '@utils/animation/cubic-bezier';
import { assert } from '@utils/helpers';
import { printIonWarning } from '@utils/logging';
Expand All @@ -9,7 +9,6 @@ import { lifecycle, setPageHidden, transition } from '@utils/transition';
import { config } from '../../global/config';
import { getIonMode } from '../../global/ionic-global';
import type { Animation, AnimationBuilder, ComponentProps, FrameworkDelegate, Gesture } from '../../interface';
import type { NavOutlet, RouteID, RouteWrite, RouterDirection } from '../router/utils/interface';

import { LIFECYCLE_DID_LEAVE, LIFECYCLE_WILL_LEAVE, LIFECYCLE_WILL_UNLOAD } from './constants';
import type {
Expand All @@ -21,18 +20,17 @@ import type {
TransitionInstruction,
} from './nav-interface';
import type { ViewController } from './view-controller';
import { VIEW_STATE_ATTACHED, VIEW_STATE_DESTROYED, VIEW_STATE_NEW, convertToViews, matches } from './view-controller';
import { VIEW_STATE_ATTACHED, VIEW_STATE_DESTROYED, VIEW_STATE_NEW, convertToViews } from './view-controller';

@Component({
tag: 'ion-nav',
styleUrl: 'nav.scss',
shadow: true,
})
export class Nav implements NavOutlet {
export class Nav implements ComponentInterface {
private transInstr: TransitionInstruction[] = [];
private sbAni?: Animation;
private gestureOrAnimationInProgress = false;
private useRouter = false;
private isTransitioning = false;
private destroyed = false;
private views: ViewController[] = [];
Expand Down Expand Up @@ -78,8 +76,6 @@ export class Nav implements NavOutlet {

@Watch('root')
rootChanged() {
const isDev = Build.isDev;

if (this.root === undefined) {
return;
}
Expand All @@ -92,13 +88,7 @@ export class Nav implements NavOutlet {
return;
}

if (!this.useRouter) {
if (this.root !== undefined) {
this.setRoot(this.root, this.rootParams);
}
} else if (isDev) {
printIonWarning('[ion-nav] - A root attribute is not supported when using ion-router.', this.el);
}
this.setRoot(this.root, this.rootParams);
}

/**
Expand All @@ -116,8 +106,6 @@ export class Nav implements NavOutlet {
@Event({ bubbles: false }) ionNavDidChange!: EventEmitter<void>;

componentWillLoad() {
this.useRouter = document.querySelector('ion-router') !== null && this.el.closest('[no-router]') === null;

if (this.swipeGesture === undefined) {
const mode = getIonMode(this);
this.swipeGesture = config.getBoolean('swipeBackEnabled', mode === 'ios');
Expand Down Expand Up @@ -352,99 +340,6 @@ export class Nav implements NavOutlet {
);
}

/**
* Called by the router to update the view.
*
* @param id The component tag.
* @param params The component params.
* @param direction A direction hint.
* @param animation an AnimationBuilder.
*
* @return the status.
* @internal
*/
@Method()
setRouteId(
id: string,
params: ComponentProps | undefined,
direction: RouterDirection,
animation?: AnimationBuilder
): Promise<RouteWrite> {
const active = this.getActiveSync();
if (matches(active, id, params)) {
return Promise.resolve({
changed: false,
element: active.element,
});
}

let resolve: (result: RouteWrite) => void;
const promise = new Promise<RouteWrite>((r) => (resolve = r));
let finish: Promise<boolean>;
const commonOpts: NavOptions = {
updateURL: false,
viewIsReady: (enteringEl) => {
let mark: () => void;
const p = new Promise<void>((r) => (mark = r));
resolve({
changed: true,
element: enteringEl,
markVisible: async () => {
mark();
await finish;
},
});
return p;
},
};

if (direction === 'root') {
finish = this.setRoot(id, params, commonOpts);
} else {
// Look for a view matching the target in the view stack.
const viewController = this.views.find((v) => matches(v, id, params));

if (viewController) {
finish = this.popTo(viewController, {
...commonOpts,
direction: 'back',
animationBuilder: animation,
});
} else if (direction === 'forward') {
finish = this.push(id, params, {
...commonOpts,
animationBuilder: animation,
});
} else if (direction === 'back') {
finish = this.setRoot(id, params, {
...commonOpts,
direction: 'back',
animated: true,
animationBuilder: animation,
});
}
}
return promise;
}

/**
* Called by <ion-router> to retrieve the current component.
*
* @internal
*/
@Method()
async getRouteId(): Promise<RouteID | undefined> {
const active = this.getActiveSync();
if (active) {
return {
id: active.element!.tagName,
params: active.params,
element: active.element,
};
}
return undefined;
}

/**
* Get the active view.
*/
Expand Down Expand Up @@ -524,26 +419,6 @@ export class Nav implements NavOutlet {
});
ti.done = done;

/**
* If using router, check to see if navigation hooks
* will allow us to perform this transition. This
* is required in order for hooks to work with
* the ion-back-button or swipe to go back.
*/
if (ti.opts && ti.opts.updateURL !== false && this.useRouter) {
const router = document.querySelector('ion-router');
if (router) {
const canTransition = await router.canTransition();
if (canTransition === false) {
return false;
}
if (typeof canTransition === 'string') {
router.push(canTransition, ti.opts!.direction || 'back');
return false;
}
}
}

// Normalize empty
if (ti.insertViews?.length === 0) {
ti.insertViews = undefined;
Expand Down Expand Up @@ -575,14 +450,6 @@ export class Nav implements NavOutlet {
);
}
ti.resolve!(result.hasCompleted);

if (ti.opts!.updateURL !== false && this.useRouter) {
const router = document.querySelector('ion-router');
if (router) {
const direction = result.direction === 'back' ? 'back' : 'forward';
router.navChanged(direction);
}
}
}

private failed(rejectReason: any, ti: TransitionInstruction) {
Expand Down
36 changes: 10 additions & 26 deletions core/src/components/nav/test/routing/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,21 @@
</head>

<body>
<!--
As of Ionic 9, ion-nav no longer integrates with ion-router. This page
reproduces the pattern from https://github.com/ionic-team/ionic-framework/issues/24641,
where an ion-router and an ion-nav coexist. The ion-nav must manage its own
stack via `root` and ion-nav-link, the URL must never change as the stack
changes, and the page must still finish loading even though the router has no
outlet to drive.
-->
<ion-app>
<ion-router>
<ion-router use-hash="true">
<ion-route url="/" component="page-root"></ion-route>
<ion-route url="/page-one" component="page-one"></ion-route>
<ion-route url="/page-two" component="page-two"></ion-route>
<ion-route url="/page-three" component="page-three"></ion-route>
</ion-router>
<ion-nav></ion-nav>
<ion-nav root="page-root"></ion-nav>
</ion-app>

<script>
Expand Down Expand Up @@ -78,36 +85,13 @@ <h1>Page One</h1>
</ion-header>
<ion-content class="ion-padding">
<h1>Page Two</h1>
<div>
<ion-nav-link router-direction="forward" component="page-three">
<button class="next">Go to Page Three</button>
</ion-nav-link>
</div>
</ion-content>
`;
}
}
class PageThree extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-title>Page Three</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<h1>Page Three</h1>
</ion-content>
`;
}
}
customElements.define('page-root', PageRoot);
customElements.define('page-one', PageOne);
customElements.define('page-two', PageTwo);
customElements.define('page-three', PageThree);
</script>
</body>
</html>
Loading
Loading