fix(angular): add support for Angular 14 (#25403)

Resolves #25353
This commit is contained in:
Sean Perkins
2022-06-07 10:11:53 -04:00
committed by GitHub
parent 0b275af5ac
commit 122cdcc825
6 changed files with 114 additions and 21 deletions

View File

@ -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<T>(token: ProviderToken<T>, notFoundValue?: T, flags?: InjectFlags): T;
/**
* @deprecated from v4.0.0 use ProviderToken<T>
* @suppress {duplicate}
*/
abstract get(token: any, notFoundValue?: any): any;
abstract destroy(): void;
/**
* @internal
*/
abstract onDestroy(callback: () => void): void;
}

View File

@ -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);

View File

@ -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<HTMLElement, () => 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<any>;
const childInjector = Injector.create({
providers: getProviders(params),
parent: injector,
});
const componentRef = location
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;

View File

@ -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<ModalOptions, HTMLIon
constructor(
private angularDelegate: AngularDelegate,
private resolver: ComponentFactoryResolver,
private injector: Injector
private injector: Injector,
// TODO: FW-1641: Migrate to Angular's version once Angular 13 is dropped
@Optional() private environmentInjector: EnvironmentInjector
) {
super(modalController);
}
@ -18,7 +21,7 @@ export class ModalController extends OverlayBaseController<ModalOptions, HTMLIon
create(opts: ModalOptions): Promise<HTMLIonModalElement> {
return super.create({
...opts,
delegate: this.angularDelegate.create(this.resolver, this.injector),
delegate: this.angularDelegate.create(this.resolver ?? this.environmentInjector, this.injector),
});
}
}

View File

@ -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<PopoverOptions, HTM
constructor(
private angularDelegate: AngularDelegate,
private resolver: ComponentFactoryResolver,
private injector: Injector
private injector: Injector,
// TODO: FW-1641: Migrate to Angular's version once Angular 13 is dropped
@Optional() private environmentInjector: EnvironmentInjector
) {
super(popoverController);
}
@ -18,7 +21,7 @@ export class PopoverController extends OverlayBaseController<PopoverOptions, HTM
create(opts: PopoverOptions): Promise<HTMLIonPopoverElement> {
return super.create({
...opts,
delegate: this.angularDelegate.create(this.resolver, this.injector),
delegate: this.angularDelegate.create(this.resolver ?? this.environmentInjector, this.injector),
});
}
}

View File

@ -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;
};