From 122cdcc8253e46d9537105b11045fd7d9ccd8917 Mon Sep 17 00:00:00 2001 From: Sean Perkins Date: Tue, 7 Jun 2022 10:11:53 -0400 Subject: [PATCH] fix(angular): add support for Angular 14 (#25403) Resolves #25353 --- angular/src/di/r3_injector.ts | 34 +++++++++++++++ .../navigation/ion-router-outlet.ts | 41 ++++++++++++++++--- angular/src/providers/angular-delegate.ts | 36 ++++++++++++---- angular/src/providers/modal-controller.ts | 9 ++-- angular/src/providers/popover-controller.ts | 9 ++-- angular/src/util/util.ts | 6 +++ 6 files changed, 114 insertions(+), 21 deletions(-) create mode 100644 angular/src/di/r3_injector.ts diff --git a/angular/src/di/r3_injector.ts b/angular/src/di/r3_injector.ts new file mode 100644 index 0000000000..32f50d5356 --- /dev/null +++ b/angular/src/di/r3_injector.ts @@ -0,0 +1,34 @@ +/** + * This class is taken directly from Angular's codebase. It can be removed once + * we remove support for < Angular 14. The replacement class will come from @angular/core. + * + * TODO: FW-1641: Remove this class once Angular 13 support is dropped. + * + */ +import { Injector, ProviderToken, InjectFlags } from '@angular/core'; +/** + * An `Injector` that's part of the environment injector hierarchy, which exists outside of the + * component tree. + * + * @developerPreview + */ +export abstract class EnvironmentInjector implements Injector { + /** + * Retrieves an instance from the injector based on the provided token. + * @returns The instance from the injector if defined, otherwise the `notFoundValue`. + * @throws When the `notFoundValue` is `undefined` or `Injector.THROW_IF_NOT_FOUND`. + */ + abstract get(token: ProviderToken, notFoundValue?: T, flags?: InjectFlags): T; + /** + * @deprecated from v4.0.0 use ProviderToken + * @suppress {duplicate} + */ + abstract get(token: any, notFoundValue?: any): any; + + abstract destroy(): void; + + /** + * @internal + */ + abstract onDestroy(callback: () => void): void; +} diff --git a/angular/src/directives/navigation/ion-router-outlet.ts b/angular/src/directives/navigation/ion-router-outlet.ts index 241105af13..83e0eec013 100644 --- a/angular/src/directives/navigation/ion-router-outlet.ts +++ b/angular/src/directives/navigation/ion-router-outlet.ts @@ -20,9 +20,11 @@ import { componentOnReady } from '@ionic/core'; import { Observable, BehaviorSubject } from 'rxjs'; import { distinctUntilChanged, filter, switchMap } from 'rxjs/operators'; +import { EnvironmentInjector } from '../../di/r3_injector'; import { AnimationBuilder } from '../../ionic-core'; import { Config } from '../../providers/config'; import { NavController } from '../../providers/nav-controller'; +import { isComponentFactoryResolver } from '../../util/util'; import { StackController } from './stack-controller'; import { RouteView, getUrl } from './stack-utils'; @@ -82,11 +84,11 @@ export class IonRouterOutlet implements OnDestroy, OnInit { constructor( private parentContexts: ChildrenOutletContexts, private location: ViewContainerRef, - private resolver: ComponentFactoryResolver, @Attribute('name') name: string, @Optional() @Attribute('tabs') tabs: string, private config: Config, private navCtrl: NavController, + @Optional() private environmentInjector: EnvironmentInjector, commonLocation: Location, elementRef: ElementRef, router: Router, @@ -206,7 +208,10 @@ export class IonRouterOutlet implements OnDestroy, OnInit { } } - activateWith(activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver | null): void { + activateWith( + activatedRoute: ActivatedRoute, + resolverOrInjector?: ComponentFactoryResolver | EnvironmentInjector | null + ): void { if (this.isActivated) { throw new Error('Cannot activate an already activated outlet'); } @@ -229,9 +234,6 @@ export class IonRouterOutlet implements OnDestroy, OnInit { const snapshot = (activatedRoute as any)._futureSnapshot; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const component = snapshot.routeConfig!.component as any; - resolver = resolver || this.resolver; - - const factory = resolver.resolveComponentFactory(component); const childContexts = this.parentContexts.getOrCreateContext(this.name).children; // We create an activated route proxy object that will maintain future updates for this component @@ -240,8 +242,35 @@ export class IonRouterOutlet implements OnDestroy, OnInit { const activatedRouteProxy = this.createActivatedRouteProxy(component$, activatedRoute); const injector = new OutletInjector(activatedRouteProxy, childContexts, this.location.injector); - cmpRef = this.activated = this.location.createComponent(factory, this.location.length, injector); + if (resolverOrInjector && isComponentFactoryResolver(resolverOrInjector)) { + // Backwards compatibility for Angular 13 and lower + const factory = resolverOrInjector.resolveComponentFactory(component); + cmpRef = this.activated = this.location.createComponent(factory, this.location.length, injector); + } else { + /** + * Angular 14 and higher. + * + * TODO: FW-1641: Migrate once Angular 13 support is dropped. + * + * When we drop < Angular 14, we can replace the following code with: + * ```ts + const environmentInjector = resolverOrInjector ?? this.environmentInjector; + cmpRef = this.activated = location.createComponent(component, { + index: location.length, + injector, + environmentInjector, + }); + * ``` + * where `this.environmentInjector` is a provider of `EnvironmentInjector` from @angular/core. + */ + const environmentInjector = resolverOrInjector ?? this.environmentInjector; + cmpRef = this.activated = this.location.createComponent(component, { + index: this.location.length, + injector, + environmentInjector, + } as any); + } // Once the component is created we can push it to our local subject supplied to the proxy component$.next(cmpRef.instance); diff --git a/angular/src/providers/angular-delegate.ts b/angular/src/providers/angular-delegate.ts index 6bba41d65a..274d3b2257 100644 --- a/angular/src/providers/angular-delegate.ts +++ b/angular/src/providers/angular-delegate.ts @@ -6,6 +6,7 @@ import { Injectable, InjectionToken, Injector, + ComponentRef, } from '@angular/core'; import { FrameworkDelegate, @@ -16,18 +17,20 @@ import { LIFECYCLE_WILL_UNLOAD, } from '@ionic/core'; +import { EnvironmentInjector } from '../di/r3_injector'; import { NavParams } from '../directives/navigation/nav-params'; +import { isComponentFactoryResolver } from '../util/util'; @Injectable() export class AngularDelegate { constructor(private zone: NgZone, private appRef: ApplicationRef) {} create( - resolver: ComponentFactoryResolver, + resolverOrInjector: ComponentFactoryResolver, injector: Injector, location?: ViewContainerRef ): AngularFrameworkDelegate { - return new AngularFrameworkDelegate(resolver, injector, location, this.appRef, this.zone); + return new AngularFrameworkDelegate(resolverOrInjector, injector, location, this.appRef, this.zone); } } @@ -36,7 +39,7 @@ export class AngularFrameworkDelegate implements FrameworkDelegate { private elEventsMap = new WeakMap void>(); constructor( - private resolver: ComponentFactoryResolver, + private resolverOrInjector: ComponentFactoryResolver | EnvironmentInjector, private injector: Injector, private location: ViewContainerRef | undefined, private appRef: ApplicationRef, @@ -48,7 +51,7 @@ export class AngularFrameworkDelegate implements FrameworkDelegate { return new Promise((resolve) => { const el = attachView( this.zone, - this.resolver, + this.resolverOrInjector, this.injector, this.location, this.appRef, @@ -85,7 +88,7 @@ export class AngularFrameworkDelegate implements FrameworkDelegate { export const attachView = ( zone: NgZone, - resolver: ComponentFactoryResolver, + resolverOrInjector: ComponentFactoryResolver | EnvironmentInjector, injector: Injector, location: ViewContainerRef | undefined, appRef: ApplicationRef, @@ -96,14 +99,29 @@ export const attachView = ( params: any, cssClasses: string[] | undefined ): any => { - const factory = resolver.resolveComponentFactory(component); + let componentRef: ComponentRef; const childInjector = Injector.create({ providers: getProviders(params), parent: injector, }); - const componentRef = location - ? location.createComponent(factory, location.length, childInjector) - : factory.create(childInjector); + + if (resolverOrInjector && isComponentFactoryResolver(resolverOrInjector)) { + // Angular 13 and lower + const factory = resolverOrInjector.resolveComponentFactory(component); + componentRef = location + ? location.createComponent(factory, location.length, childInjector) + : factory.create(childInjector); + } else if (location) { + // Angular 14 + const environmentInjector = resolverOrInjector; + componentRef = location.createComponent(component, { + index: location.indexOf, + injector: childInjector, + environmentInjector, + } as any); + } else { + return null; + } const instance = componentRef.instance; const hostElement = componentRef.location.nativeElement; diff --git a/angular/src/providers/modal-controller.ts b/angular/src/providers/modal-controller.ts index 7acc4fefa6..6d91fe78b3 100644 --- a/angular/src/providers/modal-controller.ts +++ b/angular/src/providers/modal-controller.ts @@ -1,6 +1,7 @@ -import { ComponentFactoryResolver, Injector, Injectable } from '@angular/core'; +import { ComponentFactoryResolver, Injector, Injectable, Optional } from '@angular/core'; import { ModalOptions, modalController } from '@ionic/core'; +import { EnvironmentInjector } from '../di/r3_injector'; import { OverlayBaseController } from '../util/overlay'; import { AngularDelegate } from './angular-delegate'; @@ -10,7 +11,9 @@ export class ModalController extends OverlayBaseController { return super.create({ ...opts, - delegate: this.angularDelegate.create(this.resolver, this.injector), + delegate: this.angularDelegate.create(this.resolver ?? this.environmentInjector, this.injector), }); } } diff --git a/angular/src/providers/popover-controller.ts b/angular/src/providers/popover-controller.ts index fc7deefd0f..5a5857c9a3 100644 --- a/angular/src/providers/popover-controller.ts +++ b/angular/src/providers/popover-controller.ts @@ -1,6 +1,7 @@ -import { ComponentFactoryResolver, Injector, Injectable } from '@angular/core'; +import { ComponentFactoryResolver, Injector, Injectable, Optional } from '@angular/core'; import { PopoverOptions, popoverController } from '@ionic/core'; +import { EnvironmentInjector } from '../di/r3_injector'; import { OverlayBaseController } from '../util/overlay'; import { AngularDelegate } from './angular-delegate'; @@ -10,7 +11,9 @@ export class PopoverController extends OverlayBaseController { return super.create({ ...opts, - delegate: this.angularDelegate.create(this.resolver, this.injector), + delegate: this.angularDelegate.create(this.resolver ?? this.environmentInjector, this.injector), }); } } diff --git a/angular/src/util/util.ts b/angular/src/util/util.ts index 6f8b29c994..7c49eab59d 100644 --- a/angular/src/util/util.ts +++ b/angular/src/util/util.ts @@ -1,3 +1,5 @@ +import { ComponentFactoryResolver } from '@angular/core'; + declare const __zone_symbol__requestAnimationFrame: any; declare const requestAnimationFrame: any; @@ -10,3 +12,7 @@ export const raf = (h: any): any => { } return setTimeout(h); }; + +export const isComponentFactoryResolver = (item: any): item is ComponentFactoryResolver => { + return !!item.resolveComponentFactory; +};