Skip to content

Commit

Permalink
fix(angular): add support for Angular 14 (#25403)
Browse files Browse the repository at this point in the history
Resolves #25353
  • Loading branch information
sean-perkins committed Jun 7, 2022
1 parent 0b275af commit 122cdcc
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 21 deletions.
34 changes: 34 additions & 0 deletions angular/src/di/r3_injector.ts
Original file line number Diff line number Diff line change
@@ -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;
}
41 changes: 35 additions & 6 deletions angular/src/directives/navigation/ion-router-outlet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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');
}
Expand All @@ -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
Expand All @@ -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);

Expand Down
36 changes: 27 additions & 9 deletions angular/src/providers/angular-delegate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Injectable,
InjectionToken,
Injector,
ComponentRef,
} from '@angular/core';
import {
FrameworkDelegate,
Expand All @@ -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);
}
}

Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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
? 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;
Expand Down
9 changes: 6 additions & 3 deletions angular/src/providers/modal-controller.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -10,15 +11,17 @@ 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);
}

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),
});
}
}
9 changes: 6 additions & 3 deletions angular/src/providers/popover-controller.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -10,15 +11,17 @@ 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);
}

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),
});
}
}
6 changes: 6 additions & 0 deletions angular/src/util/util.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ComponentFactoryResolver } from '@angular/core';

declare const __zone_symbol__requestAnimationFrame: any;
declare const requestAnimationFrame: any;

Expand All @@ -10,3 +12,7 @@ export const raf = (h: any): any => {
}
return setTimeout(h);
};

export const isComponentFactoryResolver = (item: any): item is ComponentFactoryResolver => {
return !!item.resolveComponentFactory;
};

0 comments on commit 122cdcc

Please sign in to comment.