fix(upgrade): ensure downgraded components are created in the Angular zone (#18209)
PR Close #18209
This commit is contained in:
parent
6d7799fce9
commit
43c33d5663
|
@ -116,7 +116,7 @@ export interface ICloneAttachFunction {
|
||||||
(clonedElement?: IAugmentedJQuery, scope?: IScope): any;
|
(clonedElement?: IAugmentedJQuery, scope?: IScope): any;
|
||||||
}
|
}
|
||||||
export type IAugmentedJQuery = Node[] & {
|
export type IAugmentedJQuery = Node[] & {
|
||||||
bind?: (name: string, fn: () => void) => void;
|
on?: (name: string, fn: () => void) => void;
|
||||||
data?: (name: string, value?: any) => any;
|
data?: (name: string, value?: any) => any;
|
||||||
text?: () => string;
|
text?: () => string;
|
||||||
inheritedData?: (name: string, value?: any) => any;
|
inheritedData?: (name: string, value?: any) => any;
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* 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 * as angular from './angular1';
|
||||||
import {$COMPILE, $INJECTOR, $PARSE, INJECTOR_KEY, LAZY_MODULE_REF, REQUIRE_INJECTOR, REQUIRE_NG_MODEL} from './constants';
|
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,
|
$compile: angular.ICompileService,
|
||||||
$injector: angular.IInjectorService,
|
$injector: angular.IInjectorService,
|
||||||
$parse: angular.IParseService): angular.IDirective {
|
$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 {
|
return {
|
||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
|
@ -89,10 +97,11 @@ export function downgradeComponent(info: {
|
||||||
|
|
||||||
if (!parentInjector) {
|
if (!parentInjector) {
|
||||||
const lazyModuleRef = $injector.get(LAZY_MODULE_REF) as LazyModuleRef;
|
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 =
|
const componentFactoryResolver: ComponentFactoryResolver =
|
||||||
injector.get(ComponentFactoryResolver);
|
injector.get(ComponentFactoryResolver);
|
||||||
const componentFactory: ComponentFactory<any> =
|
const componentFactory: ComponentFactory<any> =
|
||||||
|
@ -106,13 +115,13 @@ export function downgradeComponent(info: {
|
||||||
const injectorPromise = new ParentInjectorPromise(element);
|
const injectorPromise = new ParentInjectorPromise(element);
|
||||||
const facade = new DowngradeComponentAdapter(
|
const facade = new DowngradeComponentAdapter(
|
||||||
id, element, attrs, scope, ngModel, injector, $injector, $compile, $parse,
|
id, element, attrs, scope, ngModel, injector, $injector, $compile, $parse,
|
||||||
componentFactory);
|
componentFactory, wrapCallback);
|
||||||
|
|
||||||
const projectableNodes = facade.compileContents();
|
const projectableNodes = facade.compileContents();
|
||||||
facade.createComponent(projectableNodes);
|
facade.createComponent(projectableNodes);
|
||||||
facade.setupInputs(info.propagateDigest);
|
facade.setupInputs(needsNgZone, info.propagateDigest);
|
||||||
facade.setupOutputs();
|
facade.setupOutputs();
|
||||||
facade.registerCleanup();
|
facade.registerCleanup(needsNgZone);
|
||||||
|
|
||||||
injectorPromise.resolve(facade.getInjector());
|
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)) {
|
if (isThenable<Injector>(parentInjector)) {
|
||||||
parentInjector.then(downgradeFn);
|
parentInjector.then(downgradeFn);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* 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 * as angular from './angular1';
|
||||||
import {PropertyBinding} from './component_info';
|
import {PropertyBinding} from './component_info';
|
||||||
|
@ -22,18 +22,21 @@ export class DowngradeComponentAdapter {
|
||||||
private inputChangeCount: number = 0;
|
private inputChangeCount: number = 0;
|
||||||
private inputChanges: SimpleChanges = {};
|
private inputChanges: SimpleChanges = {};
|
||||||
private componentScope: angular.IScope;
|
private componentScope: angular.IScope;
|
||||||
private componentRef: ComponentRef<any>|null = null;
|
private componentRef: ComponentRef<any>;
|
||||||
private component: any = null;
|
private component: any;
|
||||||
private changeDetector: ChangeDetectorRef|null = null;
|
private changeDetector: ChangeDetectorRef;
|
||||||
|
private appRef: ApplicationRef;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private id: string, private element: angular.IAugmentedJQuery,
|
private id: string, private element: angular.IAugmentedJQuery,
|
||||||
private attrs: angular.IAttributes, private scope: angular.IScope,
|
private attrs: angular.IAttributes, private scope: angular.IScope,
|
||||||
private ngModel: angular.INgModelController, private parentInjector: Injector,
|
private ngModel: angular.INgModelController, private parentInjector: Injector,
|
||||||
private $injector: angular.IInjectorService, private $compile: angular.ICompileService,
|
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.element[0] as any).id = id;
|
||||||
this.componentScope = scope.$new();
|
this.componentScope = scope.$new();
|
||||||
|
this.appRef = parentInjector.get(ApplicationRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
compileContents(): Node[][] {
|
compileContents(): Node[][] {
|
||||||
|
@ -65,7 +68,7 @@ export class DowngradeComponentAdapter {
|
||||||
hookupNgModel(this.ngModel, this.component);
|
hookupNgModel(this.ngModel, this.component);
|
||||||
}
|
}
|
||||||
|
|
||||||
setupInputs(propagateDigest = true): void {
|
setupInputs(needsNgZone: boolean, propagateDigest = true): void {
|
||||||
const attrs = this.attrs;
|
const attrs = this.attrs;
|
||||||
const inputs = this.componentFactory.inputs || [];
|
const inputs = this.componentFactory.inputs || [];
|
||||||
for (let i = 0; i < inputs.length; i++) {
|
for (let i = 0; i < inputs.length; i++) {
|
||||||
|
@ -116,11 +119,11 @@ export class DowngradeComponentAdapter {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invoke `ngOnChanges()` and Change Detection (when necessary)
|
// 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;
|
const prototype = this.componentFactory.componentType.prototype;
|
||||||
this.implementsOnChanges = !!(prototype && (<OnChanges>prototype).ngOnChanges);
|
this.implementsOnChanges = !!(prototype && (<OnChanges>prototype).ngOnChanges);
|
||||||
|
|
||||||
this.componentScope.$watch(() => this.inputChangeCount, () => {
|
this.componentScope.$watch(() => this.inputChangeCount, this.wrapCallback(() => {
|
||||||
// Invoke `ngOnChanges()`
|
// Invoke `ngOnChanges()`
|
||||||
if (this.implementsOnChanges) {
|
if (this.implementsOnChanges) {
|
||||||
const inputChanges = this.inputChanges;
|
const inputChanges = this.inputChanges;
|
||||||
|
@ -128,15 +131,21 @@ export class DowngradeComponentAdapter {
|
||||||
(<OnChanges>this.component).ngOnChanges(inputChanges !);
|
(<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) {
|
if (!propagateDigest) {
|
||||||
detectChanges();
|
detectChanges();
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
|
|
||||||
// If not opted out of propagating digests, invoke change detection on every digest
|
// If not opted out of propagating digests, invoke change detection on every digest
|
||||||
if (propagateDigest) {
|
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() {
|
registerCleanup(needsNgZone: boolean) {
|
||||||
this.element.bind !('$destroy', () => {
|
this.element.on !('$destroy', () => {
|
||||||
this.componentScope.$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) {
|
private updateInput(prop: string, prevValue: any, currValue: any) {
|
||||||
if (this.implementsOnChanges) {
|
if (this.implementsOnChanges) {
|
||||||
|
|
|
@ -68,8 +68,11 @@ export class Deferred<R> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LazyModuleRef {
|
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;
|
injector?: Injector;
|
||||||
promise: Promise<Injector>;
|
promise?: Promise<Injector>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -497,10 +497,7 @@ export class UpgradeAdapter {
|
||||||
ng1Module.factory(INJECTOR_KEY, () => this.moduleRef !.injector.get(Injector))
|
ng1Module.factory(INJECTOR_KEY, () => this.moduleRef !.injector.get(Injector))
|
||||||
.factory(
|
.factory(
|
||||||
LAZY_MODULE_REF,
|
LAZY_MODULE_REF,
|
||||||
[
|
[INJECTOR_KEY, (injector: Injector) => ({injector, needsInNgZone: false})])
|
||||||
INJECTOR_KEY,
|
|
||||||
(injector: Injector) => ({injector, promise: Promise.resolve(injector)})
|
|
||||||
])
|
|
||||||
.constant(NG_ZONE_KEY, this.ngZone)
|
.constant(NG_ZONE_KEY, this.ngZone)
|
||||||
.factory(COMPILER_KEY, () => this.moduleRef !.injector.get(Compiler))
|
.factory(COMPILER_KEY, () => this.moduleRef !.injector.get(Compiler))
|
||||||
.config([
|
.config([
|
||||||
|
|
|
@ -35,7 +35,8 @@ export function downgradeModule<T>(
|
||||||
INJECTOR_KEY,
|
INJECTOR_KEY,
|
||||||
() => {
|
() => {
|
||||||
if (!injector) {
|
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;
|
return injector;
|
||||||
})
|
})
|
||||||
|
@ -44,6 +45,7 @@ export function downgradeModule<T>(
|
||||||
($injector: angular.IInjectorService) => {
|
($injector: angular.IInjectorService) => {
|
||||||
setTempInjectorRef($injector);
|
setTempInjectorRef($injector);
|
||||||
const result: LazyModuleRef = {
|
const result: LazyModuleRef = {
|
||||||
|
needsNgZone: true,
|
||||||
promise: bootstrapFn(angular1Providers).then(ref => {
|
promise: bootstrapFn(angular1Providers).then(ref => {
|
||||||
injector = result.injector = new NgAdapterInjector(ref.injector);
|
injector = result.injector = new NgAdapterInjector(ref.injector);
|
||||||
injector.get($INJECTOR);
|
injector.get($INJECTOR);
|
||||||
|
|
|
@ -166,10 +166,7 @@ export class UpgradeModule {
|
||||||
|
|
||||||
.factory(
|
.factory(
|
||||||
LAZY_MODULE_REF,
|
LAZY_MODULE_REF,
|
||||||
[
|
[INJECTOR_KEY, (injector: Injector) => ({injector, needsNgZone: false})])
|
||||||
INJECTOR_KEY,
|
|
||||||
(injector: Injector) => ({injector, promise: Promise.resolve(injector)})
|
|
||||||
])
|
|
||||||
|
|
||||||
.config([
|
.config([
|
||||||
$PROVIDE, $INJECTOR,
|
$PROVIDE, $INJECTOR,
|
||||||
|
|
|
@ -6,167 +6,367 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Component, Inject, Injector, Input, NgModule, Provider, destroyPlatform} from '@angular/core';
|
import {Component, Inject, Injector, Input, NgModule, NgZone, OnChanges, Provider, destroyPlatform} from '@angular/core';
|
||||||
import {async} from '@angular/core/testing';
|
import {async, fakeAsync, tick} from '@angular/core/testing';
|
||||||
import {BrowserModule} from '@angular/platform-browser';
|
import {BrowserModule} from '@angular/platform-browser';
|
||||||
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
||||||
import * as angular from '@angular/upgrade/src/common/angular1';
|
import * as angular from '@angular/upgrade/src/common/angular1';
|
||||||
import {$ROOT_SCOPE, INJECTOR_KEY} from '@angular/upgrade/src/common/constants';
|
import {$ROOT_SCOPE, INJECTOR_KEY, LAZY_MODULE_REF} from '@angular/upgrade/src/common/constants';
|
||||||
|
import {LazyModuleRef} from '@angular/upgrade/src/common/util';
|
||||||
import {downgradeComponent, downgradeModule} from '@angular/upgrade/static';
|
import {downgradeComponent, downgradeModule} from '@angular/upgrade/static';
|
||||||
|
|
||||||
import {html} from '../test_helpers';
|
import {html} from '../test_helpers';
|
||||||
|
|
||||||
|
|
||||||
export function main() {
|
export function main() {
|
||||||
describe('lazy-load ng2 module', () => {
|
[true, false].forEach(propagateDigest => {
|
||||||
|
describe(`lazy-load ng2 module (propagateDigest: ${propagateDigest})`, () => {
|
||||||
|
|
||||||
beforeEach(() => destroyPlatform());
|
beforeEach(() => destroyPlatform());
|
||||||
afterEach(() => destroyPlatform());
|
|
||||||
|
|
||||||
it('should support downgrading a component and propagate inputs', async(() => {
|
it('should support downgrading a component and propagate inputs', async(() => {
|
||||||
@Component({selector: 'ng2A', template: 'a({{ value }}) | <ng2B [value]="value"></ng2B>'})
|
@Component(
|
||||||
class Ng2AComponent {
|
{selector: 'ng2A', template: 'a({{ value }}) | <ng2B [value]="value"></ng2B>'})
|
||||||
@Input() value = -1;
|
class Ng2AComponent {
|
||||||
}
|
@Input() value = -1;
|
||||||
|
|
||||||
@Component({selector: 'ng2B', template: 'b({{ value }})'})
|
|
||||||
class Ng2BComponent {
|
|
||||||
@Input() value = -2;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [Ng2AComponent, Ng2BComponent],
|
|
||||||
entryComponents: [Ng2AComponent],
|
|
||||||
imports: [BrowserModule],
|
|
||||||
})
|
|
||||||
class Ng2Module {
|
|
||||||
ngDoBootstrap() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const bootstrapFn = (extraProviders: Provider[]) =>
|
|
||||||
platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module);
|
|
||||||
const lazyModuleName = downgradeModule<Ng2Module>(bootstrapFn);
|
|
||||||
const ng1Module =
|
|
||||||
angular.module('ng1', [lazyModuleName])
|
|
||||||
.directive(
|
|
||||||
'ng2', downgradeComponent({component: Ng2AComponent, propagateDigest: false}))
|
|
||||||
.run(($rootScope: angular.IRootScopeService) => $rootScope.value = 0);
|
|
||||||
|
|
||||||
const element = html('<div><ng2 [value]="value" ng-if="loadNg2"></ng2></div>');
|
|
||||||
const $injector = angular.bootstrap(element, [ng1Module.name]);
|
|
||||||
const $rootScope = $injector.get($ROOT_SCOPE) as angular.IRootScopeService;
|
|
||||||
|
|
||||||
expect(element.textContent).toBe('');
|
|
||||||
expect(() => $injector.get(INJECTOR_KEY)).toThrowError();
|
|
||||||
|
|
||||||
$rootScope.$apply('value = 1');
|
|
||||||
expect(element.textContent).toBe('');
|
|
||||||
expect(() => $injector.get(INJECTOR_KEY)).toThrowError();
|
|
||||||
|
|
||||||
$rootScope.$apply('loadNg2 = true');
|
|
||||||
expect(element.textContent).toBe('');
|
|
||||||
expect(() => $injector.get(INJECTOR_KEY)).toThrowError();
|
|
||||||
|
|
||||||
// Wait for the module to be bootstrapped.
|
|
||||||
setTimeout(() => {
|
|
||||||
expect(() => $injector.get(INJECTOR_KEY)).not.toThrow();
|
|
||||||
|
|
||||||
// Wait for `$evalAsync()` to propagate inputs.
|
|
||||||
setTimeout(() => expect(element.textContent).toBe('a(1) | b(1)'));
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should support using an upgraded service', async(() => {
|
|
||||||
class Ng2Service {
|
|
||||||
constructor(@Inject('ng1Value') private ng1Value: string) {}
|
|
||||||
getValue = () => `${this.ng1Value}-bar`;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({selector: 'ng2', template: '{{ value }}'})
|
|
||||||
class Ng2Component {
|
|
||||||
value: string;
|
|
||||||
constructor(ng2Service: Ng2Service) { this.value = ng2Service.getValue(); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [Ng2Component],
|
|
||||||
entryComponents: [Ng2Component],
|
|
||||||
imports: [BrowserModule],
|
|
||||||
providers: [
|
|
||||||
Ng2Service,
|
|
||||||
{
|
|
||||||
provide: 'ng1Value',
|
|
||||||
useFactory: (i: angular.IInjectorService) => i.get('ng1Value'),
|
|
||||||
deps: ['$injector'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
class Ng2Module {
|
|
||||||
ngDoBootstrap() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const bootstrapFn = (extraProviders: Provider[]) =>
|
|
||||||
platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module);
|
|
||||||
const lazyModuleName = downgradeModule<Ng2Module>(bootstrapFn);
|
|
||||||
const ng1Module =
|
|
||||||
angular.module('ng1', [lazyModuleName])
|
|
||||||
.directive(
|
|
||||||
'ng2', downgradeComponent({component: Ng2Component, propagateDigest: false}))
|
|
||||||
.value('ng1Value', 'foo');
|
|
||||||
|
|
||||||
const element = html('<div><ng2 ng-if="loadNg2"></ng2></div>');
|
|
||||||
const $injector = angular.bootstrap(element, [ng1Module.name]);
|
|
||||||
const $rootScope = $injector.get($ROOT_SCOPE) as angular.IRootScopeService;
|
|
||||||
|
|
||||||
expect(element.textContent).toBe('');
|
|
||||||
expect(() => $injector.get(INJECTOR_KEY)).toThrowError();
|
|
||||||
|
|
||||||
$rootScope.$apply('loadNg2 = true');
|
|
||||||
expect(element.textContent).toBe('');
|
|
||||||
expect(() => $injector.get(INJECTOR_KEY)).toThrowError();
|
|
||||||
|
|
||||||
// Wait for the module to be bootstrapped.
|
|
||||||
setTimeout(() => {
|
|
||||||
expect(() => $injector.get(INJECTOR_KEY)).not.toThrow();
|
|
||||||
|
|
||||||
// Wait for `$evalAsync()` to propagate inputs.
|
|
||||||
setTimeout(() => expect(element.textContent).toBe('foo-bar'));
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should give access to both injectors in the Angular module\'s constructor', async(() => {
|
|
||||||
let $injectorFromNg2: angular.IInjectorService|null = null;
|
|
||||||
|
|
||||||
@Component({selector: 'ng2', template: ''})
|
|
||||||
class Ng2Component {
|
|
||||||
}
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [Ng2Component],
|
|
||||||
entryComponents: [Ng2Component],
|
|
||||||
imports: [BrowserModule],
|
|
||||||
})
|
|
||||||
class Ng2Module {
|
|
||||||
constructor(injector: Injector) {
|
|
||||||
$injectorFromNg2 = injector.get<angular.IInjectorService>('$injector' as any);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngDoBootstrap() {}
|
@Component({selector: 'ng2B', template: 'b({{ value }})'})
|
||||||
}
|
class Ng2BComponent {
|
||||||
|
@Input() value = -2;
|
||||||
|
}
|
||||||
|
|
||||||
const bootstrapFn = (extraProviders: Provider[]) =>
|
@NgModule({
|
||||||
platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module);
|
declarations: [Ng2AComponent, Ng2BComponent],
|
||||||
const lazyModuleName = downgradeModule<Ng2Module>(bootstrapFn);
|
entryComponents: [Ng2AComponent],
|
||||||
const ng1Module =
|
imports: [BrowserModule],
|
||||||
angular.module('ng1', [lazyModuleName])
|
})
|
||||||
.directive(
|
class Ng2Module {
|
||||||
'ng2', downgradeComponent({component: Ng2Component, propagateDigest: false}))
|
ngDoBootstrap() {}
|
||||||
.value('ng1Value', 'foo');
|
}
|
||||||
|
|
||||||
const element = html('<ng2></ng2>');
|
const bootstrapFn = (extraProviders: Provider[]) =>
|
||||||
const $injectorFromNg1 = angular.bootstrap(element, [ng1Module.name]);
|
platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module);
|
||||||
|
const lazyModuleName = downgradeModule<Ng2Module>(bootstrapFn);
|
||||||
|
const ng1Module =
|
||||||
|
angular.module('ng1', [lazyModuleName])
|
||||||
|
.directive(
|
||||||
|
'ng2', downgradeComponent({component: Ng2AComponent, propagateDigest}))
|
||||||
|
.run(($rootScope: angular.IRootScopeService) => $rootScope.value = 0);
|
||||||
|
|
||||||
// Wait for the module to be bootstrapped.
|
const element = html('<div><ng2 [value]="value" ng-if="loadNg2"></ng2></div>');
|
||||||
setTimeout(() => expect($injectorFromNg2).toBe($injectorFromNg1));
|
const $injector = angular.bootstrap(element, [ng1Module.name]);
|
||||||
}));
|
const $rootScope = $injector.get($ROOT_SCOPE) as angular.IRootScopeService;
|
||||||
|
|
||||||
|
expect(element.textContent).toBe('');
|
||||||
|
expect(() => $injector.get(INJECTOR_KEY)).toThrowError();
|
||||||
|
|
||||||
|
$rootScope.$apply('value = 1');
|
||||||
|
expect(element.textContent).toBe('');
|
||||||
|
expect(() => $injector.get(INJECTOR_KEY)).toThrowError();
|
||||||
|
|
||||||
|
$rootScope.$apply('loadNg2 = true');
|
||||||
|
expect(element.textContent).toBe('');
|
||||||
|
expect(() => $injector.get(INJECTOR_KEY)).toThrowError();
|
||||||
|
|
||||||
|
// Wait for the module to be bootstrapped.
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(() => $injector.get(INJECTOR_KEY)).not.toThrow();
|
||||||
|
|
||||||
|
// Wait for `$evalAsync()` to propagate inputs.
|
||||||
|
setTimeout(() => expect(element.textContent).toBe('a(1) | b(1)'));
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should support using an upgraded service', async(() => {
|
||||||
|
class Ng2Service {
|
||||||
|
constructor(@Inject('ng1Value') private ng1Value: string) {}
|
||||||
|
getValue = () => `${this.ng1Value}-bar`;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({selector: 'ng2', template: '{{ value }}'})
|
||||||
|
class Ng2Component {
|
||||||
|
value: string;
|
||||||
|
constructor(ng2Service: Ng2Service) { this.value = ng2Service.getValue(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [Ng2Component],
|
||||||
|
entryComponents: [Ng2Component],
|
||||||
|
imports: [BrowserModule],
|
||||||
|
providers: [
|
||||||
|
Ng2Service,
|
||||||
|
{
|
||||||
|
provide: 'ng1Value',
|
||||||
|
useFactory: (i: angular.IInjectorService) => i.get('ng1Value'),
|
||||||
|
deps: ['$injector'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
class Ng2Module {
|
||||||
|
ngDoBootstrap() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bootstrapFn = (extraProviders: Provider[]) =>
|
||||||
|
platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module);
|
||||||
|
const lazyModuleName = downgradeModule<Ng2Module>(bootstrapFn);
|
||||||
|
const ng1Module =
|
||||||
|
angular.module('ng1', [lazyModuleName])
|
||||||
|
.directive('ng2', downgradeComponent({component: Ng2Component, propagateDigest}))
|
||||||
|
.value('ng1Value', 'foo');
|
||||||
|
|
||||||
|
const element = html('<div><ng2 ng-if="loadNg2"></ng2></div>');
|
||||||
|
const $injector = angular.bootstrap(element, [ng1Module.name]);
|
||||||
|
const $rootScope = $injector.get($ROOT_SCOPE) as angular.IRootScopeService;
|
||||||
|
|
||||||
|
expect(element.textContent).toBe('');
|
||||||
|
expect(() => $injector.get(INJECTOR_KEY)).toThrowError();
|
||||||
|
|
||||||
|
$rootScope.$apply('loadNg2 = true');
|
||||||
|
expect(element.textContent).toBe('');
|
||||||
|
expect(() => $injector.get(INJECTOR_KEY)).toThrowError();
|
||||||
|
|
||||||
|
// Wait for the module to be bootstrapped.
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(() => $injector.get(INJECTOR_KEY)).not.toThrow();
|
||||||
|
|
||||||
|
// Wait for `$evalAsync()` to propagate inputs.
|
||||||
|
setTimeout(() => expect(element.textContent).toBe('foo-bar'));
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should create components inside the Angular zone', async(() => {
|
||||||
|
@Component({selector: 'ng2', template: 'In the zone: {{ inTheZone }}'})
|
||||||
|
class Ng2Component {
|
||||||
|
private inTheZone = false;
|
||||||
|
constructor() { this.inTheZone = NgZone.isInAngularZone(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [Ng2Component],
|
||||||
|
entryComponents: [Ng2Component],
|
||||||
|
imports: [BrowserModule],
|
||||||
|
})
|
||||||
|
class Ng2Module {
|
||||||
|
ngDoBootstrap() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bootstrapFn = (extraProviders: Provider[]) =>
|
||||||
|
platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module);
|
||||||
|
const lazyModuleName = downgradeModule<Ng2Module>(bootstrapFn);
|
||||||
|
const ng1Module =
|
||||||
|
angular.module('ng1', [lazyModuleName])
|
||||||
|
.directive(
|
||||||
|
'ng2', downgradeComponent({component: Ng2Component, propagateDigest}));
|
||||||
|
|
||||||
|
const element = html('<ng2></ng2>');
|
||||||
|
angular.bootstrap(element, [ng1Module.name]);
|
||||||
|
|
||||||
|
// Wait for the module to be bootstrapped.
|
||||||
|
setTimeout(() => {
|
||||||
|
// Wait for `$evalAsync()` to propagate inputs.
|
||||||
|
setTimeout(() => expect(element.textContent).toBe('In the zone: true'));
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should propagate input changes inside the Angular zone', async(() => {
|
||||||
|
let ng2Component: Ng2Component;
|
||||||
|
|
||||||
|
@Component({selector: 'ng2', template: ''})
|
||||||
|
class Ng2Component implements OnChanges {
|
||||||
|
@Input() attrInput = 'foo';
|
||||||
|
@Input() propInput = 'foo';
|
||||||
|
|
||||||
|
constructor() { ng2Component = this; }
|
||||||
|
ngOnChanges() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [Ng2Component],
|
||||||
|
entryComponents: [Ng2Component],
|
||||||
|
imports: [BrowserModule],
|
||||||
|
})
|
||||||
|
class Ng2Module {
|
||||||
|
ngDoBootstrap() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bootstrapFn = (extraProviders: Provider[]) =>
|
||||||
|
platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module);
|
||||||
|
const lazyModuleName = downgradeModule<Ng2Module>(bootstrapFn);
|
||||||
|
const ng1Module =
|
||||||
|
angular.module('ng1', [lazyModuleName])
|
||||||
|
.directive('ng2', downgradeComponent({component: Ng2Component, propagateDigest}))
|
||||||
|
.run(($rootScope: angular.IRootScopeService) => {
|
||||||
|
$rootScope.attrVal = 'bar';
|
||||||
|
$rootScope.propVal = 'bar';
|
||||||
|
});
|
||||||
|
|
||||||
|
const element = html('<ng2 attr-input="{{ attrVal }}" [prop-input]="propVal"></ng2>');
|
||||||
|
const $injector = angular.bootstrap(element, [ng1Module.name]);
|
||||||
|
const $rootScope = $injector.get($ROOT_SCOPE) as angular.IRootScopeService;
|
||||||
|
|
||||||
|
setTimeout(() => { // Wait for the module to be bootstrapped.
|
||||||
|
setTimeout(() => { // Wait for `$evalAsync()` to propagate inputs.
|
||||||
|
const expectToBeInNgZone = () => expect(NgZone.isInAngularZone()).toBe(true);
|
||||||
|
const changesSpy =
|
||||||
|
spyOn(ng2Component, 'ngOnChanges').and.callFake(expectToBeInNgZone);
|
||||||
|
|
||||||
|
expect(ng2Component.attrInput).toBe('bar');
|
||||||
|
expect(ng2Component.propInput).toBe('bar');
|
||||||
|
|
||||||
|
$rootScope.$apply('attrVal = "baz"');
|
||||||
|
expect(ng2Component.attrInput).toBe('baz');
|
||||||
|
expect(ng2Component.propInput).toBe('bar');
|
||||||
|
expect(changesSpy).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
$rootScope.$apply('propVal = "qux"');
|
||||||
|
expect(ng2Component.attrInput).toBe('baz');
|
||||||
|
expect(ng2Component.propInput).toBe('qux');
|
||||||
|
expect(changesSpy).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should wire up the component for change detection', async(() => {
|
||||||
|
@Component(
|
||||||
|
{selector: 'ng2', template: '{{ count }}<button (click)="increment()"></button>'})
|
||||||
|
class Ng2Component {
|
||||||
|
private count = 0;
|
||||||
|
increment() { ++this.count; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [Ng2Component],
|
||||||
|
entryComponents: [Ng2Component],
|
||||||
|
imports: [BrowserModule],
|
||||||
|
})
|
||||||
|
class Ng2Module {
|
||||||
|
ngDoBootstrap() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bootstrapFn = (extraProviders: Provider[]) =>
|
||||||
|
platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module);
|
||||||
|
const lazyModuleName = downgradeModule<Ng2Module>(bootstrapFn);
|
||||||
|
const ng1Module =
|
||||||
|
angular.module('ng1', [lazyModuleName])
|
||||||
|
.directive(
|
||||||
|
'ng2', downgradeComponent({component: Ng2Component, propagateDigest}));
|
||||||
|
|
||||||
|
const element = html('<ng2></ng2>');
|
||||||
|
angular.bootstrap(element, [ng1Module.name]);
|
||||||
|
|
||||||
|
setTimeout(() => { // Wait for the module to be bootstrapped.
|
||||||
|
setTimeout(() => { // Wait for `$evalAsync()` to propagate inputs.
|
||||||
|
const button = element.querySelector('button') !;
|
||||||
|
expect(element.textContent).toBe('0');
|
||||||
|
|
||||||
|
button.click();
|
||||||
|
expect(element.textContent).toBe('1');
|
||||||
|
|
||||||
|
button.click();
|
||||||
|
expect(element.textContent).toBe('2');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should only retrieve the Angular zone once (and cache it for later use)',
|
||||||
|
fakeAsync(() => {
|
||||||
|
let count = 0;
|
||||||
|
let getNgZoneCount = 0;
|
||||||
|
|
||||||
|
@Component(
|
||||||
|
{selector: 'ng2', template: 'Count: {{ count }} | In the zone: {{ inTheZone }}'})
|
||||||
|
class Ng2Component {
|
||||||
|
private count = ++count;
|
||||||
|
private inTheZone = false;
|
||||||
|
constructor() { this.inTheZone = NgZone.isInAngularZone(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [Ng2Component],
|
||||||
|
entryComponents: [Ng2Component],
|
||||||
|
imports: [BrowserModule],
|
||||||
|
})
|
||||||
|
class Ng2Module {
|
||||||
|
constructor(injector: Injector) {
|
||||||
|
const originalGet = injector.get;
|
||||||
|
injector.get = function(token: any) {
|
||||||
|
if (token === NgZone) ++getNgZoneCount;
|
||||||
|
return originalGet.apply(injector, arguments);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
ngDoBootstrap() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bootstrapFn = (extraProviders: Provider[]) =>
|
||||||
|
platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module);
|
||||||
|
const lazyModuleName = downgradeModule<Ng2Module>(bootstrapFn);
|
||||||
|
const ng1Module =
|
||||||
|
angular.module('ng1', [lazyModuleName])
|
||||||
|
.directive(
|
||||||
|
'ng2', downgradeComponent({component: Ng2Component, propagateDigest}));
|
||||||
|
|
||||||
|
const element = html('<div><ng2 ng-if="showNg2"></ng2></div>');
|
||||||
|
const $injector = angular.bootstrap(element, [ng1Module.name]);
|
||||||
|
const $rootScope = $injector.get($ROOT_SCOPE) as angular.IRootScopeService;
|
||||||
|
|
||||||
|
$rootScope.$apply('showNg2 = true');
|
||||||
|
tick(); // Wait for the module to be bootstrapped and `$evalAsync()` to propagate
|
||||||
|
// inputs.
|
||||||
|
|
||||||
|
const injector = ($injector.get(LAZY_MODULE_REF) as LazyModuleRef).injector !;
|
||||||
|
const injectorGet = injector.get;
|
||||||
|
spyOn(injector, 'get').and.callFake((...args: any[]) => {
|
||||||
|
expect(args[0]).not.toBe(NgZone);
|
||||||
|
return injectorGet.apply(injector, args);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(element.textContent).toBe('Count: 1 | In the zone: true');
|
||||||
|
|
||||||
|
$rootScope.$apply('showNg2 = false');
|
||||||
|
expect(element.textContent).toBe('');
|
||||||
|
|
||||||
|
$rootScope.$apply('showNg2 = true');
|
||||||
|
tick(); // Wait for `$evalAsync()` to propagate inputs.
|
||||||
|
expect(element.textContent).toBe('Count: 2 | In the zone: true');
|
||||||
|
|
||||||
|
$rootScope.$destroy();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should give access to both injectors in the Angular module\'s constructor', async(() => {
|
||||||
|
let $injectorFromNg2: angular.IInjectorService|null = null;
|
||||||
|
|
||||||
|
@Component({selector: 'ng2', template: ''})
|
||||||
|
class Ng2Component {
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [Ng2Component],
|
||||||
|
entryComponents: [Ng2Component],
|
||||||
|
imports: [BrowserModule],
|
||||||
|
})
|
||||||
|
class Ng2Module {
|
||||||
|
constructor(injector: Injector) {
|
||||||
|
$injectorFromNg2 = injector.get<angular.IInjectorService>('$injector' as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngDoBootstrap() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bootstrapFn = (extraProviders: Provider[]) =>
|
||||||
|
platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module);
|
||||||
|
const lazyModuleName = downgradeModule<Ng2Module>(bootstrapFn);
|
||||||
|
const ng1Module =
|
||||||
|
angular.module('ng1', [lazyModuleName])
|
||||||
|
.directive(
|
||||||
|
'ng2', downgradeComponent({component: Ng2Component, propagateDigest}));
|
||||||
|
|
||||||
|
const element = html('<ng2></ng2>');
|
||||||
|
const $injectorFromNg1 = angular.bootstrap(element, [ng1Module.name]);
|
||||||
|
|
||||||
|
// Wait for the module to be bootstrapped.
|
||||||
|
setTimeout(() => expect($injectorFromNg2).toBe($injectorFromNg1));
|
||||||
|
}));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue