fix(upgrade): ensure downgraded components are created in the Angular zone (#18209)
PR Close #18209
This commit is contained in:

committed by
Miško Hevery

parent
6d7799fce9
commit
43c33d5663
@ -116,7 +116,7 @@ export interface ICloneAttachFunction {
|
||||
(clonedElement?: IAugmentedJQuery, scope?: IScope): any;
|
||||
}
|
||||
export type IAugmentedJQuery = Node[] & {
|
||||
bind?: (name: string, fn: () => void) => void;
|
||||
on?: (name: string, fn: () => void) => void;
|
||||
data?: (name: string, value?: any) => any;
|
||||
text?: () => string;
|
||||
inheritedData?: (name: string, value?: any) => any;
|
||||
|
@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ComponentFactory, ComponentFactoryResolver, Injector, Type} from '@angular/core';
|
||||
import {ComponentFactory, ComponentFactoryResolver, Injector, NgZone, Type} from '@angular/core';
|
||||
|
||||
import * as angular from './angular1';
|
||||
import {$COMPILE, $INJECTOR, $PARSE, INJECTOR_KEY, LAZY_MODULE_REF, REQUIRE_INJECTOR, REQUIRE_NG_MODEL} from './constants';
|
||||
@ -72,6 +72,14 @@ export function downgradeComponent(info: {
|
||||
$compile: angular.ICompileService,
|
||||
$injector: angular.IInjectorService,
|
||||
$parse: angular.IParseService): angular.IDirective {
|
||||
// When using `UpgradeModule`, we don't need to ensure callbacks to Angular APIs (e.g. change
|
||||
// detection) are run inside the Angular zone, because `$digest()` will be run inside the zone
|
||||
// (except if explicitly escaped, in which case we shouldn't force it back in).
|
||||
// When using `downgradeModule()` though, we need to ensure such callbacks are run inside the
|
||||
// Angular zone.
|
||||
let needsNgZone = false;
|
||||
let wrapCallback = <T>(cb: () => T) => cb;
|
||||
let ngZone: NgZone;
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
@ -89,10 +97,11 @@ export function downgradeComponent(info: {
|
||||
|
||||
if (!parentInjector) {
|
||||
const lazyModuleRef = $injector.get(LAZY_MODULE_REF) as LazyModuleRef;
|
||||
parentInjector = lazyModuleRef.injector || lazyModuleRef.promise;
|
||||
needsNgZone = lazyModuleRef.needsNgZone;
|
||||
parentInjector = lazyModuleRef.injector || lazyModuleRef.promise as Promise<Injector>;
|
||||
}
|
||||
|
||||
const downgradeFn = (injector: Injector) => {
|
||||
const doDowngrade = (injector: Injector) => {
|
||||
const componentFactoryResolver: ComponentFactoryResolver =
|
||||
injector.get(ComponentFactoryResolver);
|
||||
const componentFactory: ComponentFactory<any> =
|
||||
@ -106,13 +115,13 @@ export function downgradeComponent(info: {
|
||||
const injectorPromise = new ParentInjectorPromise(element);
|
||||
const facade = new DowngradeComponentAdapter(
|
||||
id, element, attrs, scope, ngModel, injector, $injector, $compile, $parse,
|
||||
componentFactory);
|
||||
componentFactory, wrapCallback);
|
||||
|
||||
const projectableNodes = facade.compileContents();
|
||||
facade.createComponent(projectableNodes);
|
||||
facade.setupInputs(info.propagateDigest);
|
||||
facade.setupInputs(needsNgZone, info.propagateDigest);
|
||||
facade.setupOutputs();
|
||||
facade.registerCleanup();
|
||||
facade.registerCleanup(needsNgZone);
|
||||
|
||||
injectorPromise.resolve(facade.getInjector());
|
||||
|
||||
@ -123,6 +132,16 @@ export function downgradeComponent(info: {
|
||||
}
|
||||
};
|
||||
|
||||
const downgradeFn = !needsNgZone ? doDowngrade : (injector: Injector) => {
|
||||
if (!ngZone) {
|
||||
ngZone = injector.get(NgZone);
|
||||
wrapCallback = <T>(cb: () => T) => () =>
|
||||
NgZone.isInAngularZone() ? cb() : ngZone.run(cb);
|
||||
}
|
||||
|
||||
wrapCallback(() => doDowngrade(injector))();
|
||||
};
|
||||
|
||||
if (isThenable<Injector>(parentInjector)) {
|
||||
parentInjector.then(downgradeFn);
|
||||
} else {
|
||||
|
@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ChangeDetectorRef, ComponentFactory, ComponentRef, EventEmitter, Injector, OnChanges, ReflectiveInjector, SimpleChange, SimpleChanges, Type} from '@angular/core';
|
||||
import {ApplicationRef, ChangeDetectorRef, ComponentFactory, ComponentRef, EventEmitter, Injector, OnChanges, ReflectiveInjector, SimpleChange, SimpleChanges, Type} from '@angular/core';
|
||||
|
||||
import * as angular from './angular1';
|
||||
import {PropertyBinding} from './component_info';
|
||||
@ -22,18 +22,21 @@ export class DowngradeComponentAdapter {
|
||||
private inputChangeCount: number = 0;
|
||||
private inputChanges: SimpleChanges = {};
|
||||
private componentScope: angular.IScope;
|
||||
private componentRef: ComponentRef<any>|null = null;
|
||||
private component: any = null;
|
||||
private changeDetector: ChangeDetectorRef|null = null;
|
||||
private componentRef: ComponentRef<any>;
|
||||
private component: any;
|
||||
private changeDetector: ChangeDetectorRef;
|
||||
private appRef: ApplicationRef;
|
||||
|
||||
constructor(
|
||||
private id: string, private element: angular.IAugmentedJQuery,
|
||||
private attrs: angular.IAttributes, private scope: angular.IScope,
|
||||
private ngModel: angular.INgModelController, private parentInjector: Injector,
|
||||
private $injector: angular.IInjectorService, private $compile: angular.ICompileService,
|
||||
private $parse: angular.IParseService, private componentFactory: ComponentFactory<any>) {
|
||||
private $parse: angular.IParseService, private componentFactory: ComponentFactory<any>,
|
||||
private wrapCallback: <T>(cb: () => T) => () => T) {
|
||||
(this.element[0] as any).id = id;
|
||||
this.componentScope = scope.$new();
|
||||
this.appRef = parentInjector.get(ApplicationRef);
|
||||
}
|
||||
|
||||
compileContents(): Node[][] {
|
||||
@ -65,7 +68,7 @@ export class DowngradeComponentAdapter {
|
||||
hookupNgModel(this.ngModel, this.component);
|
||||
}
|
||||
|
||||
setupInputs(propagateDigest = true): void {
|
||||
setupInputs(needsNgZone: boolean, propagateDigest = true): void {
|
||||
const attrs = this.attrs;
|
||||
const inputs = this.componentFactory.inputs || [];
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
@ -116,11 +119,11 @@ export class DowngradeComponentAdapter {
|
||||
}
|
||||
|
||||
// Invoke `ngOnChanges()` and Change Detection (when necessary)
|
||||
const detectChanges = () => this.changeDetector && this.changeDetector.detectChanges();
|
||||
const detectChanges = () => this.changeDetector.detectChanges();
|
||||
const prototype = this.componentFactory.componentType.prototype;
|
||||
this.implementsOnChanges = !!(prototype && (<OnChanges>prototype).ngOnChanges);
|
||||
|
||||
this.componentScope.$watch(() => this.inputChangeCount, () => {
|
||||
this.componentScope.$watch(() => this.inputChangeCount, this.wrapCallback(() => {
|
||||
// Invoke `ngOnChanges()`
|
||||
if (this.implementsOnChanges) {
|
||||
const inputChanges = this.inputChanges;
|
||||
@ -128,15 +131,21 @@ export class DowngradeComponentAdapter {
|
||||
(<OnChanges>this.component).ngOnChanges(inputChanges !);
|
||||
}
|
||||
|
||||
// If opted out of propagating digests, invoke change detection when inputs change
|
||||
// If opted out of propagating digests, invoke change detection
|
||||
// when inputs change
|
||||
if (!propagateDigest) {
|
||||
detectChanges();
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
// If not opted out of propagating digests, invoke change detection on every digest
|
||||
if (propagateDigest) {
|
||||
this.componentScope.$watch(detectChanges);
|
||||
this.componentScope.$watch(this.wrapCallback(detectChanges));
|
||||
}
|
||||
|
||||
// Attach the view so that it will be dirty-checked.
|
||||
if (needsNgZone) {
|
||||
this.appRef.attachView(this.componentRef.hostView);
|
||||
}
|
||||
}
|
||||
|
||||
@ -184,14 +193,17 @@ export class DowngradeComponentAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
registerCleanup() {
|
||||
this.element.bind !('$destroy', () => {
|
||||
registerCleanup(needsNgZone: boolean) {
|
||||
this.element.on !('$destroy', () => {
|
||||
this.componentScope.$destroy();
|
||||
this.componentRef !.destroy();
|
||||
this.componentRef.destroy();
|
||||
if (needsNgZone) {
|
||||
this.appRef.detachView(this.componentRef.hostView);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getInjector(): Injector { return this.componentRef ! && this.componentRef !.injector; }
|
||||
getInjector(): Injector { return this.componentRef.injector; }
|
||||
|
||||
private updateInput(prop: string, prevValue: any, currValue: any) {
|
||||
if (this.implementsOnChanges) {
|
||||
|
@ -68,8 +68,11 @@ export class Deferred<R> {
|
||||
}
|
||||
|
||||
export interface LazyModuleRef {
|
||||
// Whether the AngularJS app has been bootstrapped outside the Angular zone
|
||||
// (in which case calls to Angular APIs need to be brought back in).
|
||||
needsNgZone: boolean;
|
||||
injector?: Injector;
|
||||
promise: Promise<Injector>;
|
||||
promise?: Promise<Injector>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -497,10 +497,7 @@ export class UpgradeAdapter {
|
||||
ng1Module.factory(INJECTOR_KEY, () => this.moduleRef !.injector.get(Injector))
|
||||
.factory(
|
||||
LAZY_MODULE_REF,
|
||||
[
|
||||
INJECTOR_KEY,
|
||||
(injector: Injector) => ({injector, promise: Promise.resolve(injector)})
|
||||
])
|
||||
[INJECTOR_KEY, (injector: Injector) => ({injector, needsInNgZone: false})])
|
||||
.constant(NG_ZONE_KEY, this.ngZone)
|
||||
.factory(COMPILER_KEY, () => this.moduleRef !.injector.get(Compiler))
|
||||
.config([
|
||||
|
@ -35,7 +35,8 @@ export function downgradeModule<T>(
|
||||
INJECTOR_KEY,
|
||||
() => {
|
||||
if (!injector) {
|
||||
throw new Error('The Angular module has not been bootstrapped yet.');
|
||||
throw new Error(
|
||||
'Trying to get the Angular injector before bootstrapping an Angular module.');
|
||||
}
|
||||
return injector;
|
||||
})
|
||||
@ -44,6 +45,7 @@ export function downgradeModule<T>(
|
||||
($injector: angular.IInjectorService) => {
|
||||
setTempInjectorRef($injector);
|
||||
const result: LazyModuleRef = {
|
||||
needsNgZone: true,
|
||||
promise: bootstrapFn(angular1Providers).then(ref => {
|
||||
injector = result.injector = new NgAdapterInjector(ref.injector);
|
||||
injector.get($INJECTOR);
|
||||
|
@ -166,10 +166,7 @@ export class UpgradeModule {
|
||||
|
||||
.factory(
|
||||
LAZY_MODULE_REF,
|
||||
[
|
||||
INJECTOR_KEY,
|
||||
(injector: Injector) => ({injector, promise: Promise.resolve(injector)})
|
||||
])
|
||||
[INJECTOR_KEY, (injector: Injector) => ({injector, needsNgZone: false})])
|
||||
|
||||
.config([
|
||||
$PROVIDE, $INJECTOR,
|
||||
|
Reference in New Issue
Block a user