From 92cedd3c67264b5cc6f01d0711e2ede957e0b89c Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Tue, 1 Aug 2023 15:08:01 -0400 Subject: [PATCH 01/23] refactor(angular): move nav controller to common --- packages/angular/common/src/index.ts | 1 + .../common/src/providers/nav-controller.ts | 261 ++++++++++++++++++ .../directives/navigation/ion-back-button.ts | 4 +- .../navigation/ion-router-outlet.ts | 3 +- .../src/directives/navigation/ion-tabs.ts | 2 +- .../navigation/router-link-delegate.ts | 2 +- .../directives/navigation/stack-controller.ts | 2 +- packages/angular/src/index.ts | 2 +- 8 files changed, 268 insertions(+), 9 deletions(-) create mode 100644 packages/angular/common/src/providers/nav-controller.ts diff --git a/packages/angular/common/src/index.ts b/packages/angular/common/src/index.ts index 9ed4b009796..7f77bc878dd 100644 --- a/packages/angular/common/src/index.ts +++ b/packages/angular/common/src/index.ts @@ -10,6 +10,7 @@ export { ToastController } from './providers/toast-controller'; export { AnimationController } from './providers/animation-controller'; export { GestureController } from './providers/gesture-controller'; export { DomController } from './providers/dom-controller'; +export { NavController } from './providers/nav-controller'; export { Config, ConfigToken } from './providers/config'; export { Platform } from './providers/platform'; diff --git a/packages/angular/common/src/providers/nav-controller.ts b/packages/angular/common/src/providers/nav-controller.ts new file mode 100644 index 00000000000..c8b530274b1 --- /dev/null +++ b/packages/angular/common/src/providers/nav-controller.ts @@ -0,0 +1,261 @@ +import { Location } from '@angular/common'; +import { Injectable, Optional } from '@angular/core'; +import { NavigationExtras, Router, UrlSerializer, UrlTree, NavigationStart } from '@angular/router'; +import { Platform } from './platform'; +import { AnimationBuilder, NavDirection, RouterDirection } from '@ionic/core'; + +// LIAM TODO +//import { IonRouterOutlet } from '../directives/navigation/ion-router-outlet'; + +export interface AnimationOptions { + animated?: boolean; + animation?: AnimationBuilder; + animationDirection?: 'forward' | 'back'; +} + +export interface NavigationOptions extends NavigationExtras, AnimationOptions {} + +@Injectable({ + providedIn: 'root', +}) +export class NavController { + // LIAM TODO + private topOutlet?: any; + 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`. + * + * Resolves to `true` if it was able to pop. + */ + async pop(): Promise { + let outlet = this.topOutlet; + + while (outlet) { + if (await outlet.pop()) { + return true; + } else { + outlet = outlet.parentOutlet; + } + } + + return false; + } + + /** + * 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 + */ + + // LIAM TODO + setTopOutlet(outlet: any): 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; diff --git a/packages/angular/src/directives/navigation/ion-back-button.ts b/packages/angular/src/directives/navigation/ion-back-button.ts index 210096f598c..cefdac067a8 100644 --- a/packages/angular/src/directives/navigation/ion-back-button.ts +++ b/packages/angular/src/directives/navigation/ion-back-button.ts @@ -1,9 +1,7 @@ import { Directive, HostListener, Input, Optional } from '@angular/core'; -import { Config } from '@ionic/angular/common'; +import { Config, NavController } from '@ionic/angular/common'; import { AnimationBuilder } from '@ionic/core'; -import { NavController } from '../../providers/nav-controller'; - import { IonRouterOutlet } from './ion-router-outlet'; @Directive({ diff --git a/packages/angular/src/directives/navigation/ion-router-outlet.ts b/packages/angular/src/directives/navigation/ion-router-outlet.ts index cef96a1c67b..3be22d08b40 100644 --- a/packages/angular/src/directives/navigation/ion-router-outlet.ts +++ b/packages/angular/src/directives/navigation/ion-router-outlet.ts @@ -21,13 +21,12 @@ import { reflectComponentType, } from '@angular/core'; import { OutletContext, Router, ActivatedRoute, ChildrenOutletContexts, PRIMARY_OUTLET, Data } from '@angular/router'; -import { Config } from '@ionic/angular/common'; +import { Config, NavController } from '@ionic/angular/common'; import { componentOnReady } from '@ionic/core'; import { Observable, BehaviorSubject, Subscription, combineLatest, of } from 'rxjs'; import { distinctUntilChanged, filter, switchMap } from 'rxjs/operators'; import { AnimationBuilder } from '../../ionic-core'; -import { NavController } from '../../providers/nav-controller'; import { StackController } from './stack-controller'; import { RouteView, getUrl } from './stack-utils'; diff --git a/packages/angular/src/directives/navigation/ion-tabs.ts b/packages/angular/src/directives/navigation/ion-tabs.ts index 1fa8965218b..927c79c70dc 100644 --- a/packages/angular/src/directives/navigation/ion-tabs.ts +++ b/packages/angular/src/directives/navigation/ion-tabs.ts @@ -12,7 +12,7 @@ import { ViewChild, } from '@angular/core'; -import { NavController } from '../../providers/nav-controller'; +import { NavController } from '@ionic/angular/common'; import { IonTabBar } from '../proxies'; import { IonRouterOutlet } from './ion-router-outlet'; diff --git a/packages/angular/src/directives/navigation/router-link-delegate.ts b/packages/angular/src/directives/navigation/router-link-delegate.ts index 40a2a395a8b..bcca0e0271e 100644 --- a/packages/angular/src/directives/navigation/router-link-delegate.ts +++ b/packages/angular/src/directives/navigation/router-link-delegate.ts @@ -3,7 +3,7 @@ import { ElementRef, OnChanges, OnInit, Directive, HostListener, Input, Optional import { Router, RouterLink } from '@angular/router'; import { AnimationBuilder, RouterDirection } from '@ionic/core'; -import { NavController } from '../../providers/nav-controller'; +import { NavController } from '@ionic/angular/common'; /** * Adds support for Ionic routing directions and animations to the base Angular router link directive. diff --git a/packages/angular/src/directives/navigation/stack-controller.ts b/packages/angular/src/directives/navigation/stack-controller.ts index 809b67315f4..440a6eaf00a 100644 --- a/packages/angular/src/directives/navigation/stack-controller.ts +++ b/packages/angular/src/directives/navigation/stack-controller.ts @@ -4,7 +4,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { bindLifecycleEvents } from '@ionic/angular/common'; import { AnimationBuilder, RouterDirection } from '@ionic/core'; -import { NavController } from '../../providers/nav-controller'; +import { NavController } from '@ionic/angular/common'; import { RouteView, diff --git a/packages/angular/src/index.ts b/packages/angular/src/index.ts index 77860a8daa2..ee3d20c6515 100644 --- a/packages/angular/src/index.ts +++ b/packages/angular/src/index.ts @@ -30,12 +30,12 @@ export { AnimationController, GestureController, DomController, + NavController, Config, Platform, AngularDelegate, NavParams, } from '@ionic/angular/common'; -export { NavController } from './providers/nav-controller'; // ROUTER STRATEGY export { IonicRouteStrategy } from './util/ionic-router-reuse-strategy'; From c4d3e12646f07a167e664514872f362b1daf84b0 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Tue, 1 Aug 2023 15:08:07 -0400 Subject: [PATCH 02/23] chore(): add other files --- .../angular/src/providers/nav-controller.ts | 257 ------------------ 1 file changed, 257 deletions(-) delete mode 100644 packages/angular/src/providers/nav-controller.ts diff --git a/packages/angular/src/providers/nav-controller.ts b/packages/angular/src/providers/nav-controller.ts deleted file mode 100644 index b5859d52496..00000000000 --- a/packages/angular/src/providers/nav-controller.ts +++ /dev/null @@ -1,257 +0,0 @@ -import { Location } from '@angular/common'; -import { Injectable, Optional } from '@angular/core'; -import { NavigationExtras, Router, UrlSerializer, UrlTree, NavigationStart } from '@angular/router'; -import { Platform } from '@ionic/angular/common'; -import { AnimationBuilder, NavDirection, RouterDirection } from '@ionic/core'; - -import { IonRouterOutlet } from '../directives/navigation/ion-router-outlet'; - -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`. - * - * Resolves to `true` if it was able to pop. - */ - async pop(): Promise { - let outlet = this.topOutlet; - - while (outlet) { - if (await outlet.pop()) { - return true; - } else { - outlet = outlet.parentOutlet; - } - } - - return false; - } - - /** - * 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; From 909a8fbf464ca90749948d8d0a4e747038ed508e Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Tue, 1 Aug 2023 15:24:48 -0400 Subject: [PATCH 03/23] refactor(angular): move router outlet to common --- .../directives/navigation/router-outlet.ts | 509 ++++++++++++++++++ .../directives/navigation/stack-controller.ts | 6 +- .../src/directives/navigation/stack-utils.ts | 2 +- packages/angular/common/src/index.ts | 2 + .../common/src/providers/nav-controller.ts | 5 +- .../navigation/ion-router-outlet.ts | 506 +---------------- .../src/directives/navigation/ion-tabs.ts | 6 +- .../navigation/router-link-delegate.ts | 3 +- packages/angular/src/ionic-module.ts | 11 +- 9 files changed, 533 insertions(+), 517 deletions(-) create mode 100644 packages/angular/common/src/directives/navigation/router-outlet.ts rename packages/angular/{ => common}/src/directives/navigation/stack-controller.ts (97%) rename packages/angular/{ => common}/src/directives/navigation/stack-utils.ts (96%) diff --git a/packages/angular/common/src/directives/navigation/router-outlet.ts b/packages/angular/common/src/directives/navigation/router-outlet.ts new file mode 100644 index 00000000000..e57a3faf854 --- /dev/null +++ b/packages/angular/common/src/directives/navigation/router-outlet.ts @@ -0,0 +1,509 @@ +import { Location } from '@angular/common'; +import { + ComponentRef, + ElementRef, + Injector, + NgZone, + OnDestroy, + OnInit, + ViewContainerRef, + inject, + Attribute, + Directive, + EventEmitter, + Optional, + Output, + SkipSelf, + EnvironmentInjector, + Input, + InjectionToken, + Injectable, + reflectComponentType, +} from '@angular/core'; +import { OutletContext, Router, ActivatedRoute, ChildrenOutletContexts, PRIMARY_OUTLET, Data } from '@angular/router'; +import { componentOnReady } from '@ionic/core/components'; +import type { AnimationBuilder } from '@ionic/core/components'; +import { Observable, BehaviorSubject, Subscription, combineLatest, of } from 'rxjs'; +import { distinctUntilChanged, filter, switchMap } from 'rxjs/operators'; + +import { Config } from '../../providers/config'; +import { NavController } from '../../providers/nav-controller'; + +import { StackController } from './stack-controller'; +import { RouteView, getUrl } from './stack-utils'; + +// TODO(FW-2827): types + +@Directive({ + selector: 'ion-router-outlet', + exportAs: 'outlet', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: ['animated', 'animation', 'mode', 'swipeGesture'], +}) +// eslint-disable-next-line @angular-eslint/directive-class-suffix +export class IonRouterOutlet implements OnDestroy, OnInit { + nativeEl: HTMLIonRouterOutletElement; + activatedView: RouteView | null = null; + tabsPrefix: string | undefined; + + private _swipeGesture?: boolean; + private stackCtrl: StackController; + + // Maintain map of activated route proxies for each component instance + private proxyMap = new WeakMap(); + // Keep the latest activated route in a subject for the proxy routes to switch map to + private currentActivatedRoute$ = new BehaviorSubject<{ component: any; activatedRoute: ActivatedRoute } | null>(null); + + private activated: ComponentRef | null = null; + /** @internal */ + get activatedComponentRef(): ComponentRef | null { + return this.activated; + } + private _activatedRoute: ActivatedRoute | null = null; + + /** + * The name of the outlet + */ + @Input() name = PRIMARY_OUTLET; + + @Output() stackEvents = new EventEmitter(); + // eslint-disable-next-line @angular-eslint/no-output-rename + @Output('activate') activateEvents = new EventEmitter(); + // eslint-disable-next-line @angular-eslint/no-output-rename + @Output('deactivate') deactivateEvents = new EventEmitter(); + + private parentContexts = inject(ChildrenOutletContexts); + private location = inject(ViewContainerRef); + private environmentInjector = inject(EnvironmentInjector); + private inputBinder = inject(INPUT_BINDER, { optional: true }); + /** @nodoc */ + readonly supportsBindingToComponentInputs = true; + + // Ionic providers + private config = inject(Config); + private navCtrl = inject(NavController); + + set animation(animation: AnimationBuilder) { + this.nativeEl.animation = animation; + } + + set animated(animated: boolean) { + this.nativeEl.animated = animated; + } + + set swipeGesture(swipe: boolean) { + this._swipeGesture = swipe; + + this.nativeEl.swipeHandler = swipe + ? { + canStart: () => this.stackCtrl.canGoBack(1) && !this.stackCtrl.hasRunningTask(), + onStart: () => this.stackCtrl.startBackTransition(), + onEnd: (shouldContinue) => this.stackCtrl.endBackTransition(shouldContinue), + } + : undefined; + } + + constructor( + @Attribute('name') name: string, + @Optional() @Attribute('tabs') tabs: string, + commonLocation: Location, + elementRef: ElementRef, + router: Router, + zone: NgZone, + activatedRoute: ActivatedRoute, + @SkipSelf() @Optional() readonly parentOutlet?: IonRouterOutlet + ) { + this.nativeEl = elementRef.nativeElement; + this.name = name || PRIMARY_OUTLET; + this.tabsPrefix = tabs === 'true' ? getUrl(router, activatedRoute) : undefined; + this.stackCtrl = new StackController(this.tabsPrefix, this.nativeEl, router, this.navCtrl, zone, commonLocation); + this.parentContexts.onChildOutletCreated(this.name, this as any); + } + + ngOnDestroy(): void { + this.stackCtrl.destroy(); + this.inputBinder?.unsubscribeFromRouteData(this); + } + + getContext(): OutletContext | null { + return this.parentContexts.getContext(this.name); + } + + ngOnInit(): void { + this.initializeOutletWithName(); + } + + // Note: Ionic deviates from the Angular Router implementation here + private initializeOutletWithName() { + if (!this.activated) { + // If the outlet was not instantiated at the time the route got activated we need to populate + // the outlet when it is initialized (ie inside a NgIf) + const context = this.getContext(); + if (context?.route) { + this.activateWith(context.route, context.injector); + } + } + + new Promise((resolve) => componentOnReady(this.nativeEl, resolve)).then(() => { + if (this._swipeGesture === undefined) { + this.swipeGesture = this.config.getBoolean('swipeBackEnabled', (this.nativeEl as any).mode === 'ios'); + } + }); + } + + get isActivated(): boolean { + return !!this.activated; + } + + get component(): Record { + if (!this.activated) { + throw new Error('Outlet is not activated'); + } + return this.activated.instance; + } + + get activatedRoute(): ActivatedRoute { + if (!this.activated) { + throw new Error('Outlet is not activated'); + } + return this._activatedRoute as ActivatedRoute; + } + + get activatedRouteData(): Data { + if (this._activatedRoute) { + return this._activatedRoute.snapshot.data; + } + return {}; + } + + /** + * Called when the `RouteReuseStrategy` instructs to detach the subtree + */ + detach(): ComponentRef { + throw new Error('incompatible reuse strategy'); + } + + /** + * Called when the `RouteReuseStrategy` instructs to re-attach a previously detached subtree + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + attach(_ref: ComponentRef, _activatedRoute: ActivatedRoute): void { + throw new Error('incompatible reuse strategy'); + } + + deactivate(): void { + if (this.activated) { + if (this.activatedView) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const context = this.getContext()!; + this.activatedView.savedData = new Map(context.children['contexts']); + + /** + * Angular v11.2.10 introduced a change + * where this route context is cleared out when + * a router-outlet is deactivated, However, + * we need this route information in order to + * return a user back to the correct tab when + * leaving and then going back to the tab context. + */ + const primaryOutlet = this.activatedView.savedData.get('primary'); + if (primaryOutlet && context.route) { + primaryOutlet.route = { ...context.route }; + } + + /** + * Ensure we are saving the NavigationExtras + * data otherwise it will be lost + */ + this.activatedView.savedExtras = {}; + if (context.route) { + const contextSnapshot = context.route.snapshot; + + this.activatedView.savedExtras.queryParams = contextSnapshot.queryParams; + (this.activatedView.savedExtras.fragment as string | null) = contextSnapshot.fragment; + } + } + const c = this.component; + this.activatedView = null; + this.activated = null; + this._activatedRoute = null; + this.deactivateEvents.emit(c); + } + } + + activateWith(activatedRoute: ActivatedRoute, environmentInjector: EnvironmentInjector | null): void { + if (this.isActivated) { + throw new Error('Cannot activate an already activated outlet'); + } + this._activatedRoute = activatedRoute; + + let cmpRef: any; + let enteringView = this.stackCtrl.getExistingView(activatedRoute); + if (enteringView) { + cmpRef = this.activated = enteringView.ref; + const saved = enteringView.savedData; + if (saved) { + // self-restore + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const context = this.getContext()!; + context.children['contexts'] = saved; + } + // Updated activated route proxy for this component + this.updateActivatedRouteProxy(cmpRef.instance, activatedRoute); + } else { + const snapshot = (activatedRoute as any)._futureSnapshot; + + /** + * Angular 14 introduces a new `loadComponent` property to the route config. + * This function will assign a `component` property to the route snapshot. + * We check for the presence of this property to determine if the route is + * using standalone components. + */ + const childContexts = this.parentContexts.getOrCreateContext(this.name).children; + + // We create an activated route proxy object that will maintain future updates for this component + // over its lifecycle in the stack. + const component$ = new BehaviorSubject(null); + const activatedRouteProxy = this.createActivatedRouteProxy(component$, activatedRoute); + + const injector = new OutletInjector(activatedRouteProxy, childContexts, this.location.injector); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const component = snapshot.routeConfig!.component ?? snapshot.component; + + cmpRef = this.activated = this.location.createComponent(component, { + index: this.location.length, + injector, + environmentInjector: environmentInjector ?? this.environmentInjector, + }); + + // Once the component is created we can push it to our local subject supplied to the proxy + component$.next(cmpRef.instance); + + // Calling `markForCheck` to make sure we will run the change detection when the + // `RouterOutlet` is inside a `ChangeDetectionStrategy.OnPush` component. + enteringView = this.stackCtrl.createView(this.activated, activatedRoute); + + // Store references to the proxy by component + this.proxyMap.set(cmpRef.instance, activatedRouteProxy); + this.currentActivatedRoute$.next({ component: cmpRef.instance, activatedRoute }); + } + + this.inputBinder?.bindActivatedRouteToOutletComponent(this); + + this.activatedView = enteringView; + + /** + * The top outlet is set prior to the entering view's transition completing, + * so that when we have nested outlets (e.g. ion-tabs inside an ion-router-outlet), + * the tabs outlet will be assigned as the top outlet when a view inside tabs is + * activated. + * + * In this scenario, activeWith is called for both the tabs and the root router outlet. + * To avoid a race condition, we assign the top outlet synchronously. + */ + this.navCtrl.setTopOutlet(this); + + this.stackCtrl.setActive(enteringView).then((data) => { + this.activateEvents.emit(cmpRef.instance); + this.stackEvents.emit(data); + }); + } + + /** + * Returns `true` if there are pages in the stack to go back. + */ + canGoBack(deep = 1, stackId?: string): boolean { + return this.stackCtrl.canGoBack(deep, stackId); + } + + /** + * Resolves to `true` if it the outlet was able to sucessfully pop the last N pages. + */ + pop(deep = 1, stackId?: string): Promise { + return this.stackCtrl.pop(deep, stackId); + } + + /** + * Returns the URL of the active page of each stack. + */ + getLastUrl(stackId?: string): string | undefined { + const active = this.stackCtrl.getLastUrl(stackId); + return active ? active.url : undefined; + } + + /** + * Returns the RouteView of the active page of each stack. + * @internal + */ + getLastRouteView(stackId?: string): RouteView | undefined { + return this.stackCtrl.getLastUrl(stackId); + } + + /** + * Returns the root view in the tab stack. + * @internal + */ + getRootView(stackId?: string): RouteView | undefined { + return this.stackCtrl.getRootUrl(stackId); + } + + /** + * Returns the active stack ID. In the context of ion-tabs, it means the active tab. + */ + getActiveStackId(): string | undefined { + return this.stackCtrl.getActiveStackId(); + } + + /** + * Since the activated route can change over the life time of a component in an ion router outlet, we create + * a proxy so that we can update the values over time as a user navigates back to components already in the stack. + */ + private createActivatedRouteProxy(component$: Observable, activatedRoute: ActivatedRoute): ActivatedRoute { + const proxy: any = new ActivatedRoute(); + + proxy._futureSnapshot = (activatedRoute as any)._futureSnapshot; + proxy._routerState = (activatedRoute as any)._routerState; + proxy.snapshot = activatedRoute.snapshot; + proxy.outlet = activatedRoute.outlet; + proxy.component = activatedRoute.component; + + // Setup wrappers for the observables so consumers don't have to worry about switching to new observables as the state updates + (proxy as any)._paramMap = this.proxyObservable(component$, 'paramMap'); + (proxy as any)._queryParamMap = this.proxyObservable(component$, 'queryParamMap'); + proxy.url = this.proxyObservable(component$, 'url'); + proxy.params = this.proxyObservable(component$, 'params'); + proxy.queryParams = this.proxyObservable(component$, 'queryParams'); + proxy.fragment = this.proxyObservable(component$, 'fragment'); + proxy.data = this.proxyObservable(component$, 'data'); + + return proxy as ActivatedRoute; + } + + /** + * Create a wrapped observable that will switch to the latest activated route matched by the given component + */ + private proxyObservable(component$: Observable, path: string): Observable { + return component$.pipe( + // First wait until the component instance is pushed + filter((component) => !!component), + switchMap((component) => + this.currentActivatedRoute$.pipe( + filter((current) => current !== null && current.component === component), + switchMap((current) => current && (current.activatedRoute as any)[path]), + distinctUntilChanged() + ) + ) + ); + } + + /** + * Updates the activated route proxy for the given component to the new incoming router state + */ + private updateActivatedRouteProxy(component: any, activatedRoute: ActivatedRoute): void { + const proxy = this.proxyMap.get(component); + if (!proxy) { + throw new Error(`Could not find activated route proxy for view`); + } + + (proxy as any)._futureSnapshot = (activatedRoute as any)._futureSnapshot; + (proxy as any)._routerState = (activatedRoute as any)._routerState; + proxy.snapshot = activatedRoute.snapshot; + proxy.outlet = activatedRoute.outlet; + proxy.component = activatedRoute.component; + + this.currentActivatedRoute$.next({ component, activatedRoute }); + } +} + +class OutletInjector implements Injector { + constructor(private route: ActivatedRoute, private childContexts: ChildrenOutletContexts, private parent: Injector) {} + + get(token: any, notFoundValue?: any): any { + if (token === ActivatedRoute) { + return this.route; + } + + if (token === ChildrenOutletContexts) { + return this.childContexts; + } + + return this.parent.get(token, notFoundValue); + } +} + +// TODO: FW-4785 - Remove this once Angular 15 support is dropped +export const INPUT_BINDER = new InjectionToken(''); + +/** + * Injectable used as a tree-shakable provider for opting in to binding router data to component + * inputs. + * + * The RouterOutlet registers itself with this service when an `ActivatedRoute` is attached or + * activated. When this happens, the service subscribes to the `ActivatedRoute` observables (params, + * queryParams, data) and sets the inputs of the component using `ComponentRef.setInput`. + * Importantly, when an input does not have an item in the route data with a matching key, this + * input is set to `undefined`. If it were not done this way, the previous information would be + * retained if the data got removed from the route (i.e. if a query parameter is removed). + * + * The `RouterOutlet` should unregister itself when destroyed via `unsubscribeFromRouteData` so that + * the subscriptions are cleaned up. + */ +@Injectable() +export class RoutedComponentInputBinder { + private outletDataSubscriptions = new Map(); + + bindActivatedRouteToOutletComponent(outlet: IonRouterOutlet): void { + this.unsubscribeFromRouteData(outlet); + this.subscribeToRouteData(outlet); + } + + unsubscribeFromRouteData(outlet: IonRouterOutlet): void { + this.outletDataSubscriptions.get(outlet)?.unsubscribe(); + this.outletDataSubscriptions.delete(outlet); + } + + private subscribeToRouteData(outlet: IonRouterOutlet) { + const { activatedRoute } = outlet; + const dataSubscription = combineLatest([activatedRoute.queryParams, activatedRoute.params, activatedRoute.data]) + .pipe( + switchMap(([queryParams, params, data], index) => { + data = { ...queryParams, ...params, ...data }; + // Get the first result from the data subscription synchronously so it's available to + // the component as soon as possible (and doesn't require a second change detection). + if (index === 0) { + return of(data); + } + // Promise.resolve is used to avoid synchronously writing the wrong data when + // two of the Observables in the `combineLatest` stream emit one after + // another. + return Promise.resolve(data); + }) + ) + .subscribe((data) => { + // Outlet may have been deactivated or changed names to be associated with a different + // route + if ( + !outlet.isActivated || + !outlet.activatedComponentRef || + outlet.activatedRoute !== activatedRoute || + activatedRoute.component === null + ) { + this.unsubscribeFromRouteData(outlet); + return; + } + + const mirror = reflectComponentType(activatedRoute.component); + if (!mirror) { + this.unsubscribeFromRouteData(outlet); + return; + } + + for (const { templateName } of mirror.inputs) { + outlet.activatedComponentRef.setInput(templateName, data[templateName]); + } + }); + + this.outletDataSubscriptions.set(outlet, dataSubscription); + } +} diff --git a/packages/angular/src/directives/navigation/stack-controller.ts b/packages/angular/common/src/directives/navigation/stack-controller.ts similarity index 97% rename from packages/angular/src/directives/navigation/stack-controller.ts rename to packages/angular/common/src/directives/navigation/stack-controller.ts index 440a6eaf00a..d44c4d0e86e 100644 --- a/packages/angular/src/directives/navigation/stack-controller.ts +++ b/packages/angular/common/src/directives/navigation/stack-controller.ts @@ -1,10 +1,10 @@ import { Location } from '@angular/common'; import { ComponentRef, NgZone } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { bindLifecycleEvents } from '@ionic/angular/common'; -import { AnimationBuilder, RouterDirection } from '@ionic/core'; +import type { AnimationBuilder, RouterDirection } from '@ionic/core/components'; -import { NavController } from '@ionic/angular/common'; +import { bindLifecycleEvents } from '../../providers/angular-delegate'; +import { NavController } from '../../providers/nav-controller'; import { RouteView, diff --git a/packages/angular/src/directives/navigation/stack-utils.ts b/packages/angular/common/src/directives/navigation/stack-utils.ts similarity index 96% rename from packages/angular/src/directives/navigation/stack-utils.ts rename to packages/angular/common/src/directives/navigation/stack-utils.ts index ae33e03ce7a..1dc539e25d0 100644 --- a/packages/angular/src/directives/navigation/stack-utils.ts +++ b/packages/angular/common/src/directives/navigation/stack-utils.ts @@ -1,6 +1,6 @@ import { ComponentRef } from '@angular/core'; import { ActivatedRoute, NavigationExtras, Router } from '@angular/router'; -import { AnimationBuilder, NavDirection, RouterDirection } from '@ionic/core'; +import type { AnimationBuilder, NavDirection, RouterDirection } from '@ionic/core/components'; export const insertView = (views: RouteView[], view: RouteView, direction: RouterDirection): RouteView[] => { if (direction === 'root') { diff --git a/packages/angular/common/src/index.ts b/packages/angular/common/src/index.ts index 7f77bc878dd..dbb4bde6afa 100644 --- a/packages/angular/common/src/index.ts +++ b/packages/angular/common/src/index.ts @@ -20,3 +20,5 @@ export { bindLifecycleEvents, AngularDelegate } from './providers/angular-delega export type { IonicWindow } from './types/interfaces'; export { NavParams } from './directives/navigation/nav-params'; +export { IonRouterOutlet, INPUT_BINDER, RoutedComponentInputBinder } from './directives/navigation/router-outlet'; +export type { StackEvent } from './directives/navigation/stack-utils'; diff --git a/packages/angular/common/src/providers/nav-controller.ts b/packages/angular/common/src/providers/nav-controller.ts index c8b530274b1..4ea29e99fe0 100644 --- a/packages/angular/common/src/providers/nav-controller.ts +++ b/packages/angular/common/src/providers/nav-controller.ts @@ -1,9 +1,10 @@ import { Location } from '@angular/common'; import { Injectable, Optional } from '@angular/core'; import { NavigationExtras, Router, UrlSerializer, UrlTree, NavigationStart } from '@angular/router'; -import { Platform } from './platform'; import { AnimationBuilder, NavDirection, RouterDirection } from '@ionic/core'; +import { Platform } from './platform'; + // LIAM TODO //import { IonRouterOutlet } from '../directives/navigation/ion-router-outlet'; @@ -171,7 +172,7 @@ export class NavController { * @internal */ - // LIAM TODO + // LIAM TODO setTopOutlet(outlet: any): void { this.topOutlet = outlet; } diff --git a/packages/angular/src/directives/navigation/ion-router-outlet.ts b/packages/angular/src/directives/navigation/ion-router-outlet.ts index 3be22d08b40..2187f28b376 100644 --- a/packages/angular/src/directives/navigation/ion-router-outlet.ts +++ b/packages/angular/src/directives/navigation/ion-router-outlet.ts @@ -1,508 +1,8 @@ -import { Location } from '@angular/common'; -import { - ComponentRef, - ElementRef, - Injector, - NgZone, - OnDestroy, - OnInit, - ViewContainerRef, - inject, - Attribute, - Directive, - EventEmitter, - Optional, - Output, - SkipSelf, - EnvironmentInjector, - Input, - InjectionToken, - Injectable, - reflectComponentType, -} from '@angular/core'; -import { OutletContext, Router, ActivatedRoute, ChildrenOutletContexts, PRIMARY_OUTLET, Data } from '@angular/router'; -import { Config, NavController } from '@ionic/angular/common'; -import { componentOnReady } from '@ionic/core'; -import { Observable, BehaviorSubject, Subscription, combineLatest, of } from 'rxjs'; -import { distinctUntilChanged, filter, switchMap } from 'rxjs/operators'; - -import { AnimationBuilder } from '../../ionic-core'; - -import { StackController } from './stack-controller'; -import { RouteView, getUrl } from './stack-utils'; - -// TODO(FW-2827): types +import { Directive } from '@angular/core'; +import { IonRouterOutlet as IonRouterOutletBase } from '@ionic/angular/common'; @Directive({ selector: 'ion-router-outlet', - exportAs: 'outlet', - // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['animated', 'animation', 'mode', 'swipeGesture'], }) // eslint-disable-next-line @angular-eslint/directive-class-suffix -export class IonRouterOutlet implements OnDestroy, OnInit { - nativeEl: HTMLIonRouterOutletElement; - activatedView: RouteView | null = null; - tabsPrefix: string | undefined; - - private _swipeGesture?: boolean; - private stackCtrl: StackController; - - // Maintain map of activated route proxies for each component instance - private proxyMap = new WeakMap(); - // Keep the latest activated route in a subject for the proxy routes to switch map to - private currentActivatedRoute$ = new BehaviorSubject<{ component: any; activatedRoute: ActivatedRoute } | null>(null); - - private activated: ComponentRef | null = null; - /** @internal */ - get activatedComponentRef(): ComponentRef | null { - return this.activated; - } - private _activatedRoute: ActivatedRoute | null = null; - - /** - * The name of the outlet - */ - @Input() name = PRIMARY_OUTLET; - - @Output() stackEvents = new EventEmitter(); - // eslint-disable-next-line @angular-eslint/no-output-rename - @Output('activate') activateEvents = new EventEmitter(); - // eslint-disable-next-line @angular-eslint/no-output-rename - @Output('deactivate') deactivateEvents = new EventEmitter(); - - private parentContexts = inject(ChildrenOutletContexts); - private location = inject(ViewContainerRef); - private environmentInjector = inject(EnvironmentInjector); - private inputBinder = inject(INPUT_BINDER, { optional: true }); - /** @nodoc */ - readonly supportsBindingToComponentInputs = true; - - // Ionic providers - private config = inject(Config); - private navCtrl = inject(NavController); - - set animation(animation: AnimationBuilder) { - this.nativeEl.animation = animation; - } - - set animated(animated: boolean) { - this.nativeEl.animated = animated; - } - - set swipeGesture(swipe: boolean) { - this._swipeGesture = swipe; - - this.nativeEl.swipeHandler = swipe - ? { - canStart: () => this.stackCtrl.canGoBack(1) && !this.stackCtrl.hasRunningTask(), - onStart: () => this.stackCtrl.startBackTransition(), - onEnd: (shouldContinue) => this.stackCtrl.endBackTransition(shouldContinue), - } - : undefined; - } - - constructor( - @Attribute('name') name: string, - @Optional() @Attribute('tabs') tabs: string, - commonLocation: Location, - elementRef: ElementRef, - router: Router, - zone: NgZone, - activatedRoute: ActivatedRoute, - @SkipSelf() @Optional() readonly parentOutlet?: IonRouterOutlet - ) { - this.nativeEl = elementRef.nativeElement; - this.name = name || PRIMARY_OUTLET; - this.tabsPrefix = tabs === 'true' ? getUrl(router, activatedRoute) : undefined; - this.stackCtrl = new StackController(this.tabsPrefix, this.nativeEl, router, this.navCtrl, zone, commonLocation); - this.parentContexts.onChildOutletCreated(this.name, this as any); - } - - ngOnDestroy(): void { - this.stackCtrl.destroy(); - this.inputBinder?.unsubscribeFromRouteData(this); - } - - getContext(): OutletContext | null { - return this.parentContexts.getContext(this.name); - } - - ngOnInit(): void { - this.initializeOutletWithName(); - } - - // Note: Ionic deviates from the Angular Router implementation here - private initializeOutletWithName() { - if (!this.activated) { - // If the outlet was not instantiated at the time the route got activated we need to populate - // the outlet when it is initialized (ie inside a NgIf) - const context = this.getContext(); - if (context?.route) { - this.activateWith(context.route, context.injector); - } - } - - new Promise((resolve) => componentOnReady(this.nativeEl, resolve)).then(() => { - if (this._swipeGesture === undefined) { - this.swipeGesture = this.config.getBoolean('swipeBackEnabled', (this.nativeEl as any).mode === 'ios'); - } - }); - } - - get isActivated(): boolean { - return !!this.activated; - } - - get component(): Record { - if (!this.activated) { - throw new Error('Outlet is not activated'); - } - return this.activated.instance; - } - - get activatedRoute(): ActivatedRoute { - if (!this.activated) { - throw new Error('Outlet is not activated'); - } - return this._activatedRoute as ActivatedRoute; - } - - get activatedRouteData(): Data { - if (this._activatedRoute) { - return this._activatedRoute.snapshot.data; - } - return {}; - } - - /** - * Called when the `RouteReuseStrategy` instructs to detach the subtree - */ - detach(): ComponentRef { - throw new Error('incompatible reuse strategy'); - } - - /** - * Called when the `RouteReuseStrategy` instructs to re-attach a previously detached subtree - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - attach(_ref: ComponentRef, _activatedRoute: ActivatedRoute): void { - throw new Error('incompatible reuse strategy'); - } - - deactivate(): void { - if (this.activated) { - if (this.activatedView) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const context = this.getContext()!; - this.activatedView.savedData = new Map(context.children['contexts']); - - /** - * Angular v11.2.10 introduced a change - * where this route context is cleared out when - * a router-outlet is deactivated, However, - * we need this route information in order to - * return a user back to the correct tab when - * leaving and then going back to the tab context. - */ - const primaryOutlet = this.activatedView.savedData.get('primary'); - if (primaryOutlet && context.route) { - primaryOutlet.route = { ...context.route }; - } - - /** - * Ensure we are saving the NavigationExtras - * data otherwise it will be lost - */ - this.activatedView.savedExtras = {}; - if (context.route) { - const contextSnapshot = context.route.snapshot; - - this.activatedView.savedExtras.queryParams = contextSnapshot.queryParams; - (this.activatedView.savedExtras.fragment as string | null) = contextSnapshot.fragment; - } - } - const c = this.component; - this.activatedView = null; - this.activated = null; - this._activatedRoute = null; - this.deactivateEvents.emit(c); - } - } - - activateWith(activatedRoute: ActivatedRoute, environmentInjector: EnvironmentInjector | null): void { - if (this.isActivated) { - throw new Error('Cannot activate an already activated outlet'); - } - this._activatedRoute = activatedRoute; - - let cmpRef: any; - let enteringView = this.stackCtrl.getExistingView(activatedRoute); - if (enteringView) { - cmpRef = this.activated = enteringView.ref; - const saved = enteringView.savedData; - if (saved) { - // self-restore - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const context = this.getContext()!; - context.children['contexts'] = saved; - } - // Updated activated route proxy for this component - this.updateActivatedRouteProxy(cmpRef.instance, activatedRoute); - } else { - const snapshot = (activatedRoute as any)._futureSnapshot; - - /** - * Angular 14 introduces a new `loadComponent` property to the route config. - * This function will assign a `component` property to the route snapshot. - * We check for the presence of this property to determine if the route is - * using standalone components. - */ - const childContexts = this.parentContexts.getOrCreateContext(this.name).children; - - // We create an activated route proxy object that will maintain future updates for this component - // over its lifecycle in the stack. - const component$ = new BehaviorSubject(null); - const activatedRouteProxy = this.createActivatedRouteProxy(component$, activatedRoute); - - const injector = new OutletInjector(activatedRouteProxy, childContexts, this.location.injector); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const component = snapshot.routeConfig!.component ?? snapshot.component; - - cmpRef = this.activated = this.location.createComponent(component, { - index: this.location.length, - injector, - environmentInjector: environmentInjector ?? this.environmentInjector, - }); - - // Once the component is created we can push it to our local subject supplied to the proxy - component$.next(cmpRef.instance); - - // Calling `markForCheck` to make sure we will run the change detection when the - // `RouterOutlet` is inside a `ChangeDetectionStrategy.OnPush` component. - enteringView = this.stackCtrl.createView(this.activated, activatedRoute); - - // Store references to the proxy by component - this.proxyMap.set(cmpRef.instance, activatedRouteProxy); - this.currentActivatedRoute$.next({ component: cmpRef.instance, activatedRoute }); - } - - this.inputBinder?.bindActivatedRouteToOutletComponent(this); - - this.activatedView = enteringView; - - /** - * The top outlet is set prior to the entering view's transition completing, - * so that when we have nested outlets (e.g. ion-tabs inside an ion-router-outlet), - * the tabs outlet will be assigned as the top outlet when a view inside tabs is - * activated. - * - * In this scenario, activeWith is called for both the tabs and the root router outlet. - * To avoid a race condition, we assign the top outlet synchronously. - */ - this.navCtrl.setTopOutlet(this); - - this.stackCtrl.setActive(enteringView).then((data) => { - this.activateEvents.emit(cmpRef.instance); - this.stackEvents.emit(data); - }); - } - - /** - * Returns `true` if there are pages in the stack to go back. - */ - canGoBack(deep = 1, stackId?: string): boolean { - return this.stackCtrl.canGoBack(deep, stackId); - } - - /** - * Resolves to `true` if it the outlet was able to sucessfully pop the last N pages. - */ - pop(deep = 1, stackId?: string): Promise { - return this.stackCtrl.pop(deep, stackId); - } - - /** - * Returns the URL of the active page of each stack. - */ - getLastUrl(stackId?: string): string | undefined { - const active = this.stackCtrl.getLastUrl(stackId); - return active ? active.url : undefined; - } - - /** - * Returns the RouteView of the active page of each stack. - * @internal - */ - getLastRouteView(stackId?: string): RouteView | undefined { - return this.stackCtrl.getLastUrl(stackId); - } - - /** - * Returns the root view in the tab stack. - * @internal - */ - getRootView(stackId?: string): RouteView | undefined { - return this.stackCtrl.getRootUrl(stackId); - } - - /** - * Returns the active stack ID. In the context of ion-tabs, it means the active tab. - */ - getActiveStackId(): string | undefined { - return this.stackCtrl.getActiveStackId(); - } - - /** - * Since the activated route can change over the life time of a component in an ion router outlet, we create - * a proxy so that we can update the values over time as a user navigates back to components already in the stack. - */ - private createActivatedRouteProxy(component$: Observable, activatedRoute: ActivatedRoute): ActivatedRoute { - const proxy: any = new ActivatedRoute(); - - proxy._futureSnapshot = (activatedRoute as any)._futureSnapshot; - proxy._routerState = (activatedRoute as any)._routerState; - proxy.snapshot = activatedRoute.snapshot; - proxy.outlet = activatedRoute.outlet; - proxy.component = activatedRoute.component; - - // Setup wrappers for the observables so consumers don't have to worry about switching to new observables as the state updates - (proxy as any)._paramMap = this.proxyObservable(component$, 'paramMap'); - (proxy as any)._queryParamMap = this.proxyObservable(component$, 'queryParamMap'); - proxy.url = this.proxyObservable(component$, 'url'); - proxy.params = this.proxyObservable(component$, 'params'); - proxy.queryParams = this.proxyObservable(component$, 'queryParams'); - proxy.fragment = this.proxyObservable(component$, 'fragment'); - proxy.data = this.proxyObservable(component$, 'data'); - - return proxy as ActivatedRoute; - } - - /** - * Create a wrapped observable that will switch to the latest activated route matched by the given component - */ - private proxyObservable(component$: Observable, path: string): Observable { - return component$.pipe( - // First wait until the component instance is pushed - filter((component) => !!component), - switchMap((component) => - this.currentActivatedRoute$.pipe( - filter((current) => current !== null && current.component === component), - switchMap((current) => current && (current.activatedRoute as any)[path]), - distinctUntilChanged() - ) - ) - ); - } - - /** - * Updates the activated route proxy for the given component to the new incoming router state - */ - private updateActivatedRouteProxy(component: any, activatedRoute: ActivatedRoute): void { - const proxy = this.proxyMap.get(component); - if (!proxy) { - throw new Error(`Could not find activated route proxy for view`); - } - - (proxy as any)._futureSnapshot = (activatedRoute as any)._futureSnapshot; - (proxy as any)._routerState = (activatedRoute as any)._routerState; - proxy.snapshot = activatedRoute.snapshot; - proxy.outlet = activatedRoute.outlet; - proxy.component = activatedRoute.component; - - this.currentActivatedRoute$.next({ component, activatedRoute }); - } -} - -class OutletInjector implements Injector { - constructor(private route: ActivatedRoute, private childContexts: ChildrenOutletContexts, private parent: Injector) {} - - get(token: any, notFoundValue?: any): any { - if (token === ActivatedRoute) { - return this.route; - } - - if (token === ChildrenOutletContexts) { - return this.childContexts; - } - - return this.parent.get(token, notFoundValue); - } -} - -// TODO: FW-4785 - Remove this once Angular 15 support is dropped -export const INPUT_BINDER = new InjectionToken(''); - -/** - * Injectable used as a tree-shakable provider for opting in to binding router data to component - * inputs. - * - * The RouterOutlet registers itself with this service when an `ActivatedRoute` is attached or - * activated. When this happens, the service subscribes to the `ActivatedRoute` observables (params, - * queryParams, data) and sets the inputs of the component using `ComponentRef.setInput`. - * Importantly, when an input does not have an item in the route data with a matching key, this - * input is set to `undefined`. If it were not done this way, the previous information would be - * retained if the data got removed from the route (i.e. if a query parameter is removed). - * - * The `RouterOutlet` should unregister itself when destroyed via `unsubscribeFromRouteData` so that - * the subscriptions are cleaned up. - */ -@Injectable() -export class RoutedComponentInputBinder { - private outletDataSubscriptions = new Map(); - - bindActivatedRouteToOutletComponent(outlet: IonRouterOutlet): void { - this.unsubscribeFromRouteData(outlet); - this.subscribeToRouteData(outlet); - } - - unsubscribeFromRouteData(outlet: IonRouterOutlet): void { - this.outletDataSubscriptions.get(outlet)?.unsubscribe(); - this.outletDataSubscriptions.delete(outlet); - } - - private subscribeToRouteData(outlet: IonRouterOutlet) { - const { activatedRoute } = outlet; - const dataSubscription = combineLatest([activatedRoute.queryParams, activatedRoute.params, activatedRoute.data]) - .pipe( - switchMap(([queryParams, params, data], index) => { - data = { ...queryParams, ...params, ...data }; - // Get the first result from the data subscription synchronously so it's available to - // the component as soon as possible (and doesn't require a second change detection). - if (index === 0) { - return of(data); - } - // Promise.resolve is used to avoid synchronously writing the wrong data when - // two of the Observables in the `combineLatest` stream emit one after - // another. - return Promise.resolve(data); - }) - ) - .subscribe((data) => { - // Outlet may have been deactivated or changed names to be associated with a different - // route - if ( - !outlet.isActivated || - !outlet.activatedComponentRef || - outlet.activatedRoute !== activatedRoute || - activatedRoute.component === null - ) { - this.unsubscribeFromRouteData(outlet); - return; - } - - const mirror = reflectComponentType(activatedRoute.component); - if (!mirror) { - this.unsubscribeFromRouteData(outlet); - return; - } - - for (const { templateName } of mirror.inputs) { - outlet.activatedComponentRef.setInput(templateName, data[templateName]); - } - }); - - this.outletDataSubscriptions.set(outlet, dataSubscription); - } -} +export class IonRouterOutlet extends IonRouterOutletBase {} diff --git a/packages/angular/src/directives/navigation/ion-tabs.ts b/packages/angular/src/directives/navigation/ion-tabs.ts index 927c79c70dc..8d1e4adc33a 100644 --- a/packages/angular/src/directives/navigation/ion-tabs.ts +++ b/packages/angular/src/directives/navigation/ion-tabs.ts @@ -11,13 +11,11 @@ import { QueryList, ViewChild, } from '@angular/core'; +import { NavController, IonRouterOutlet } from '@ionic/angular/common'; +import type { StackEvent } from '@ionic/angular/common'; -import { NavController } from '@ionic/angular/common'; import { IonTabBar } from '../proxies'; -import { IonRouterOutlet } from './ion-router-outlet'; -import { StackEvent } from './stack-utils'; - @Component({ selector: 'ion-tabs', template: ` diff --git a/packages/angular/src/directives/navigation/router-link-delegate.ts b/packages/angular/src/directives/navigation/router-link-delegate.ts index bcca0e0271e..8d70fc57ba7 100644 --- a/packages/angular/src/directives/navigation/router-link-delegate.ts +++ b/packages/angular/src/directives/navigation/router-link-delegate.ts @@ -1,9 +1,8 @@ import { LocationStrategy } from '@angular/common'; import { ElementRef, OnChanges, OnInit, Directive, HostListener, Input, Optional } from '@angular/core'; import { Router, RouterLink } from '@angular/router'; -import { AnimationBuilder, RouterDirection } from '@ionic/core'; - import { NavController } from '@ionic/angular/common'; +import { AnimationBuilder, RouterDirection } from '@ionic/core'; /** * Adds support for Ionic routing directions and animations to the base Angular router link directive. diff --git a/packages/angular/src/ionic-module.ts b/packages/angular/src/ionic-module.ts index b30fb8b97d0..d24c4b727ac 100644 --- a/packages/angular/src/ionic-module.ts +++ b/packages/angular/src/ionic-module.ts @@ -1,7 +1,14 @@ import { CommonModule, DOCUMENT } from '@angular/common'; import { ModuleWithProviders, APP_INITIALIZER, NgModule, NgZone } from '@angular/core'; import { Router } from '@angular/router'; -import { ModalController, PopoverController, ConfigToken, AngularDelegate } from '@ionic/angular/common'; +import { + ModalController, + PopoverController, + ConfigToken, + AngularDelegate, + INPUT_BINDER, + RoutedComponentInputBinder, +} from '@ionic/angular/common'; import { IonicConfig } from '@ionic/core'; import { appInitialize } from './app-initialize'; @@ -13,7 +20,7 @@ import { TextValueAccessorDirective, } from './directives/control-value-accessors'; import { IonBackButtonDelegateDirective } from './directives/navigation/ion-back-button'; -import { INPUT_BINDER, IonRouterOutlet, RoutedComponentInputBinder } from './directives/navigation/ion-router-outlet'; +import { IonRouterOutlet } from './directives/navigation/ion-router-outlet'; import { IonTabs } from './directives/navigation/ion-tabs'; import { NavDelegate } from './directives/navigation/nav-delegate'; import { From 168b135261df8942cc1e9a06d0b3552306287335 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Tue, 1 Aug 2023 15:28:04 -0400 Subject: [PATCH 04/23] fix import --- packages/angular/src/directives/navigation/ion-tabs.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/angular/src/directives/navigation/ion-tabs.ts b/packages/angular/src/directives/navigation/ion-tabs.ts index 8d1e4adc33a..05f7a05493a 100644 --- a/packages/angular/src/directives/navigation/ion-tabs.ts +++ b/packages/angular/src/directives/navigation/ion-tabs.ts @@ -11,9 +11,9 @@ import { QueryList, ViewChild, } from '@angular/core'; -import { NavController, IonRouterOutlet } from '@ionic/angular/common'; +import { NavController } from '@ionic/angular/common'; import type { StackEvent } from '@ionic/angular/common'; - +import { IonRouterOutlet } from './ion-router-outlet'; import { IonTabBar } from '../proxies'; @Component({ From 2fef5103fb2b79e441db3631057db2ba105d6767 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Tue, 1 Aug 2023 15:30:08 -0400 Subject: [PATCH 05/23] resolve TODOs --- .../angular/common/src/providers/nav-controller.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/angular/common/src/providers/nav-controller.ts b/packages/angular/common/src/providers/nav-controller.ts index 4ea29e99fe0..acdb52c3de3 100644 --- a/packages/angular/common/src/providers/nav-controller.ts +++ b/packages/angular/common/src/providers/nav-controller.ts @@ -1,12 +1,11 @@ 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 type { AnimationBuilder, NavDirection, RouterDirection } from '@ionic/core/components'; import { Platform } from './platform'; -// LIAM TODO -//import { IonRouterOutlet } from '../directives/navigation/ion-router-outlet'; +import { IonRouterOutlet } from '../directives/navigation/router-outlet'; export interface AnimationOptions { animated?: boolean; @@ -20,8 +19,7 @@ export interface NavigationOptions extends NavigationExtras, AnimationOptions {} providedIn: 'root', }) export class NavController { - // LIAM TODO - private topOutlet?: any; + private topOutlet?: IonRouterOutlet; private direction: 'forward' | 'back' | 'root' | 'auto' = DEFAULT_DIRECTION; private animated?: NavDirection = DEFAULT_ANIMATED; private animationBuilder?: AnimationBuilder; @@ -171,9 +169,7 @@ export class NavController { /** * @internal */ - - // LIAM TODO - setTopOutlet(outlet: any): void { + setTopOutlet(outlet: IonRouterOutlet): void { this.topOutlet = outlet; } From 3e94c73c9a26139398758a76309d4727d70f3c00 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Tue, 1 Aug 2023 16:14:05 -0400 Subject: [PATCH 06/23] refactor(angular): migrate back button to common --- .../src/directives/navigation/back-button.ts | 41 +++++++++++++++++++ packages/angular/common/src/index.ts | 3 ++ .../directives/navigation/ion-back-button.ts | 39 ++---------------- 3 files changed, 48 insertions(+), 35 deletions(-) create mode 100644 packages/angular/common/src/directives/navigation/back-button.ts diff --git a/packages/angular/common/src/directives/navigation/back-button.ts b/packages/angular/common/src/directives/navigation/back-button.ts new file mode 100644 index 00000000000..35665015ac0 --- /dev/null +++ b/packages/angular/common/src/directives/navigation/back-button.ts @@ -0,0 +1,41 @@ +import { Directive, HostListener, Input, Optional } from '@angular/core'; +import { NavController } from '../../providers/nav-controller'; +import { Config } from '../../providers/config' +import type { AnimationBuilder } from '@ionic/core/components'; + +import { IonRouterOutlet } from './router-outlet'; + +@Directive({ + selector: 'ion-back-button', +}) +// eslint-disable-next-line @angular-eslint/directive-class-suffix +export class IonBackButton { + @Input() + defaultHref: string | undefined | null; + + @Input() + routerAnimation?: AnimationBuilder; + + constructor( + @Optional() private routerOutlet: IonRouterOutlet, + private navCtrl: NavController, + private config: Config + ) {} + + /** + * @internal + */ + @HostListener('click', ['$event']) + onClick(ev: Event): void { + const defaultHref = this.defaultHref || this.config.get('backButtonDefaultHref'); + + if (this.routerOutlet?.canGoBack()) { + this.navCtrl.setDirection('back', undefined, undefined, this.routerAnimation); + this.routerOutlet.pop(); + ev.preventDefault(); + } else if (defaultHref != null) { + this.navCtrl.navigateBack(defaultHref, { animation: this.routerAnimation }); + ev.preventDefault(); + } + } +} diff --git a/packages/angular/common/src/index.ts b/packages/angular/common/src/index.ts index dbb4bde6afa..fbadbf668f1 100644 --- a/packages/angular/common/src/index.ts +++ b/packages/angular/common/src/index.ts @@ -22,3 +22,6 @@ export type { IonicWindow } from './types/interfaces'; export { NavParams } from './directives/navigation/nav-params'; export { IonRouterOutlet, INPUT_BINDER, RoutedComponentInputBinder } from './directives/navigation/router-outlet'; export type { StackEvent } from './directives/navigation/stack-utils'; + +export { IonBackButton } from './directives/navigation/back-button'; + diff --git a/packages/angular/src/directives/navigation/ion-back-button.ts b/packages/angular/src/directives/navigation/ion-back-button.ts index cefdac067a8..d84f49e4bc4 100644 --- a/packages/angular/src/directives/navigation/ion-back-button.ts +++ b/packages/angular/src/directives/navigation/ion-back-button.ts @@ -1,39 +1,8 @@ -import { Directive, HostListener, Input, Optional } from '@angular/core'; -import { Config, NavController } from '@ionic/angular/common'; -import { AnimationBuilder } from '@ionic/core'; - -import { IonRouterOutlet } from './ion-router-outlet'; +import { Directive } from '@angular/core'; +import { IonBackButton as IonBackButtonBase } from '@ionic/angular/common'; @Directive({ selector: 'ion-back-button', }) -export class IonBackButtonDelegateDirective { - @Input() - defaultHref: string | undefined | null; - - @Input() - routerAnimation?: AnimationBuilder; - - constructor( - @Optional() private routerOutlet: IonRouterOutlet, - private navCtrl: NavController, - private config: Config - ) {} - - /** - * @internal - */ - @HostListener('click', ['$event']) - onClick(ev: Event): void { - const defaultHref = this.defaultHref || this.config.get('backButtonDefaultHref'); - - if (this.routerOutlet?.canGoBack()) { - this.navCtrl.setDirection('back', undefined, undefined, this.routerAnimation); - this.routerOutlet.pop(); - ev.preventDefault(); - } else if (defaultHref != null) { - this.navCtrl.navigateBack(defaultHref, { animation: this.routerAnimation }); - ev.preventDefault(); - } - } -} +// eslint-disable-next-line @angular-eslint/directive-class-suffix +export class IonBackButtonDelegateDirective extends IonBackButtonBase {} From 8fc7bdf44efb0287e9085db7389982ef5de2526d Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Tue, 1 Aug 2023 16:14:14 -0400 Subject: [PATCH 07/23] chore: lint --- .../angular/common/src/directives/navigation/back-button.ts | 5 +++-- packages/angular/common/src/providers/nav-controller.ts | 3 ++- packages/angular/src/directives/navigation/ion-tabs.ts | 4 +++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/angular/common/src/directives/navigation/back-button.ts b/packages/angular/common/src/directives/navigation/back-button.ts index 35665015ac0..97b302d7d2e 100644 --- a/packages/angular/common/src/directives/navigation/back-button.ts +++ b/packages/angular/common/src/directives/navigation/back-button.ts @@ -1,8 +1,9 @@ import { Directive, HostListener, Input, Optional } from '@angular/core'; -import { NavController } from '../../providers/nav-controller'; -import { Config } from '../../providers/config' import type { AnimationBuilder } from '@ionic/core/components'; +import { Config } from '../../providers/config' +import { NavController } from '../../providers/nav-controller'; + import { IonRouterOutlet } from './router-outlet'; @Directive({ diff --git a/packages/angular/common/src/providers/nav-controller.ts b/packages/angular/common/src/providers/nav-controller.ts index acdb52c3de3..a412981da9f 100644 --- a/packages/angular/common/src/providers/nav-controller.ts +++ b/packages/angular/common/src/providers/nav-controller.ts @@ -3,9 +3,10 @@ import { Injectable, Optional } from '@angular/core'; import { NavigationExtras, Router, UrlSerializer, UrlTree, NavigationStart } from '@angular/router'; import type { AnimationBuilder, NavDirection, RouterDirection } from '@ionic/core/components'; +import { IonRouterOutlet } from '../directives/navigation/router-outlet'; + import { Platform } from './platform'; -import { IonRouterOutlet } from '../directives/navigation/router-outlet'; export interface AnimationOptions { animated?: boolean; diff --git a/packages/angular/src/directives/navigation/ion-tabs.ts b/packages/angular/src/directives/navigation/ion-tabs.ts index 05f7a05493a..7755863a497 100644 --- a/packages/angular/src/directives/navigation/ion-tabs.ts +++ b/packages/angular/src/directives/navigation/ion-tabs.ts @@ -13,9 +13,11 @@ import { } from '@angular/core'; import { NavController } from '@ionic/angular/common'; import type { StackEvent } from '@ionic/angular/common'; -import { IonRouterOutlet } from './ion-router-outlet'; + import { IonTabBar } from '../proxies'; +import { IonRouterOutlet } from './ion-router-outlet'; + @Component({ selector: 'ion-tabs', template: ` From 9dc382eef9f6daeff8302efa50819ccb3c22b194 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Tue, 1 Aug 2023 16:18:46 -0400 Subject: [PATCH 08/23] refactor(anguar): migrate router link directive to common --- .../navigation/router-link-delegate.ts | 105 ++++++++++++++++++ packages/angular/common/src/index.ts | 1 + .../navigation/router-link-delegate.ts | 95 +--------------- 3 files changed, 110 insertions(+), 91 deletions(-) create mode 100644 packages/angular/common/src/directives/navigation/router-link-delegate.ts diff --git a/packages/angular/common/src/directives/navigation/router-link-delegate.ts b/packages/angular/common/src/directives/navigation/router-link-delegate.ts new file mode 100644 index 00000000000..a250c222246 --- /dev/null +++ b/packages/angular/common/src/directives/navigation/router-link-delegate.ts @@ -0,0 +1,105 @@ +import { LocationStrategy } from '@angular/common'; +import { ElementRef, OnChanges, OnInit, Directive, HostListener, Input, Optional } from '@angular/core'; +import { Router, RouterLink } from '@angular/router'; +import { NavController } from '../../providers/nav-controller'; +import type { AnimationBuilder, RouterDirection } from '@ionic/core/components'; + +/** + * Adds support for Ionic routing directions and animations to the base Angular router link directive. + * + * When the router link is clicked, the directive will assign the direction and + * animation so that the routing integration will transition correctly. + */ +@Directive({ + selector: ':not(a):not(area)[routerLink]', +}) +export class RouterLinkDelegateDirective implements OnInit, OnChanges { + @Input() + routerDirection: RouterDirection = 'forward'; + + @Input() + routerAnimation?: AnimationBuilder; + + constructor( + private locationStrategy: LocationStrategy, + private navCtrl: NavController, + private elementRef: ElementRef, + private router: Router, + @Optional() private routerLink?: RouterLink + ) {} + + ngOnInit(): void { + this.updateTargetUrlAndHref(); + } + + ngOnChanges(): void { + this.updateTargetUrlAndHref(); + } + + private updateTargetUrlAndHref() { + if (this.routerLink?.urlTree) { + const href = this.locationStrategy.prepareExternalUrl(this.router.serializeUrl(this.routerLink.urlTree)); + this.elementRef.nativeElement.href = href; + } + } + + /** + * @internal + */ + @HostListener('click', ['$event']) + onClick(ev: UIEvent): void { + this.navCtrl.setDirection(this.routerDirection, undefined, undefined, this.routerAnimation); + + /** + * This prevents the browser from + * performing a page reload when pressing + * an Ionic component with routerLink. + * The page reload interferes with routing + * and causes ion-back-button to disappear + * since the local history is wiped on reload. + */ + ev.preventDefault(); + } +} + +@Directive({ + selector: 'a[routerLink],area[routerLink]', +}) +export class RouterLinkWithHrefDelegateDirective implements OnInit, OnChanges { + @Input() + routerDirection: RouterDirection = 'forward'; + + @Input() + routerAnimation?: AnimationBuilder; + + constructor( + private locationStrategy: LocationStrategy, + private navCtrl: NavController, + private elementRef: ElementRef, + private router: Router, + @Optional() private routerLink?: RouterLink + ) {} + + ngOnInit(): void { + this.updateTargetUrlAndHref(); + } + + ngOnChanges(): void { + this.updateTargetUrlAndHref(); + } + + private updateTargetUrlAndHref() { + if (this.routerLink?.urlTree) { + const href = this.locationStrategy.prepareExternalUrl(this.router.serializeUrl(this.routerLink.urlTree)); + this.elementRef.nativeElement.href = href; + } + } + + /** + * @internal + */ + @HostListener('click') + onClick(): void { + this.navCtrl.setDirection(this.routerDirection, undefined, undefined, this.routerAnimation); + } +} diff --git a/packages/angular/common/src/index.ts b/packages/angular/common/src/index.ts index fbadbf668f1..62340968ba2 100644 --- a/packages/angular/common/src/index.ts +++ b/packages/angular/common/src/index.ts @@ -24,4 +24,5 @@ export { IonRouterOutlet, INPUT_BINDER, RoutedComponentInputBinder } from './dir export type { StackEvent } from './directives/navigation/stack-utils'; export { IonBackButton } from './directives/navigation/back-button'; +export { RouterLinkDelegateDirective, RouterLinkWithHrefDelegateDirective } from './directives/navigation/router-link-delegate'; diff --git a/packages/angular/src/directives/navigation/router-link-delegate.ts b/packages/angular/src/directives/navigation/router-link-delegate.ts index 8d70fc57ba7..1d579276e7f 100644 --- a/packages/angular/src/directives/navigation/router-link-delegate.ts +++ b/packages/angular/src/directives/navigation/router-link-delegate.ts @@ -1,8 +1,5 @@ -import { LocationStrategy } from '@angular/common'; -import { ElementRef, OnChanges, OnInit, Directive, HostListener, Input, Optional } from '@angular/core'; -import { Router, RouterLink } from '@angular/router'; -import { NavController } from '@ionic/angular/common'; -import { AnimationBuilder, RouterDirection } from '@ionic/core'; +import { Directive, } from '@angular/core'; +import { RouterLinkDelegateDirective as RouterLinkDelegateBase, RouterLinkWithHrefDelegateDirective as RouterLinkHrefDelegateBase } from '@ionic/angular/common'; /** * Adds support for Ionic routing directions and animations to the base Angular router link directive. @@ -13,93 +10,9 @@ import { AnimationBuilder, RouterDirection } from '@ionic/core'; @Directive({ selector: ':not(a):not(area)[routerLink]', }) -export class RouterLinkDelegateDirective implements OnInit, OnChanges { - @Input() - routerDirection: RouterDirection = 'forward'; - - @Input() - routerAnimation?: AnimationBuilder; - - constructor( - private locationStrategy: LocationStrategy, - private navCtrl: NavController, - private elementRef: ElementRef, - private router: Router, - @Optional() private routerLink?: RouterLink - ) {} - - ngOnInit(): void { - this.updateTargetUrlAndHref(); - } - - ngOnChanges(): void { - this.updateTargetUrlAndHref(); - } - - private updateTargetUrlAndHref() { - if (this.routerLink?.urlTree) { - const href = this.locationStrategy.prepareExternalUrl(this.router.serializeUrl(this.routerLink.urlTree)); - this.elementRef.nativeElement.href = href; - } - } - - /** - * @internal - */ - @HostListener('click', ['$event']) - onClick(ev: UIEvent): void { - this.navCtrl.setDirection(this.routerDirection, undefined, undefined, this.routerAnimation); - - /** - * This prevents the browser from - * performing a page reload when pressing - * an Ionic component with routerLink. - * The page reload interferes with routing - * and causes ion-back-button to disappear - * since the local history is wiped on reload. - */ - ev.preventDefault(); - } -} +export class RouterLinkDelegateDirective extends RouterLinkDelegateBase {} @Directive({ selector: 'a[routerLink],area[routerLink]', }) -export class RouterLinkWithHrefDelegateDirective implements OnInit, OnChanges { - @Input() - routerDirection: RouterDirection = 'forward'; - - @Input() - routerAnimation?: AnimationBuilder; - - constructor( - private locationStrategy: LocationStrategy, - private navCtrl: NavController, - private elementRef: ElementRef, - private router: Router, - @Optional() private routerLink?: RouterLink - ) {} - - ngOnInit(): void { - this.updateTargetUrlAndHref(); - } - - ngOnChanges(): void { - this.updateTargetUrlAndHref(); - } - - private updateTargetUrlAndHref() { - if (this.routerLink?.urlTree) { - const href = this.locationStrategy.prepareExternalUrl(this.router.serializeUrl(this.routerLink.urlTree)); - this.elementRef.nativeElement.href = href; - } - } - - /** - * @internal - */ - @HostListener('click') - onClick(): void { - this.navCtrl.setDirection(this.routerDirection, undefined, undefined, this.routerAnimation); - } -} +export class RouterLinkWithHrefDelegateDirective extends RouterLinkHrefDelegateBase {} From fa912e4ca7b8cc3923a4d4bb20a647e1a112bee9 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Tue, 1 Aug 2023 16:18:52 -0400 Subject: [PATCH 09/23] chore: lint --- .../common/src/directives/navigation/router-link-delegate.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/angular/common/src/directives/navigation/router-link-delegate.ts b/packages/angular/common/src/directives/navigation/router-link-delegate.ts index a250c222246..d996d739428 100644 --- a/packages/angular/common/src/directives/navigation/router-link-delegate.ts +++ b/packages/angular/common/src/directives/navigation/router-link-delegate.ts @@ -1,9 +1,10 @@ import { LocationStrategy } from '@angular/common'; import { ElementRef, OnChanges, OnInit, Directive, HostListener, Input, Optional } from '@angular/core'; import { Router, RouterLink } from '@angular/router'; -import { NavController } from '../../providers/nav-controller'; import type { AnimationBuilder, RouterDirection } from '@ionic/core/components'; +import { NavController } from '../../providers/nav-controller'; + /** * Adds support for Ionic routing directions and animations to the base Angular router link directive. * From b52a4e237d45b8693c27b40ad5a129d8d65c07bc Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Tue, 1 Aug 2023 16:20:50 -0400 Subject: [PATCH 10/23] refactor(angular): migrate nav delegate to common --- .../src/directives/navigation/nav-delegate.ts | 40 ++++++++++++++ packages/angular/common/src/index.ts | 2 +- packages/angular/common/src/utils/proxy.ts | 53 +++++++++++++++++++ .../src/directives/navigation/nav-delegate.ts | 38 ++----------- 4 files changed, 97 insertions(+), 36 deletions(-) create mode 100644 packages/angular/common/src/directives/navigation/nav-delegate.ts create mode 100644 packages/angular/common/src/utils/proxy.ts diff --git a/packages/angular/common/src/directives/navigation/nav-delegate.ts b/packages/angular/common/src/directives/navigation/nav-delegate.ts new file mode 100644 index 00000000000..9072a1c84d8 --- /dev/null +++ b/packages/angular/common/src/directives/navigation/nav-delegate.ts @@ -0,0 +1,40 @@ +import { ElementRef, Injector, Directive, EnvironmentInjector } from '@angular/core'; +import { AngularDelegate } from '../../providers/angular-delegate'; + +import { ProxyCmp, proxyOutputs } from '../../utils/proxy'; + +@ProxyCmp({ + inputs: ['animated', 'animation', 'root', 'rootParams', 'swipeGesture'], + methods: [ + 'push', + 'insert', + 'insertPages', + 'pop', + 'popTo', + 'popToRoot', + 'removeIndex', + 'setRoot', + 'setPages', + 'getActive', + 'getByIndex', + 'canGoBack', + 'getPrevious', + ], +}) +@Directive({ + selector: 'ion-nav', +}) +// eslint-disable-next-line @angular-eslint/directive-class-suffix +export class NavDelegate { + protected el: HTMLElement; + constructor( + ref: ElementRef, + environmentInjector: EnvironmentInjector, + injector: Injector, + angularDelegate: AngularDelegate + ) { + this.el = ref.nativeElement; + ref.nativeElement.delegate = angularDelegate.create(environmentInjector, injector); + proxyOutputs(this, this.el, ['ionNavDidChange', 'ionNavWillChange']); + } +} diff --git a/packages/angular/common/src/index.ts b/packages/angular/common/src/index.ts index 62340968ba2..359deb97fbd 100644 --- a/packages/angular/common/src/index.ts +++ b/packages/angular/common/src/index.ts @@ -25,4 +25,4 @@ export type { StackEvent } from './directives/navigation/stack-utils'; export { IonBackButton } from './directives/navigation/back-button'; export { RouterLinkDelegateDirective, RouterLinkWithHrefDelegateDirective } from './directives/navigation/router-link-delegate'; - +export { NavDelegate } from './directives/navigation/nav-delegate'; diff --git a/packages/angular/common/src/utils/proxy.ts b/packages/angular/common/src/utils/proxy.ts new file mode 100644 index 00000000000..b9070978db1 --- /dev/null +++ b/packages/angular/common/src/utils/proxy.ts @@ -0,0 +1,53 @@ +// TODO: Is there a way we can grab this from angular-component-lib instead? + +/* eslint-disable */ +/* tslint:disable */ +import { fromEvent } from 'rxjs'; + +export const proxyInputs = (Cmp: any, inputs: string[]) => { + const Prototype = Cmp.prototype; + inputs.forEach((item) => { + Object.defineProperty(Prototype, item, { + get() { + return this.el[item]; + }, + set(val: any) { + this.z.runOutsideAngular(() => (this.el[item] = val)); + }, + }); + }); +}; + +export const proxyMethods = (Cmp: any, methods: string[]) => { + const Prototype = Cmp.prototype; + methods.forEach((methodName) => { + Prototype[methodName] = function () { + const args = arguments; + return this.z.runOutsideAngular(() => this.el[methodName].apply(this.el, args)); + }; + }); +}; + +export const proxyOutputs = (instance: any, el: any, events: string[]) => { + events.forEach((eventName) => (instance[eventName] = fromEvent(el, eventName))); +}; + +// tslint:disable-next-line: only-arrow-functions +export function ProxyCmp(opts: { defineCustomElementFn?: () => void; inputs?: any; methods?: any }) { + const decorator = function (cls: any) { + const { defineCustomElementFn, inputs, methods } = opts; + + if (defineCustomElementFn !== undefined) { + defineCustomElementFn(); + } + + if (inputs) { + proxyInputs(cls, inputs); + } + if (methods) { + proxyMethods(cls, methods); + } + return cls; + }; + return decorator; +} diff --git a/packages/angular/src/directives/navigation/nav-delegate.ts b/packages/angular/src/directives/navigation/nav-delegate.ts index bdf7613b85e..000ccc060c6 100644 --- a/packages/angular/src/directives/navigation/nav-delegate.ts +++ b/packages/angular/src/directives/navigation/nav-delegate.ts @@ -1,40 +1,8 @@ -import { ElementRef, Injector, Directive, EnvironmentInjector } from '@angular/core'; -import { AngularDelegate } from '@ionic/angular/common'; +import { Directive } from '@angular/core'; +import { NavDelegate as NavDelegateBase } from '@ionic/angular/common'; -import { ProxyCmp, proxyOutputs } from '../angular-component-lib/utils'; - -@ProxyCmp({ - inputs: ['animated', 'animation', 'root', 'rootParams', 'swipeGesture'], - methods: [ - 'push', - 'insert', - 'insertPages', - 'pop', - 'popTo', - 'popToRoot', - 'removeIndex', - 'setRoot', - 'setPages', - 'getActive', - 'getByIndex', - 'canGoBack', - 'getPrevious', - ], -}) @Directive({ selector: 'ion-nav', }) // eslint-disable-next-line @angular-eslint/directive-class-suffix -export class NavDelegate { - protected el: HTMLElement; - constructor( - ref: ElementRef, - environmentInjector: EnvironmentInjector, - injector: Injector, - angularDelegate: AngularDelegate - ) { - this.el = ref.nativeElement; - ref.nativeElement.delegate = angularDelegate.create(environmentInjector, injector); - proxyOutputs(this, this.el, ['ionNavDidChange', 'ionNavWillChange']); - } -} +export class NavDelegate extends NavDelegateBase {} From 98ef53ef53c79af0190f3cc627e55ce5d42f6c58 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Tue, 1 Aug 2023 16:21:09 -0400 Subject: [PATCH 11/23] chore: lint --- .../angular/common/src/directives/navigation/nav-delegate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/angular/common/src/directives/navigation/nav-delegate.ts b/packages/angular/common/src/directives/navigation/nav-delegate.ts index 9072a1c84d8..a9dfdd29042 100644 --- a/packages/angular/common/src/directives/navigation/nav-delegate.ts +++ b/packages/angular/common/src/directives/navigation/nav-delegate.ts @@ -1,6 +1,6 @@ import { ElementRef, Injector, Directive, EnvironmentInjector } from '@angular/core'; -import { AngularDelegate } from '../../providers/angular-delegate'; +import { AngularDelegate } from '../../providers/angular-delegate'; import { ProxyCmp, proxyOutputs } from '../../utils/proxy'; @ProxyCmp({ From 141dd8a6c81a167524935254cc2c4bea11b9a619 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Tue, 1 Aug 2023 16:32:33 -0400 Subject: [PATCH 12/23] refactor(angular): migrate tabs to common --- .../common/src/directives/navigation/tabs.ts | 183 ++++++++++++++++++ packages/angular/common/src/index.ts | 1 + .../src/directives/navigation/ion-tabs.ts | 168 +--------------- 3 files changed, 187 insertions(+), 165 deletions(-) create mode 100644 packages/angular/common/src/directives/navigation/tabs.ts diff --git a/packages/angular/common/src/directives/navigation/tabs.ts b/packages/angular/common/src/directives/navigation/tabs.ts new file mode 100644 index 00000000000..63e73680da0 --- /dev/null +++ b/packages/angular/common/src/directives/navigation/tabs.ts @@ -0,0 +1,183 @@ +import { + AfterContentChecked, + AfterContentInit, + // ContentChild, + // ContentChildren, + Directive, + ElementRef, + EventEmitter, + HostListener, + Output, + //QueryList, + ViewChild, +} from '@angular/core'; +import { NavController } from '../../providers/nav-controller'; +import type { StackEvent } from './stack-utils'; + +// LIAM TODO +//import { IonTabBar } from '../proxies'; + +//import { IonRouterOutlet } from './router-outlet'; + +@Directive({ + selector: 'ion-tabs' +}) +// eslint-disable-next-line @angular-eslint/component-class-suffix +export class IonTabs implements AfterContentInit, AfterContentChecked { + outlet: any; + tabBar: any; + tabBars: any; + + //@ViewChild('outlet', { read: IonRouterOutlet, static: false }) outlet: IonRouterOutlet; + @ViewChild('tabsInner', { read: ElementRef, static: true }) tabsInner: ElementRef; + + //@ContentChild(IonTabBar, { static: false }) tabBar: IonTabBar | undefined; + //@ContentChildren(IonTabBar) tabBars: QueryList; + + @Output() ionTabsWillChange = new EventEmitter<{ tab: string }>(); + @Output() ionTabsDidChange = new EventEmitter<{ tab: string }>(); + + private tabBarSlot = 'bottom'; + + constructor(private navCtrl: NavController) {} + + ngAfterContentInit(): void { + this.detectSlotChanges(); + } + + ngAfterContentChecked(): void { + this.detectSlotChanges(); + } + + /** + * @internal + */ + onPageSelected(detail: StackEvent): void { + const stackId = detail.enteringView.stackId; + if (detail.tabSwitch && stackId !== undefined) { + this.ionTabsWillChange.emit({ tab: stackId }); + if (this.tabBar) { + this.tabBar.selectedTab = stackId; + } + this.ionTabsDidChange.emit({ tab: stackId }); + } + } + + /** + * When a tab button is clicked, there are several scenarios: + * 1. If the selected tab is currently active (the tab button has been clicked + * again), then it should go to the root view for that tab. + * + * a. Get the saved root view from the router outlet. If the saved root view + * matches the tabRootUrl, set the route view to this view including the + * navigation extras. + * b. If the saved root view from the router outlet does + * not match, navigate to the tabRootUrl. No navigation extras are + * included. + * + * 2. If the current tab tab is not currently selected, get the last route + * view from the router outlet. + * + * a. If the last route view exists, navigate to that view including any + * navigation extras + * b. If the last route view doesn't exist, then navigate + * to the default tabRootUrl + */ + @HostListener('ionTabButtonClick', ['$event']) + select(tabOrEvent: string | CustomEvent): Promise | undefined { + const isTabString = typeof tabOrEvent === 'string'; + const tab = isTabString ? tabOrEvent : (tabOrEvent as CustomEvent).detail.tab; + const alreadySelected = this.outlet.getActiveStackId() === tab; + const tabRootUrl = `${this.outlet.tabsPrefix}/${tab}`; + + /** + * If this is a nested tab, prevent the event + * from bubbling otherwise the outer tabs + * will respond to this event too, causing + * the app to get directed to the wrong place. + */ + if (!isTabString) { + (tabOrEvent as CustomEvent).stopPropagation(); + } + + if (alreadySelected) { + const activeStackId = this.outlet.getActiveStackId(); + const activeView = this.outlet.getLastRouteView(activeStackId); + + // If on root tab, do not navigate to root tab again + if (activeView?.url === tabRootUrl) { + return; + } + + const rootView = this.outlet.getRootView(tab); + const navigationExtras = rootView && tabRootUrl === rootView.url && rootView.savedExtras; + return this.navCtrl.navigateRoot(tabRootUrl, { + ...navigationExtras, + animated: true, + animationDirection: 'back', + }); + } else { + const lastRoute = this.outlet.getLastRouteView(tab); + /** + * If there is a lastRoute, goto that, otherwise goto the fallback url of the + * selected tab + */ + const url = lastRoute?.url || tabRootUrl; + const navigationExtras = lastRoute?.savedExtras; + + return this.navCtrl.navigateRoot(url, { + ...navigationExtras, + animated: true, + animationDirection: 'back', + }); + } + } + + getSelected(): string | undefined { + return this.outlet.getActiveStackId(); + } + + /** + * Detects changes to the slot attribute of the tab bar. + * + * If the slot attribute has changed, then the tab bar + * should be relocated to the new slot position. + */ + private detectSlotChanges(): void { + this.tabBars.forEach((tabBar: any) => { + // el is a protected attribute from the generated component wrapper + const currentSlot = tabBar.el.getAttribute('slot'); + + if (currentSlot !== this.tabBarSlot) { + this.tabBarSlot = currentSlot; + this.relocateTabBar(); + } + }); + } + + /** + * Relocates the tab bar to the new slot position. + */ + private relocateTabBar(): void { + /** + * `el` is a protected attribute from the generated component wrapper. + * To avoid having to manually create the wrapper for tab bar, we + * cast the tab bar to any and access the protected attribute. + */ + const tabBar = (this.tabBar as any).el as HTMLElement; + + if (this.tabBarSlot === 'top') { + /** + * A tab bar with a slot of "top" should be inserted + * at the top of the container. + */ + this.tabsInner.nativeElement.before(tabBar); + } else { + /** + * A tab bar with a slot of "bottom" or without a slot + * should be inserted at the end of the container. + */ + this.tabsInner.nativeElement.after(tabBar); + } + } +} diff --git a/packages/angular/common/src/index.ts b/packages/angular/common/src/index.ts index 359deb97fbd..140f1500ab3 100644 --- a/packages/angular/common/src/index.ts +++ b/packages/angular/common/src/index.ts @@ -26,3 +26,4 @@ export type { StackEvent } from './directives/navigation/stack-utils'; export { IonBackButton } from './directives/navigation/back-button'; export { RouterLinkDelegateDirective, RouterLinkWithHrefDelegateDirective } from './directives/navigation/router-link-delegate'; export { NavDelegate } from './directives/navigation/nav-delegate'; +export { IonTabs } from './directives/navigation/tabs'; diff --git a/packages/angular/src/directives/navigation/ion-tabs.ts b/packages/angular/src/directives/navigation/ion-tabs.ts index 7755863a497..a80e715e7aa 100644 --- a/packages/angular/src/directives/navigation/ion-tabs.ts +++ b/packages/angular/src/directives/navigation/ion-tabs.ts @@ -1,21 +1,7 @@ -import { - AfterContentChecked, - AfterContentInit, - Component, - ContentChild, - ContentChildren, - ElementRef, - EventEmitter, - HostListener, - Output, - QueryList, - ViewChild, -} from '@angular/core'; -import { NavController } from '@ionic/angular/common'; -import type { StackEvent } from '@ionic/angular/common'; +import { Component, ContentChild, ContentChildren, ViewChild, QueryList } from '@angular/core'; +import { IonTabs as IonTabsBase } from '@ionic/angular/common'; import { IonTabBar } from '../proxies'; - import { IonRouterOutlet } from './ion-router-outlet'; @Component({ @@ -55,157 +41,9 @@ import { IonRouterOutlet } from './ion-router-outlet'; ], }) // eslint-disable-next-line @angular-eslint/component-class-suffix -export class IonTabs implements AfterContentInit, AfterContentChecked { +export class IonTabs extends IonTabsBase { @ViewChild('outlet', { read: IonRouterOutlet, static: false }) outlet: IonRouterOutlet; - @ViewChild('tabsInner', { read: ElementRef, static: true }) tabsInner: ElementRef; @ContentChild(IonTabBar, { static: false }) tabBar: IonTabBar | undefined; @ContentChildren(IonTabBar) tabBars: QueryList; - - @Output() ionTabsWillChange = new EventEmitter<{ tab: string }>(); - @Output() ionTabsDidChange = new EventEmitter<{ tab: string }>(); - - private tabBarSlot = 'bottom'; - - constructor(private navCtrl: NavController) {} - - ngAfterContentInit(): void { - this.detectSlotChanges(); - } - - ngAfterContentChecked(): void { - this.detectSlotChanges(); - } - - /** - * @internal - */ - onPageSelected(detail: StackEvent): void { - const stackId = detail.enteringView.stackId; - if (detail.tabSwitch && stackId !== undefined) { - this.ionTabsWillChange.emit({ tab: stackId }); - if (this.tabBar) { - this.tabBar.selectedTab = stackId; - } - this.ionTabsDidChange.emit({ tab: stackId }); - } - } - - /** - * When a tab button is clicked, there are several scenarios: - * 1. If the selected tab is currently active (the tab button has been clicked - * again), then it should go to the root view for that tab. - * - * a. Get the saved root view from the router outlet. If the saved root view - * matches the tabRootUrl, set the route view to this view including the - * navigation extras. - * b. If the saved root view from the router outlet does - * not match, navigate to the tabRootUrl. No navigation extras are - * included. - * - * 2. If the current tab tab is not currently selected, get the last route - * view from the router outlet. - * - * a. If the last route view exists, navigate to that view including any - * navigation extras - * b. If the last route view doesn't exist, then navigate - * to the default tabRootUrl - */ - @HostListener('ionTabButtonClick', ['$event']) - select(tabOrEvent: string | CustomEvent): Promise | undefined { - const isTabString = typeof tabOrEvent === 'string'; - const tab = isTabString ? tabOrEvent : (tabOrEvent as CustomEvent).detail.tab; - const alreadySelected = this.outlet.getActiveStackId() === tab; - const tabRootUrl = `${this.outlet.tabsPrefix}/${tab}`; - - /** - * If this is a nested tab, prevent the event - * from bubbling otherwise the outer tabs - * will respond to this event too, causing - * the app to get directed to the wrong place. - */ - if (!isTabString) { - (tabOrEvent as CustomEvent).stopPropagation(); - } - - if (alreadySelected) { - const activeStackId = this.outlet.getActiveStackId(); - const activeView = this.outlet.getLastRouteView(activeStackId); - - // If on root tab, do not navigate to root tab again - if (activeView?.url === tabRootUrl) { - return; - } - - const rootView = this.outlet.getRootView(tab); - const navigationExtras = rootView && tabRootUrl === rootView.url && rootView.savedExtras; - return this.navCtrl.navigateRoot(tabRootUrl, { - ...navigationExtras, - animated: true, - animationDirection: 'back', - }); - } else { - const lastRoute = this.outlet.getLastRouteView(tab); - /** - * If there is a lastRoute, goto that, otherwise goto the fallback url of the - * selected tab - */ - const url = lastRoute?.url || tabRootUrl; - const navigationExtras = lastRoute?.savedExtras; - - return this.navCtrl.navigateRoot(url, { - ...navigationExtras, - animated: true, - animationDirection: 'back', - }); - } - } - - getSelected(): string | undefined { - return this.outlet.getActiveStackId(); - } - - /** - * Detects changes to the slot attribute of the tab bar. - * - * If the slot attribute has changed, then the tab bar - * should be relocated to the new slot position. - */ - private detectSlotChanges(): void { - this.tabBars.forEach((tabBar: any) => { - // el is a protected attribute from the generated component wrapper - const currentSlot = tabBar.el.getAttribute('slot'); - - if (currentSlot !== this.tabBarSlot) { - this.tabBarSlot = currentSlot; - this.relocateTabBar(); - } - }); - } - - /** - * Relocates the tab bar to the new slot position. - */ - private relocateTabBar(): void { - /** - * `el` is a protected attribute from the generated component wrapper. - * To avoid having to manually create the wrapper for tab bar, we - * cast the tab bar to any and access the protected attribute. - */ - const tabBar = (this.tabBar as any).el as HTMLElement; - - if (this.tabBarSlot === 'top') { - /** - * A tab bar with a slot of "top" should be inserted - * at the top of the container. - */ - this.tabsInner.nativeElement.before(tabBar); - } else { - /** - * A tab bar with a slot of "bottom" or without a slot - * should be inserted at the end of the container. - */ - this.tabsInner.nativeElement.after(tabBar); - } - } } From e46e1d6a08f6f5895736ab993425679a18beb98d Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Tue, 1 Aug 2023 16:34:54 -0400 Subject: [PATCH 13/23] lint --- .../common/src/directives/navigation/tabs.ts | 21 +++++++------------ .../src/directives/navigation/ion-tabs.ts | 1 + 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/packages/angular/common/src/directives/navigation/tabs.ts b/packages/angular/common/src/directives/navigation/tabs.ts index 63e73680da0..f5629569935 100644 --- a/packages/angular/common/src/directives/navigation/tabs.ts +++ b/packages/angular/common/src/directives/navigation/tabs.ts @@ -1,39 +1,34 @@ import { AfterContentChecked, AfterContentInit, - // ContentChild, - // ContentChildren, Directive, ElementRef, EventEmitter, HostListener, Output, - //QueryList, ViewChild, } from '@angular/core'; -import { NavController } from '../../providers/nav-controller'; -import type { StackEvent } from './stack-utils'; -// LIAM TODO -//import { IonTabBar } from '../proxies'; +import { NavController } from '../../providers/nav-controller'; -//import { IonRouterOutlet } from './router-outlet'; +import type { StackEvent } from './stack-utils'; @Directive({ selector: 'ion-tabs' }) -// eslint-disable-next-line @angular-eslint/component-class-suffix +// eslint-disable-next-line @angular-eslint/directive-class-suffix export class IonTabs implements AfterContentInit, AfterContentChecked { + + /** + * Note: These must be redeclared on each child class since it needs + * access to generated components such as IonRouterOutlet and IonTabBar. + */ outlet: any; tabBar: any; tabBars: any; - //@ViewChild('outlet', { read: IonRouterOutlet, static: false }) outlet: IonRouterOutlet; @ViewChild('tabsInner', { read: ElementRef, static: true }) tabsInner: ElementRef; - //@ContentChild(IonTabBar, { static: false }) tabBar: IonTabBar | undefined; - //@ContentChildren(IonTabBar) tabBars: QueryList; - @Output() ionTabsWillChange = new EventEmitter<{ tab: string }>(); @Output() ionTabsDidChange = new EventEmitter<{ tab: string }>(); diff --git a/packages/angular/src/directives/navigation/ion-tabs.ts b/packages/angular/src/directives/navigation/ion-tabs.ts index a80e715e7aa..2d24e822eec 100644 --- a/packages/angular/src/directives/navigation/ion-tabs.ts +++ b/packages/angular/src/directives/navigation/ion-tabs.ts @@ -2,6 +2,7 @@ import { Component, ContentChild, ContentChildren, ViewChild, QueryList } from ' import { IonTabs as IonTabsBase } from '@ionic/angular/common'; import { IonTabBar } from '../proxies'; + import { IonRouterOutlet } from './ion-router-outlet'; @Component({ From ddcf2c6bf59a991af2216cf811dd4b4cfec3ea3b Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Tue, 1 Aug 2023 16:41:24 -0400 Subject: [PATCH 14/23] lint (again!) --- .../common/src/directives/navigation/back-button.ts | 2 +- packages/angular/common/src/directives/navigation/tabs.ts | 3 +-- packages/angular/common/src/index.ts | 5 ++++- packages/angular/common/src/providers/nav-controller.ts | 1 - .../src/directives/navigation/router-link-delegate.ts | 7 +++++-- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/angular/common/src/directives/navigation/back-button.ts b/packages/angular/common/src/directives/navigation/back-button.ts index 97b302d7d2e..8ddc9e14833 100644 --- a/packages/angular/common/src/directives/navigation/back-button.ts +++ b/packages/angular/common/src/directives/navigation/back-button.ts @@ -1,7 +1,7 @@ import { Directive, HostListener, Input, Optional } from '@angular/core'; import type { AnimationBuilder } from '@ionic/core/components'; -import { Config } from '../../providers/config' +import { Config } from '../../providers/config'; import { NavController } from '../../providers/nav-controller'; import { IonRouterOutlet } from './router-outlet'; diff --git a/packages/angular/common/src/directives/navigation/tabs.ts b/packages/angular/common/src/directives/navigation/tabs.ts index f5629569935..9807ea8bed7 100644 --- a/packages/angular/common/src/directives/navigation/tabs.ts +++ b/packages/angular/common/src/directives/navigation/tabs.ts @@ -14,11 +14,10 @@ import { NavController } from '../../providers/nav-controller'; import type { StackEvent } from './stack-utils'; @Directive({ - selector: 'ion-tabs' + selector: 'ion-tabs', }) // eslint-disable-next-line @angular-eslint/directive-class-suffix export class IonTabs implements AfterContentInit, AfterContentChecked { - /** * Note: These must be redeclared on each child class since it needs * access to generated components such as IonRouterOutlet and IonTabBar. diff --git a/packages/angular/common/src/index.ts b/packages/angular/common/src/index.ts index 140f1500ab3..74bc82f3c80 100644 --- a/packages/angular/common/src/index.ts +++ b/packages/angular/common/src/index.ts @@ -24,6 +24,9 @@ export { IonRouterOutlet, INPUT_BINDER, RoutedComponentInputBinder } from './dir export type { StackEvent } from './directives/navigation/stack-utils'; export { IonBackButton } from './directives/navigation/back-button'; -export { RouterLinkDelegateDirective, RouterLinkWithHrefDelegateDirective } from './directives/navigation/router-link-delegate'; +export { + RouterLinkDelegateDirective, + RouterLinkWithHrefDelegateDirective, +} from './directives/navigation/router-link-delegate'; export { NavDelegate } from './directives/navigation/nav-delegate'; export { IonTabs } from './directives/navigation/tabs'; diff --git a/packages/angular/common/src/providers/nav-controller.ts b/packages/angular/common/src/providers/nav-controller.ts index a412981da9f..aff31b090b3 100644 --- a/packages/angular/common/src/providers/nav-controller.ts +++ b/packages/angular/common/src/providers/nav-controller.ts @@ -7,7 +7,6 @@ import { IonRouterOutlet } from '../directives/navigation/router-outlet'; import { Platform } from './platform'; - export interface AnimationOptions { animated?: boolean; animation?: AnimationBuilder; diff --git a/packages/angular/src/directives/navigation/router-link-delegate.ts b/packages/angular/src/directives/navigation/router-link-delegate.ts index 1d579276e7f..066fd86b524 100644 --- a/packages/angular/src/directives/navigation/router-link-delegate.ts +++ b/packages/angular/src/directives/navigation/router-link-delegate.ts @@ -1,5 +1,8 @@ -import { Directive, } from '@angular/core'; -import { RouterLinkDelegateDirective as RouterLinkDelegateBase, RouterLinkWithHrefDelegateDirective as RouterLinkHrefDelegateBase } from '@ionic/angular/common'; +import { Directive } from '@angular/core'; +import { + RouterLinkDelegateDirective as RouterLinkDelegateBase, + RouterLinkWithHrefDelegateDirective as RouterLinkHrefDelegateBase, +} from '@ionic/angular/common'; /** * Adds support for Ionic routing directions and animations to the base Angular router link directive. From 109c37216775405818dea541bcb02afc4562b237 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Tue, 1 Aug 2023 17:04:41 -0400 Subject: [PATCH 15/23] fix(angular): back button receives router outlet --- .../src/directives/navigation/ion-back-button.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/angular/src/directives/navigation/ion-back-button.ts b/packages/angular/src/directives/navigation/ion-back-button.ts index d84f49e4bc4..7b72646fdb5 100644 --- a/packages/angular/src/directives/navigation/ion-back-button.ts +++ b/packages/angular/src/directives/navigation/ion-back-button.ts @@ -1,8 +1,17 @@ -import { Directive } from '@angular/core'; -import { IonBackButton as IonBackButtonBase } from '@ionic/angular/common'; +import { Directive, Optional } from '@angular/core'; +import { IonBackButton as IonBackButtonBase, NavController, Config } from '@ionic/angular/common'; +import { IonRouterOutlet } from './ion-router-outlet'; @Directive({ selector: 'ion-back-button', }) // eslint-disable-next-line @angular-eslint/directive-class-suffix -export class IonBackButtonDelegateDirective extends IonBackButtonBase {} +export class IonBackButtonDelegateDirective extends IonBackButtonBase { + constructor( + @Optional() routerOutlet: IonRouterOutlet, + navCtrl: NavController, + config: Config + ) { + super(routerOutlet, navCtrl, config); + } +} From d8bb3d583f45564cfea8084e767e217a98bc796b Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Tue, 1 Aug 2023 17:04:57 -0400 Subject: [PATCH 16/23] lint --- packages/angular/src/directives/navigation/ion-back-button.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/angular/src/directives/navigation/ion-back-button.ts b/packages/angular/src/directives/navigation/ion-back-button.ts index 7b72646fdb5..5a9614c0d31 100644 --- a/packages/angular/src/directives/navigation/ion-back-button.ts +++ b/packages/angular/src/directives/navigation/ion-back-button.ts @@ -1,5 +1,6 @@ import { Directive, Optional } from '@angular/core'; import { IonBackButton as IonBackButtonBase, NavController, Config } from '@ionic/angular/common'; + import { IonRouterOutlet } from './ion-router-outlet'; @Directive({ From 7a75a6f6866f5577992b2804ce92ac92f97ac1df Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Tue, 1 Aug 2023 17:10:41 -0400 Subject: [PATCH 17/23] lint --- .../angular/src/directives/navigation/ion-back-button.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/angular/src/directives/navigation/ion-back-button.ts b/packages/angular/src/directives/navigation/ion-back-button.ts index 5a9614c0d31..fbf14d7fb5a 100644 --- a/packages/angular/src/directives/navigation/ion-back-button.ts +++ b/packages/angular/src/directives/navigation/ion-back-button.ts @@ -8,11 +8,7 @@ import { IonRouterOutlet } from './ion-router-outlet'; }) // eslint-disable-next-line @angular-eslint/directive-class-suffix export class IonBackButtonDelegateDirective extends IonBackButtonBase { - constructor( - @Optional() routerOutlet: IonRouterOutlet, - navCtrl: NavController, - config: Config - ) { + constructor(@Optional() routerOutlet: IonRouterOutlet, navCtrl: NavController, config: Config) { super(routerOutlet, navCtrl, config); } } From d9175395016821b8b40e8095adbbcd2cec717cbc Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Thu, 3 Aug 2023 10:26:18 -0400 Subject: [PATCH 18/23] feat(angular): add standalone router outlet --- packages/angular/common/src/index.ts | 2 ++ packages/angular/standalone/src/index.ts | 4 +--- .../standalone/src/navigation/router-outlet.ts | 13 +++++++++++++ 3 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 packages/angular/standalone/src/navigation/router-outlet.ts diff --git a/packages/angular/common/src/index.ts b/packages/angular/common/src/index.ts index 74bc82f3c80..7229a93a0d1 100644 --- a/packages/angular/common/src/index.ts +++ b/packages/angular/common/src/index.ts @@ -30,3 +30,5 @@ export { } from './directives/navigation/router-link-delegate'; export { NavDelegate } from './directives/navigation/nav-delegate'; export { IonTabs } from './directives/navigation/tabs'; + +export { ProxyCmp } from './utils/proxy'; diff --git a/packages/angular/standalone/src/index.ts b/packages/angular/standalone/src/index.ts index a5a128b2514..f5fc4e8273d 100644 --- a/packages/angular/standalone/src/index.ts +++ b/packages/angular/standalone/src/index.ts @@ -1,3 +1 @@ -// This is required to get ng-packagr to build. -// Remove this when you actually have something to export -export const placeholder = true; +export { IonRouterOutlet } from './navigation/router-outlet'; diff --git a/packages/angular/standalone/src/navigation/router-outlet.ts b/packages/angular/standalone/src/navigation/router-outlet.ts new file mode 100644 index 00000000000..98bb16c1f27 --- /dev/null +++ b/packages/angular/standalone/src/navigation/router-outlet.ts @@ -0,0 +1,13 @@ +import { Directive } from '@angular/core'; +import { IonRouterOutlet as IonRouterOutletBase, ProxyCmp } from '@ionic/angular/common'; +import { defineCustomElement } from '@ionic/core/components/ion-router-outlet.js'; + +@ProxyCmp({ + defineCustomElementFn: defineCustomElement, +}) +@Directive({ + selector: 'ion-router-outlet', + standalone: true +}) +// eslint-disable-next-line @angular-eslint/directive-class-suffix +export class IonRouterOutlet extends IonRouterOutletBase {} From 1e941202ad50e47aa453a889999ab5753b8a419b Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Thu, 3 Aug 2023 10:33:19 -0400 Subject: [PATCH 19/23] test: add test --- .../test/base/e2e/src/standalone/router-outlet.spec.ts | 9 +++++++++ .../src/app/standalone/app-standalone/app.component.html | 2 +- .../src/app/standalone/app-standalone/app.component.ts | 4 ++-- .../base/src/app/standalone/app-standalone/app.routes.ts | 1 + .../router-outlet/router-outlet.component.html | 1 + .../standalone/router-outlet/router-outlet.component.ts | 8 ++++++++ 6 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 packages/angular/test/base/e2e/src/standalone/router-outlet.spec.ts create mode 100644 packages/angular/test/base/src/app/standalone/router-outlet/router-outlet.component.html create mode 100644 packages/angular/test/base/src/app/standalone/router-outlet/router-outlet.component.ts diff --git a/packages/angular/test/base/e2e/src/standalone/router-outlet.spec.ts b/packages/angular/test/base/e2e/src/standalone/router-outlet.spec.ts new file mode 100644 index 00000000000..37beac46fc8 --- /dev/null +++ b/packages/angular/test/base/e2e/src/standalone/router-outlet.spec.ts @@ -0,0 +1,9 @@ +describe('Router Outlet', () => { + beforeEach(() => { + cy.visit('/standalone/router-outlet'); + }) + + it('should render a as a child page of the router outlet', () => { + cy.ionPageVisible('ion-router-outlet app-router-outlet'); + }); +}) diff --git a/packages/angular/test/base/src/app/standalone/app-standalone/app.component.html b/packages/angular/test/base/src/app/standalone/app-standalone/app.component.html index 0680b43f9c6..15a92a22447 100644 --- a/packages/angular/test/base/src/app/standalone/app-standalone/app.component.html +++ b/packages/angular/test/base/src/app/standalone/app-standalone/app.component.html @@ -1 +1 @@ - + diff --git a/packages/angular/test/base/src/app/standalone/app-standalone/app.component.ts b/packages/angular/test/base/src/app/standalone/app-standalone/app.component.ts index b8ba6d5caa7..280de0d8c76 100644 --- a/packages/angular/test/base/src/app/standalone/app-standalone/app.component.ts +++ b/packages/angular/test/base/src/app/standalone/app-standalone/app.component.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core'; import { RouterModule } from '@angular/router'; - +import { IonRouterOutlet } from '@ionic/angular/standalone'; /** * This temporary code initialized Ionic and ensures components are visible. * TODO FW-4766 Can be removed when ticket is implemented @@ -15,7 +15,7 @@ document.querySelector('html')!.classList.add('ion-ce') selector: 'app-root-standalone', templateUrl: './app.component.html', standalone: true, - imports: [RouterModule] + imports: [RouterModule, IonRouterOutlet] }) export class AppComponent { } diff --git a/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts b/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts index 3182d3167a2..a453e1e46e3 100644 --- a/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts +++ b/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts @@ -7,6 +7,7 @@ export const routes: Routes = [ component: AppComponent, children: [ { path: 'test', loadComponent: () => import('../test/test.component').then(m => m.TestComponent) }, + { path: 'router-outlet', loadComponent: () => import('../router-outlet/router-outlet.component').then(m => m.RouterOutletComponent) }, ] }, ]; diff --git a/packages/angular/test/base/src/app/standalone/router-outlet/router-outlet.component.html b/packages/angular/test/base/src/app/standalone/router-outlet/router-outlet.component.html new file mode 100644 index 00000000000..83a1ae37758 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/router-outlet/router-outlet.component.html @@ -0,0 +1 @@ +This should be visible and rendered inside of an ion-router-outlet. diff --git a/packages/angular/test/base/src/app/standalone/router-outlet/router-outlet.component.ts b/packages/angular/test/base/src/app/standalone/router-outlet/router-outlet.component.ts new file mode 100644 index 00000000000..c28ea0e309d --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/router-outlet/router-outlet.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-router-outlet', + templateUrl: './router-outlet.component.html', + standalone: true +}) +export class RouterOutletComponent {} From 10648db776f1042aebfd6e98a78b7d26432019af Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Thu, 3 Aug 2023 10:35:19 -0400 Subject: [PATCH 20/23] lint --- packages/angular/standalone/src/navigation/router-outlet.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/angular/standalone/src/navigation/router-outlet.ts b/packages/angular/standalone/src/navigation/router-outlet.ts index 98bb16c1f27..2b750736e06 100644 --- a/packages/angular/standalone/src/navigation/router-outlet.ts +++ b/packages/angular/standalone/src/navigation/router-outlet.ts @@ -7,7 +7,7 @@ import { defineCustomElement } from '@ionic/core/components/ion-router-outlet.js }) @Directive({ selector: 'ion-router-outlet', - standalone: true + standalone: true, }) // eslint-disable-next-line @angular-eslint/directive-class-suffix export class IonRouterOutlet extends IonRouterOutletBase {} From 71b0c73bac8cf20870d3e977aaae10dd3bfb885c Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Thu, 3 Aug 2023 12:27:21 -0400 Subject: [PATCH 21/23] feat(angular): add standalone back button --- .../src/directives/navigation/back-button.ts | 29 +++++++++++++++---- packages/angular/common/src/index.ts | 2 +- .../directives/navigation/ion-back-button.ts | 10 +++---- packages/angular/standalone/src/index.ts | 1 + .../standalone/src/navigation/back-button.ts | 21 ++++++++++++++ 5 files changed, 52 insertions(+), 11 deletions(-) create mode 100644 packages/angular/standalone/src/navigation/back-button.ts diff --git a/packages/angular/common/src/directives/navigation/back-button.ts b/packages/angular/common/src/directives/navigation/back-button.ts index 8ddc9e14833..cac3add5a85 100644 --- a/packages/angular/common/src/directives/navigation/back-button.ts +++ b/packages/angular/common/src/directives/navigation/back-button.ts @@ -1,27 +1,46 @@ -import { Directive, HostListener, Input, Optional } from '@angular/core'; +import { + Directive, + HostListener, + Input, + Optional, + ElementRef, + NgZone +} from '@angular/core'; import type { AnimationBuilder } from '@ionic/core/components'; import { Config } from '../../providers/config'; import { NavController } from '../../providers/nav-controller'; - +import { ProxyCmp } from '../../utils/proxy'; import { IonRouterOutlet } from './router-outlet'; +const BACK_BUTTON_INPUTS = ['color', 'defaultHref', 'disabled', 'icon', 'mode', 'routerAnimation', 'text', 'type']; + +@ProxyCmp({ + inputs: BACK_BUTTON_INPUTS, +}) @Directive({ selector: 'ion-back-button', + inputs: BACK_BUTTON_INPUTS, }) // eslint-disable-next-line @angular-eslint/directive-class-suffix -export class IonBackButton { +export class IonBackButtonDelegate { @Input() defaultHref: string | undefined | null; @Input() routerAnimation?: AnimationBuilder; + protected el: HTMLElement; + constructor( @Optional() private routerOutlet: IonRouterOutlet, private navCtrl: NavController, - private config: Config - ) {} + private config: Config, + private r: ElementRef, + protected z: NgZone + ) { + this.el = this.r.nativeElement; + } /** * @internal diff --git a/packages/angular/common/src/index.ts b/packages/angular/common/src/index.ts index 7229a93a0d1..f3439baeb56 100644 --- a/packages/angular/common/src/index.ts +++ b/packages/angular/common/src/index.ts @@ -23,7 +23,7 @@ export { NavParams } from './directives/navigation/nav-params'; export { IonRouterOutlet, INPUT_BINDER, RoutedComponentInputBinder } from './directives/navigation/router-outlet'; export type { StackEvent } from './directives/navigation/stack-utils'; -export { IonBackButton } from './directives/navigation/back-button'; +export { IonBackButtonDelegate } from './directives/navigation/back-button'; export { RouterLinkDelegateDirective, RouterLinkWithHrefDelegateDirective, diff --git a/packages/angular/src/directives/navigation/ion-back-button.ts b/packages/angular/src/directives/navigation/ion-back-button.ts index fbf14d7fb5a..7d0001676ea 100644 --- a/packages/angular/src/directives/navigation/ion-back-button.ts +++ b/packages/angular/src/directives/navigation/ion-back-button.ts @@ -1,5 +1,5 @@ -import { Directive, Optional } from '@angular/core'; -import { IonBackButton as IonBackButtonBase, NavController, Config } from '@ionic/angular/common'; +import { Directive, Optional, ElementRef, NgZone } from '@angular/core'; +import { IonBackButtonDelegate as IonBackButtonDelegateBase, NavController, Config } from '@ionic/angular/common'; import { IonRouterOutlet } from './ion-router-outlet'; @@ -7,8 +7,8 @@ import { IonRouterOutlet } from './ion-router-outlet'; selector: 'ion-back-button', }) // eslint-disable-next-line @angular-eslint/directive-class-suffix -export class IonBackButtonDelegateDirective extends IonBackButtonBase { - constructor(@Optional() routerOutlet: IonRouterOutlet, navCtrl: NavController, config: Config) { - super(routerOutlet, navCtrl, config); +export class IonBackButtonDelegateDirective extends IonBackButtonDelegateBase { + constructor(@Optional() routerOutlet: IonRouterOutlet, navCtrl: NavController, config: Config, r: ElementRef, z: NgZone) { + super(routerOutlet, navCtrl, config, r, z); } } diff --git a/packages/angular/standalone/src/index.ts b/packages/angular/standalone/src/index.ts index f5fc4e8273d..9dcb40d1f94 100644 --- a/packages/angular/standalone/src/index.ts +++ b/packages/angular/standalone/src/index.ts @@ -1 +1,2 @@ +export { IonBackButton } from './navigation/back-button'; export { IonRouterOutlet } from './navigation/router-outlet'; diff --git a/packages/angular/standalone/src/navigation/back-button.ts b/packages/angular/standalone/src/navigation/back-button.ts new file mode 100644 index 00000000000..5606f8026c0 --- /dev/null +++ b/packages/angular/standalone/src/navigation/back-button.ts @@ -0,0 +1,21 @@ +import { Component, Optional, ChangeDetectionStrategy, ElementRef, NgZone } from '@angular/core'; +import { IonBackButtonDelegate as IonBackButtonDelegateBase, NavController, Config, ProxyCmp } from '@ionic/angular/common'; +import { defineCustomElement } from '@ionic/core/components/ion-back-button.js'; + +import { IonRouterOutlet } from './router-outlet'; + +@ProxyCmp({ + defineCustomElementFn: defineCustomElement, +}) +@Component({ + selector: 'ion-back-button', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + standalone: true, +}) +// eslint-disable-next-line @angular-eslint/directive-class-suffix +export class IonBackButton extends IonBackButtonDelegateBase { + constructor(@Optional() routerOutlet: IonRouterOutlet, navCtrl: NavController, config: Config, r: ElementRef, z: NgZone) { + super(routerOutlet, navCtrl, config, r, z); + } +} From c9ef2576e2d80357b7bf16b0a8c27c811c2ad9c5 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Thu, 3 Aug 2023 12:29:09 -0400 Subject: [PATCH 22/23] add test --- .../base/e2e/src/standalone/back-button.spec.ts | 14 ++++++++++++++ .../app/standalone/app-standalone/app.routes.ts | 1 + .../back-button/back-button.component.html | 1 + .../back-button/back-button.component.ts | 10 ++++++++++ 4 files changed, 26 insertions(+) create mode 100644 packages/angular/test/base/e2e/src/standalone/back-button.spec.ts create mode 100644 packages/angular/test/base/src/app/standalone/back-button/back-button.component.html create mode 100644 packages/angular/test/base/src/app/standalone/back-button/back-button.component.ts diff --git a/packages/angular/test/base/e2e/src/standalone/back-button.spec.ts b/packages/angular/test/base/e2e/src/standalone/back-button.spec.ts new file mode 100644 index 00000000000..7030dcdf5e5 --- /dev/null +++ b/packages/angular/test/base/e2e/src/standalone/back-button.spec.ts @@ -0,0 +1,14 @@ +describe('Back Button', () => { + beforeEach(() => { + cy.visit('/standalone/back-button'); + }) + + it('should be visible and navigate back to page', () => { + cy.ionPageVisible('app-back-button'); + + cy.get('ion-back-button').click(); + + cy.ionPageDoesNotExist('app-back-button'); + cy.ionPageVisible('app-router-outlet'); + }); +}) diff --git a/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts b/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts index a453e1e46e3..a045a0a69ae 100644 --- a/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts +++ b/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts @@ -8,6 +8,7 @@ export const routes: Routes = [ children: [ { path: 'test', loadComponent: () => import('../test/test.component').then(m => m.TestComponent) }, { path: 'router-outlet', loadComponent: () => import('../router-outlet/router-outlet.component').then(m => m.RouterOutletComponent) }, + { path: 'back-button', loadComponent: () => import('../back-button/back-button.component').then(m => m.BackButtonComponent) }, ] }, ]; diff --git a/packages/angular/test/base/src/app/standalone/back-button/back-button.component.html b/packages/angular/test/base/src/app/standalone/back-button/back-button.component.html new file mode 100644 index 00000000000..0224eb0fb0a --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/back-button/back-button.component.html @@ -0,0 +1 @@ + diff --git a/packages/angular/test/base/src/app/standalone/back-button/back-button.component.ts b/packages/angular/test/base/src/app/standalone/back-button/back-button.component.ts new file mode 100644 index 00000000000..7f9280d1d71 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/back-button/back-button.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { IonBackButton } from '@ionic/angular/standalone'; + +@Component({ + selector: 'app-back-button', + templateUrl: './back-button.component.html', + standalone: true, + imports: [IonBackButton] +}) +export class BackButtonComponent {} From 27870b8247a4d997d2952bb6f11f64382fbad78a Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Thu, 3 Aug 2023 12:32:59 -0400 Subject: [PATCH 23/23] lint --- .../src/directives/navigation/back-button.ts | 11 +++-------- .../src/directives/navigation/ion-back-button.ts | 8 +++++++- .../standalone/src/navigation/back-button.ts | 15 +++++++++++++-- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/packages/angular/common/src/directives/navigation/back-button.ts b/packages/angular/common/src/directives/navigation/back-button.ts index cac3add5a85..7ec7136a8c4 100644 --- a/packages/angular/common/src/directives/navigation/back-button.ts +++ b/packages/angular/common/src/directives/navigation/back-button.ts @@ -1,16 +1,10 @@ -import { - Directive, - HostListener, - Input, - Optional, - ElementRef, - NgZone -} from '@angular/core'; +import { Directive, HostListener, Input, Optional, ElementRef, NgZone } from '@angular/core'; import type { AnimationBuilder } from '@ionic/core/components'; import { Config } from '../../providers/config'; import { NavController } from '../../providers/nav-controller'; import { ProxyCmp } from '../../utils/proxy'; + import { IonRouterOutlet } from './router-outlet'; const BACK_BUTTON_INPUTS = ['color', 'defaultHref', 'disabled', 'icon', 'mode', 'routerAnimation', 'text', 'type']; @@ -20,6 +14,7 @@ const BACK_BUTTON_INPUTS = ['color', 'defaultHref', 'disabled', 'icon', 'mode', }) @Directive({ selector: 'ion-back-button', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property inputs: BACK_BUTTON_INPUTS, }) // eslint-disable-next-line @angular-eslint/directive-class-suffix diff --git a/packages/angular/src/directives/navigation/ion-back-button.ts b/packages/angular/src/directives/navigation/ion-back-button.ts index 7d0001676ea..309a5cf53c5 100644 --- a/packages/angular/src/directives/navigation/ion-back-button.ts +++ b/packages/angular/src/directives/navigation/ion-back-button.ts @@ -8,7 +8,13 @@ import { IonRouterOutlet } from './ion-router-outlet'; }) // eslint-disable-next-line @angular-eslint/directive-class-suffix export class IonBackButtonDelegateDirective extends IonBackButtonDelegateBase { - constructor(@Optional() routerOutlet: IonRouterOutlet, navCtrl: NavController, config: Config, r: ElementRef, z: NgZone) { + constructor( + @Optional() routerOutlet: IonRouterOutlet, + navCtrl: NavController, + config: Config, + r: ElementRef, + z: NgZone + ) { super(routerOutlet, navCtrl, config, r, z); } } diff --git a/packages/angular/standalone/src/navigation/back-button.ts b/packages/angular/standalone/src/navigation/back-button.ts index 5606f8026c0..d9e98711623 100644 --- a/packages/angular/standalone/src/navigation/back-button.ts +++ b/packages/angular/standalone/src/navigation/back-button.ts @@ -1,5 +1,10 @@ import { Component, Optional, ChangeDetectionStrategy, ElementRef, NgZone } from '@angular/core'; -import { IonBackButtonDelegate as IonBackButtonDelegateBase, NavController, Config, ProxyCmp } from '@ionic/angular/common'; +import { + IonBackButtonDelegate as IonBackButtonDelegateBase, + NavController, + Config, + ProxyCmp, +} from '@ionic/angular/common'; import { defineCustomElement } from '@ionic/core/components/ion-back-button.js'; import { IonRouterOutlet } from './router-outlet'; @@ -15,7 +20,13 @@ import { IonRouterOutlet } from './router-outlet'; }) // eslint-disable-next-line @angular-eslint/directive-class-suffix export class IonBackButton extends IonBackButtonDelegateBase { - constructor(@Optional() routerOutlet: IonRouterOutlet, navCtrl: NavController, config: Config, r: ElementRef, z: NgZone) { + constructor( + @Optional() routerOutlet: IonRouterOutlet, + navCtrl: NavController, + config: Config, + r: ElementRef, + z: NgZone + ) { super(routerOutlet, navCtrl, config, r, z); } }