diff --git a/packages/upgrade/src/common/angular1.ts b/packages/upgrade/src/common/angular1.ts index a43dba6e45..76a7864209 100644 --- a/packages/upgrade/src/common/angular1.ts +++ b/packages/upgrade/src/common/angular1.ts @@ -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; diff --git a/packages/upgrade/src/common/downgrade_component.ts b/packages/upgrade/src/common/downgrade_component.ts index 8968ba5dd4..65357f1fe6 100644 --- a/packages/upgrade/src/common/downgrade_component.ts +++ b/packages/upgrade/src/common/downgrade_component.ts @@ -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 = (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; } - const downgradeFn = (injector: Injector) => { + const doDowngrade = (injector: Injector) => { const componentFactoryResolver: ComponentFactoryResolver = injector.get(ComponentFactoryResolver); const componentFactory: ComponentFactory = @@ -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 = (cb: () => T) => () => + NgZone.isInAngularZone() ? cb() : ngZone.run(cb); + } + + wrapCallback(() => doDowngrade(injector))(); + }; + if (isThenable(parentInjector)) { parentInjector.then(downgradeFn); } else { diff --git a/packages/upgrade/src/common/downgrade_component_adapter.ts b/packages/upgrade/src/common/downgrade_component_adapter.ts index af9dad0e12..06ead26b3b 100644 --- a/packages/upgrade/src/common/downgrade_component_adapter.ts +++ b/packages/upgrade/src/common/downgrade_component_adapter.ts @@ -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|null = null; - private component: any = null; - private changeDetector: ChangeDetectorRef|null = null; + private componentRef: ComponentRef; + 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) { + private $parse: angular.IParseService, private componentFactory: ComponentFactory, + private wrapCallback: (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 && (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 { (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) { diff --git a/packages/upgrade/src/common/util.ts b/packages/upgrade/src/common/util.ts index 35dc83430f..15213dd986 100644 --- a/packages/upgrade/src/common/util.ts +++ b/packages/upgrade/src/common/util.ts @@ -68,8 +68,11 @@ export class Deferred { } 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; + promise?: Promise; } /** diff --git a/packages/upgrade/src/dynamic/upgrade_adapter.ts b/packages/upgrade/src/dynamic/upgrade_adapter.ts index 038503acfd..fe437cd1b4 100644 --- a/packages/upgrade/src/dynamic/upgrade_adapter.ts +++ b/packages/upgrade/src/dynamic/upgrade_adapter.ts @@ -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([ diff --git a/packages/upgrade/src/static/downgrade_module.ts b/packages/upgrade/src/static/downgrade_module.ts index d1fcabef00..8a82075384 100644 --- a/packages/upgrade/src/static/downgrade_module.ts +++ b/packages/upgrade/src/static/downgrade_module.ts @@ -35,7 +35,8 @@ export function downgradeModule( 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( ($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); diff --git a/packages/upgrade/src/static/upgrade_module.ts b/packages/upgrade/src/static/upgrade_module.ts index 0eeb69fc70..1c57528350 100644 --- a/packages/upgrade/src/static/upgrade_module.ts +++ b/packages/upgrade/src/static/upgrade_module.ts @@ -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, diff --git a/packages/upgrade/test/static/integration/downgrade_module_spec.ts b/packages/upgrade/test/static/integration/downgrade_module_spec.ts index a58164bae4..589d3b759e 100644 --- a/packages/upgrade/test/static/integration/downgrade_module_spec.ts +++ b/packages/upgrade/test/static/integration/downgrade_module_spec.ts @@ -6,167 +6,367 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, Inject, Injector, Input, NgModule, Provider, destroyPlatform} from '@angular/core'; -import {async} from '@angular/core/testing'; +import {Component, Inject, Injector, Input, NgModule, NgZone, OnChanges, Provider, destroyPlatform} from '@angular/core'; +import {async, fakeAsync, tick} from '@angular/core/testing'; import {BrowserModule} from '@angular/platform-browser'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; 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 {html} from '../test_helpers'; + export function main() { - describe('lazy-load ng2 module', () => { + [true, false].forEach(propagateDigest => { + describe(`lazy-load ng2 module (propagateDigest: ${propagateDigest})`, () => { - beforeEach(() => destroyPlatform()); - afterEach(() => destroyPlatform()); + beforeEach(() => destroyPlatform()); - it('should support downgrading a component and propagate inputs', async(() => { - @Component({selector: 'ng2A', template: 'a({{ value }}) | '}) - 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(bootstrapFn); - const ng1Module = - angular.module('ng1', [lazyModuleName]) - .directive( - 'ng2', downgradeComponent({component: Ng2AComponent, propagateDigest: false})) - .run(($rootScope: angular.IRootScopeService) => $rootScope.value = 0); - - const element = html('
'); - 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(bootstrapFn); - const ng1Module = - angular.module('ng1', [lazyModuleName]) - .directive( - 'ng2', downgradeComponent({component: Ng2Component, propagateDigest: false})) - .value('ng1Value', 'foo'); - - const element = html('
'); - 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('$injector' as any); + it('should support downgrading a component and propagate inputs', async(() => { + @Component( + {selector: 'ng2A', template: 'a({{ value }}) | '}) + class Ng2AComponent { + @Input() value = -1; } - ngDoBootstrap() {} - } + @Component({selector: 'ng2B', template: 'b({{ value }})'}) + class Ng2BComponent { + @Input() value = -2; + } - const bootstrapFn = (extraProviders: Provider[]) => - platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module); - const lazyModuleName = downgradeModule(bootstrapFn); - const ng1Module = - angular.module('ng1', [lazyModuleName]) - .directive( - 'ng2', downgradeComponent({component: Ng2Component, propagateDigest: false})) - .value('ng1Value', 'foo'); + @NgModule({ + declarations: [Ng2AComponent, Ng2BComponent], + entryComponents: [Ng2AComponent], + imports: [BrowserModule], + }) + class Ng2Module { + ngDoBootstrap() {} + } - const element = html(''); - const $injectorFromNg1 = angular.bootstrap(element, [ng1Module.name]); + const bootstrapFn = (extraProviders: Provider[]) => + platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module); + const lazyModuleName = downgradeModule(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. - setTimeout(() => expect($injectorFromNg2).toBe($injectorFromNg1)); - })); + const element = html('
'); + 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(bootstrapFn); + const ng1Module = + angular.module('ng1', [lazyModuleName]) + .directive('ng2', downgradeComponent({component: Ng2Component, propagateDigest})) + .value('ng1Value', 'foo'); + + const element = html('
'); + 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(bootstrapFn); + const ng1Module = + angular.module('ng1', [lazyModuleName]) + .directive( + 'ng2', downgradeComponent({component: Ng2Component, propagateDigest})); + + const element = html(''); + 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(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(''); + 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 }}'}) + 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(bootstrapFn); + const ng1Module = + angular.module('ng1', [lazyModuleName]) + .directive( + 'ng2', downgradeComponent({component: Ng2Component, propagateDigest})); + + const element = html(''); + 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(bootstrapFn); + const ng1Module = + angular.module('ng1', [lazyModuleName]) + .directive( + 'ng2', downgradeComponent({component: Ng2Component, propagateDigest})); + + const element = html('
'); + 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('$injector' as any); + } + + ngDoBootstrap() {} + } + + const bootstrapFn = (extraProviders: Provider[]) => + platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module); + const lazyModuleName = downgradeModule(bootstrapFn); + const ng1Module = + angular.module('ng1', [lazyModuleName]) + .directive( + 'ng2', downgradeComponent({component: Ng2Component, propagateDigest})); + + const element = html(''); + const $injectorFromNg1 = angular.bootstrap(element, [ng1Module.name]); + + // Wait for the module to be bootstrapped. + setTimeout(() => expect($injectorFromNg2).toBe($injectorFromNg1)); + })); + }); }); }