import { Location } from '@angular/common'; import { Injectable, Optional } from '@angular/core'; import { NavigationExtras, Router, UrlSerializer, UrlTree, NavigationStart } from '@angular/router'; import { AnimationBuilder, NavDirection, RouterDirection } from '@ionic/core'; import { IonRouterOutlet } from '../directives/navigation/ion-router-outlet'; import { Platform } from './platform'; export interface AnimationOptions { animated?: boolean; animation?: AnimationBuilder; animationDirection?: 'forward' | 'back'; } export interface NavigationOptions extends NavigationExtras, AnimationOptions {} @Injectable({ providedIn: 'root', }) export class NavController { private topOutlet?: IonRouterOutlet; private direction: 'forward' | 'back' | 'root' | 'auto' = DEFAULT_DIRECTION; private animated?: NavDirection = DEFAULT_ANIMATED; private animationBuilder?: AnimationBuilder; private guessDirection: RouterDirection = 'forward'; private guessAnimation?: NavDirection; private lastNavId = -1; constructor( platform: Platform, private location: Location, private serializer: UrlSerializer, @Optional() private router?: Router ) { // Subscribe to router events to detect direction if (router) { router.events.subscribe((ev) => { if (ev instanceof NavigationStart) { const id = ev.restoredState ? ev.restoredState.navigationId : ev.id; this.guessDirection = id < this.lastNavId ? 'back' : 'forward'; this.guessAnimation = !ev.restoredState ? this.guessDirection : undefined; this.lastNavId = this.guessDirection === 'forward' ? ev.id : id; } }); } // Subscribe to backButton events platform.backButton.subscribeWithPriority(0, (processNextHandler) => { this.pop(); processNextHandler(); }); } /** * This method uses Angular's [Router](https://angular.io/api/router/Router) under the hood, * it's equivalent to calling `this.router.navigateByUrl()`, but it's explicit about the **direction** of the transition. * * Going **forward** means that a new page is going to be pushed to the stack of the outlet (ion-router-outlet), * and that it will show a "forward" animation by default. * * Navigating forward can also be triggered in a declarative manner by using the `[routerDirection]` directive: * * ```html * Link * ``` */ navigateForward(url: string | UrlTree | any[], options: NavigationOptions = {}): Promise { this.setDirection('forward', options.animated, options.animationDirection, options.animation); return this.navigate(url, options); } /** * This method uses Angular's [Router](https://angular.io/api/router/Router) under the hood, * it's equivalent to calling: * * ```ts * this.navController.setDirection('back'); * this.router.navigateByUrl(path); * ``` * * Going **back** means that all the pages in the stack until the navigated page is found will be popped, * and that it will show a "back" animation by default. * * Navigating back can also be triggered in a declarative manner by using the `[routerDirection]` directive: * * ```html * Link * ``` */ navigateBack(url: string | UrlTree | any[], options: NavigationOptions = {}): Promise { this.setDirection('back', options.animated, options.animationDirection, options.animation); return this.navigate(url, options); } /** * This method uses Angular's [Router](https://angular.io/api/router/Router) under the hood, * it's equivalent to calling: * * ```ts * this.navController.setDirection('root'); * this.router.navigateByUrl(path); * ``` * * Going **root** means that all existing pages in the stack will be removed, * and the navigated page will become the single page in the stack. * * Navigating root can also be triggered in a declarative manner by using the `[routerDirection]` directive: * * ```html * Link * ``` */ navigateRoot(url: string | UrlTree | any[], options: NavigationOptions = {}): Promise { this.setDirection('root', options.animated, options.animationDirection, options.animation); return this.navigate(url, options); } /** * Same as [Location](https://angular.io/api/common/Location)'s back() method. * It will use the standard `window.history.back()` under the hood, but featuring a `back` animation * by default. */ back(options: AnimationOptions = { animated: true, animationDirection: 'back' }): void { this.setDirection('back', options.animated, options.animationDirection, options.animation); return this.location.back(); } /** * This methods goes back in the context of Ionic's stack navigation. * * It recursively finds the top active `ion-router-outlet` and calls `pop()`. * This is the recommended way to go back when you are using `ion-router-outlet`. */ async pop(): Promise { let outlet = this.topOutlet; while (outlet) { if (await outlet.pop()) { break; } else { outlet = outlet.parentOutlet; } } } /** * This methods specifies the direction of the next navigation performed by the Angular router. * * `setDirection()` does not trigger any transition, it just sets some flags to be consumed by `ion-router-outlet`. * * It's recommended to use `navigateForward()`, `navigateBack()` and `navigateRoot()` instead of `setDirection()`. */ setDirection( direction: RouterDirection, animated?: boolean, animationDirection?: 'forward' | 'back', animationBuilder?: AnimationBuilder ): void { this.direction = direction; this.animated = getAnimation(direction, animated, animationDirection); this.animationBuilder = animationBuilder; } /** * @internal */ setTopOutlet(outlet: IonRouterOutlet): void { this.topOutlet = outlet; } /** * @internal */ consumeTransition(): { direction: RouterDirection; animation: NavDirection | undefined; animationBuilder: AnimationBuilder | undefined; } { let direction: RouterDirection = 'root'; let animation: NavDirection | undefined; const animationBuilder = this.animationBuilder; if (this.direction === 'auto') { direction = this.guessDirection; animation = this.guessAnimation; } else { animation = this.animated; direction = this.direction; } this.direction = DEFAULT_DIRECTION; this.animated = DEFAULT_ANIMATED; this.animationBuilder = undefined; return { direction, animation, animationBuilder, }; } private navigate(url: string | UrlTree | any[], options: NavigationOptions) { if (Array.isArray(url)) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return this.router!.navigate(url, options); } else { /** * navigateByUrl ignores any properties that * would change the url, so things like queryParams * would be ignored unless we create a url tree * More Info: https://github.com/angular/angular/issues/18798 */ const urlTree = this.serializer.parse(url.toString()); if (options.queryParams !== undefined) { urlTree.queryParams = { ...options.queryParams }; } if (options.fragment !== undefined) { urlTree.fragment = options.fragment; } /** * `navigateByUrl` will still apply `NavigationExtras` properties * that do not modify the url, such as `replaceUrl` which is why * `options` is passed in here. */ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return this.router!.navigateByUrl(urlTree, options); } } } const getAnimation = ( direction: RouterDirection, animated: boolean | undefined, animationDirection: 'forward' | 'back' | undefined ): NavDirection | undefined => { if (animated === false) { return undefined; } if (animationDirection !== undefined) { return animationDirection; } if (direction === 'forward' || direction === 'back') { return direction; } else if (direction === 'root' && animated === true) { return 'forward'; } return undefined; }; const DEFAULT_DIRECTION = 'auto'; const DEFAULT_ANIMATED = undefined;