From e5c4e5801f06eed27865c7c1cdd8c935422bad6d Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Wed, 28 Dec 2016 00:42:53 +0200 Subject: [PATCH] fix(upgrade): fix/improve support for lifecycle hooks (#13020) With the exception of `$onChanges()`, all lifecycle hooks in ng1 are called on the controller, regardless if it is the binding destination or not (i.e. regardless of the value of `bindToController`). This change makes `upgrade` mimic that behavior when calling lifecycle hooks. Additionally, calling the `$onInit()` hook has been moved before calling the linking functions, which also mimics the ng1 behavior. --- .../upgrade/src/upgrade_ng1_adapter.ts | 57 +- modules/@angular/upgrade/test/upgrade_spec.ts | 713 ++++++++++++++---- 2 files changed, 596 insertions(+), 174 deletions(-) diff --git a/modules/@angular/upgrade/src/upgrade_ng1_adapter.ts b/modules/@angular/upgrade/src/upgrade_ng1_adapter.ts index f0a86d411e..974321e09f 100644 --- a/modules/@angular/upgrade/src/upgrade_ng1_adapter.ts +++ b/modules/@angular/upgrade/src/upgrade_ng1_adapter.ts @@ -12,6 +12,21 @@ import * as angular from './angular_js'; import {NG1_COMPILE, NG1_CONTROLLER, NG1_HTTP_BACKEND, NG1_SCOPE, NG1_TEMPLATE_CACHE} from './constants'; import {controllerKey} from './util'; +interface IBindingDestination { + [key: string]: any; + $onChanges?: (changes: SimpleChanges) => void; +} + +interface IControllerInstance extends IBindingDestination { + $doCheck?: () => void; + $onDestroy?: () => void; + $onInit?: () => void; + $postLink?: () => void; +} + +type LifecycleHook = '$doCheck' | '$onChanges' | '$onDestroy' | '$onInit' | '$postLink'; + + const CAMEL_CASE = /([A-Z])/g; const INITIAL_VALUE = { __UNINITIALIZED__: true @@ -134,11 +149,11 @@ export class UpgradeNg1ComponentAdapterBuilder { httpBackend: angular.IHttpBackendService): Promise { if (this.directive.template !== undefined) { this.linkFn = compileHtml( - typeof this.directive.template === 'function' ? this.directive.template() : - this.directive.template); + isFunction(this.directive.template) ? this.directive.template() : + this.directive.template); } else if (this.directive.templateUrl) { - const url = typeof this.directive.templateUrl === 'function' ? this.directive.templateUrl() : - this.directive.templateUrl; + const url = isFunction(this.directive.templateUrl) ? this.directive.templateUrl() : + this.directive.templateUrl; const html = templateCache.get(url); if (html !== undefined) { this.linkFn = compileHtml(html); @@ -193,7 +208,8 @@ export class UpgradeNg1ComponentAdapterBuilder { } class UpgradeNg1ComponentAdapter implements OnInit, OnChanges, DoCheck { - destinationObj: any = null; + private controllerInstance: IControllerInstance = null; + destinationObj: IBindingDestination = null; checkLastValues: any[] = []; componentScope: angular.IScope; element: Element; @@ -209,7 +225,8 @@ class UpgradeNg1ComponentAdapter implements OnInit, OnChanges, DoCheck { this.$element = angular.element(this.element); const controllerType = directive.controller; if (directive.bindToController && controllerType) { - this.destinationObj = this.buildController(controllerType); + this.controllerInstance = this.buildController(controllerType); + this.destinationObj = this.controllerInstance; } else { this.destinationObj = this.componentScope; } @@ -231,8 +248,13 @@ class UpgradeNg1ComponentAdapter implements OnInit, OnChanges, DoCheck { ngOnInit() { if (!this.directive.bindToController && this.directive.controller) { - this.buildController(this.directive.controller); + this.controllerInstance = this.buildController(this.directive.controller); } + + if (this.controllerInstance && isFunction(this.controllerInstance.$onInit)) { + this.controllerInstance.$onInit(); + } + let link = this.directive.link; if (typeof link == 'object') link = (link).pre; if (link) { @@ -257,8 +279,9 @@ class UpgradeNg1ComponentAdapter implements OnInit, OnChanges, DoCheck { parentBoundTranscludeFn: (scope: any /** TODO #9100 */, cloneAttach: any /** TODO #9100 */) => { cloneAttach(childNodes); } }); - if (this.destinationObj.$onInit) { - this.destinationObj.$onInit(); + + if (this.controllerInstance && isFunction(this.controllerInstance.$postLink)) { + this.controllerInstance.$postLink(); } } @@ -269,7 +292,8 @@ class UpgradeNg1ComponentAdapter implements OnInit, OnChanges, DoCheck { this.setComponentProperty(name, change.currentValue); ng1Changes[this.propertyMap[name]] = change; }); - if (this.destinationObj.$onChanges) { + + if (isFunction(this.destinationObj.$onChanges)) { this.destinationObj.$onChanges(ng1Changes); } } @@ -290,14 +314,15 @@ class UpgradeNg1ComponentAdapter implements OnInit, OnChanges, DoCheck { } } } - if (this.destinationObj.$doCheck && this.directive.controller) { - this.destinationObj.$doCheck(); + + if (this.controllerInstance && isFunction(this.controllerInstance.$doCheck)) { + this.controllerInstance.$doCheck(); } } ngOnDestroy() { - if (this.destinationObj.$onDestroy && this.directive.controller) { - this.destinationObj.$onDestroy(); + if (this.controllerInstance && isFunction(this.controllerInstance.$onDestroy)) { + this.controllerInstance.$onDestroy(); } } @@ -353,3 +378,7 @@ class UpgradeNg1ComponentAdapter implements OnInit, OnChanges, DoCheck { `Directive '${this.directive.name}' require syntax unrecognized: ${this.directive.require}`); } } + +function isFunction(value: any): value is Function { + return typeof value === 'function'; +} diff --git a/modules/@angular/upgrade/test/upgrade_spec.ts b/modules/@angular/upgrade/test/upgrade_spec.ts index 22738e5134..8db5686593 100644 --- a/modules/@angular/upgrade/test/upgrade_spec.ts +++ b/modules/@angular/upgrade/test/upgrade_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ChangeDetectorRef, Class, Component, EventEmitter, NO_ERRORS_SCHEMA, NgModule, SimpleChanges, Testability, destroyPlatform, forwardRef} from '@angular/core'; +import {ChangeDetectorRef, Class, Component, EventEmitter, NO_ERRORS_SCHEMA, NgModule, SimpleChange, SimpleChanges, Testability, destroyPlatform, forwardRef} from '@angular/core'; import {async, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing'; import {BrowserModule} from '@angular/platform-browser'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; @@ -900,167 +900,560 @@ export function main() { }); })); - it('should call $onInit of components', async(() => { - const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); - const $onInitSpy = jasmine.createSpy('$onInit'); + describe('with lifecycle hooks', () => { + it('should call `$onInit()` on controller', async(() => { + const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + const $onInitSpyA = jasmine.createSpy('$onInitA'); + const $onInitSpyB = jasmine.createSpy('$onInitB'); - @Component({selector: 'ng2', template: ''}) - class Ng2Component { - } - - angular.module('ng1', []) - .component('ng1', { - bindings: {}, - template: '', - controller: function() { this.$onInit = $onInitSpy; } - }) - .directive('ng2', adapter.downgradeNg2Component(Ng2Component)); - - @NgModule({ - declarations: [adapter.upgradeNg1Component('ng1'), Ng2Component], - imports: [BrowserModule], - }) - class Ng2Module { - } - - const element = html(`
`); - adapter.bootstrap(element, ['ng1']).ready((ref) => { - expect($onInitSpy).toHaveBeenCalled(); - ref.dispose(); - }); - })); - - it('should call $doCheck of components', async(() => { - const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); - const $doCheckSpy = jasmine.createSpy('$doCheck'); - let changeDetector: ChangeDetectorRef; - - @Component({selector: 'ng2', template: ''}) - class Ng2Component { - constructor(cd: ChangeDetectorRef) { changeDetector = cd; } - } - - angular.module('ng1', []) - .component('ng1', { - bindings: {}, - template: '{{$ctrl.value}}', - controller: function() { this.$doCheck = $doCheckSpy; } - }) - .directive('ng2', adapter.downgradeNg2Component(Ng2Component)); - - - @NgModule({ - declarations: [adapter.upgradeNg1Component('ng1'), Ng2Component], - imports: [BrowserModule], - }) - class Ng2Module { - } - - const element = html(`
`); - adapter.bootstrap(element, ['ng1']).ready((ref) => { - expect($doCheckSpy).toHaveBeenCalled(); - - $doCheckSpy.calls.reset(); - changeDetector.detectChanges(); - - expect($doCheckSpy).toHaveBeenCalled(); - - ref.dispose(); - }); - })); - - it('should call $onChanges of components', fakeAsync(() => { - const EXPECTED_VALUE = '$onChanges called'; - const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); - const $onChangesSpy = jasmine.createSpy('$onChanges'); - let ng2Instance: any; - - @Component({selector: 'ng2', template: ''}) - class Ng2Component { - constructor() { ng2Instance = this; } - } - - angular.module('ng1Module', []) - .component('ng1', { - bindings: {val: '<'}, - template: '', - controller: function() { this.$onChanges = $onChangesSpy; } - }) - .directive('ng2', adapter.downgradeNg2Component(Ng2Component)); - - @NgModule({ - declarations: [adapter.upgradeNg1Component('ng1'), Ng2Component], - imports: [BrowserModule], - }) - class Ng2Module { - } - - const element = html(`
`); - adapter.bootstrap(element, ['ng1Module']).ready((ref) => { - - ng2Instance.val = EXPECTED_VALUE; - tick(); - ref.ng1RootScope.$digest(); - - expect($onChangesSpy).toHaveBeenCalled(); - const changes = $onChangesSpy.calls.mostRecent().args[0] as SimpleChanges; - expect(changes['val'].currentValue).toEqual(EXPECTED_VALUE); - - ref.dispose(); - }); - })); - - it('should call $onDestroy of components', fakeAsync(() => { - const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); - const $onDestroySpy = jasmine.createSpy('$onDestroy'); - - @Component({selector: 'ng2', template: ''}) - class Ng2Component { - } - - angular.module('ng1', []) - .component('ng1', { - bindings: {}, - template: '
ng1
', - controller: function() { this.$onDestroy = $onDestroySpy; } - }) - .directive('ng2', adapter.downgradeNg2Component(Ng2Component)); - - - @NgModule({ - declarations: [adapter.upgradeNg1Component('ng1'), Ng2Component], - imports: [BrowserModule], - }) - class Ng2Module { - } - - - const element = html(`
`); - adapter.bootstrap(element, ['ng1']).ready((ref) => { - const $rootScope = ref.ng1RootScope as any; - - $rootScope.destroy = false; - tick(); - $rootScope.$digest(); - - expect($onDestroySpy).not.toHaveBeenCalled(); - - $rootScope.destroy = true; - tick(); - $rootScope.$digest(); - - expect($onDestroySpy).toHaveBeenCalled(); - - ref.dispose(); - - if (!(global as any)['requestAnimationFrame']) { - // Needed for browser which don't support RAF and use a 16.6 setTimeout instead in - // ng1's AnimateRunner. - // This setTimeout remains at the end of the test and needs to be discarded. - tick(20); + @Component({selector: 'ng2', template: ' | '}) + class Ng2Component { } - }); - })); + + angular.module('ng1', []) + .directive('ng1A', () => ({ + template: '', + scope: {}, + bindToController: true, + controllerAs: '$ctrl', + controller: class {$onInit() { $onInitSpyA(); }} + })) + .directive('ng1B', () => ({ + template: '', + scope: {}, + bindToController: false, + controllerAs: '$ctrl', + controller: function() { this.$onInit = $onInitSpyB; } + })) + .directive('ng2', adapter.downgradeNg2Component(Ng2Component)); + + @NgModule({ + declarations: [ + adapter.upgradeNg1Component('ng1A'), adapter.upgradeNg1Component('ng1B'), + Ng2Component + ], + imports: [BrowserModule], + }) + class Ng2Module { + } + + const element = html(`
`); + adapter.bootstrap(element, ['ng1']).ready((ref) => { + expect($onInitSpyA).toHaveBeenCalled(); + expect($onInitSpyB).toHaveBeenCalled(); + + ref.dispose(); + }); + })); + + it('should not call `$onInit()` on scope', async(() => { + const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + const $onInitSpy = jasmine.createSpy('$onInit'); + + @Component({selector: 'ng2', template: ' | '}) + class Ng2Component { + } + + angular.module('ng1', []) + .directive('ng1A', () => ({ + template: '', + scope: {}, + bindToController: true, + controllerAs: '$ctrl', + controller: function($scope: angular.IScope) { + Object.getPrototypeOf($scope).$onInit = $onInitSpy; + } + })) + .directive('ng1B', () => ({ + template: '', + scope: {}, + bindToController: false, + controllerAs: '$ctrl', + controller: function($scope: angular.IScope) { + $scope['$onInit'] = $onInitSpy; + } + })) + .directive('ng2', adapter.downgradeNg2Component(Ng2Component)); + + @NgModule({ + declarations: [ + adapter.upgradeNg1Component('ng1A'), adapter.upgradeNg1Component('ng1B'), + Ng2Component + ], + imports: [BrowserModule], + }) + class Ng2Module { + } + + const element = html(`
`); + adapter.bootstrap(element, ['ng1']).ready((ref) => { + expect($onInitSpy).not.toHaveBeenCalled(); + ref.dispose(); + }); + })); + + it('should call `$doCheck()` on controller', async(() => { + const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + const $doCheckSpyA = jasmine.createSpy('$doCheckA'); + const $doCheckSpyB = jasmine.createSpy('$doCheckB'); + let changeDetector: ChangeDetectorRef; + + @Component({selector: 'ng2', template: ' | '}) + class Ng2Component { + constructor(cd: ChangeDetectorRef) { changeDetector = cd; } + } + + angular.module('ng1', []) + .directive('ng1A', () => ({ + template: '', + scope: {}, + bindToController: true, + controllerAs: '$ctrl', + controller: class {$doCheck() { $doCheckSpyA(); }} + })) + .directive('ng1B', () => ({ + template: '', + scope: {}, + bindToController: false, + controllerAs: '$ctrl', + controller: function() { this.$doCheck = $doCheckSpyB; } + })) + .directive('ng2', adapter.downgradeNg2Component(Ng2Component)); + + @NgModule({ + declarations: [ + adapter.upgradeNg1Component('ng1A'), adapter.upgradeNg1Component('ng1B'), + Ng2Component + ], + imports: [BrowserModule], + }) + class Ng2Module { + } + + const element = html(`
`); + adapter.bootstrap(element, ['ng1']).ready((ref) => { + expect($doCheckSpyA).toHaveBeenCalled(); + expect($doCheckSpyB).toHaveBeenCalled(); + + $doCheckSpyA.calls.reset(); + $doCheckSpyB.calls.reset(); + changeDetector.detectChanges(); + + expect($doCheckSpyA).toHaveBeenCalled(); + expect($doCheckSpyB).toHaveBeenCalled(); + + ref.dispose(); + }); + })); + + it('should not call `$doCheck()` on scope', async(() => { + const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + const $doCheckSpyA = jasmine.createSpy('$doCheckA'); + const $doCheckSpyB = jasmine.createSpy('$doCheckB'); + let changeDetector: ChangeDetectorRef; + + @Component({selector: 'ng2', template: ' | '}) + class Ng2Component { + constructor(cd: ChangeDetectorRef) { changeDetector = cd; } + } + + angular.module('ng1', []) + .directive('ng1A', () => ({ + template: '', + scope: {}, + bindToController: true, + controllerAs: '$ctrl', + controller: function($scope: angular.IScope) { + Object.getPrototypeOf($scope).$doCheck = $doCheckSpyA; + } + })) + .directive('ng1B', () => ({ + template: '', + scope: {}, + bindToController: false, + controllerAs: '$ctrl', + controller: function($scope: angular.IScope) { + $scope['$doCheck'] = $doCheckSpyB; + } + })) + .directive('ng2', adapter.downgradeNg2Component(Ng2Component)); + + @NgModule({ + declarations: [ + adapter.upgradeNg1Component('ng1A'), adapter.upgradeNg1Component('ng1B'), + Ng2Component + ], + imports: [BrowserModule], + }) + class Ng2Module { + } + + const element = html(`
`); + adapter.bootstrap(element, ['ng1']).ready((ref) => { + $doCheckSpyA.calls.reset(); + $doCheckSpyB.calls.reset(); + changeDetector.detectChanges(); + + expect($doCheckSpyA).not.toHaveBeenCalled(); + expect($doCheckSpyB).not.toHaveBeenCalled(); + + ref.dispose(); + }); + })); + + it('should call `$postLink()` on controller', async(() => { + const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + const $postLinkSpyA = jasmine.createSpy('$postLinkA'); + const $postLinkSpyB = jasmine.createSpy('$postLinkB'); + + @Component({selector: 'ng2', template: ' | '}) + class Ng2Component { + } + + angular.module('ng1', []) + .directive('ng1A', () => ({ + template: '', + scope: {}, + bindToController: true, + controllerAs: '$ctrl', + controller: class {$postLink() { $postLinkSpyA(); }} + })) + .directive('ng1B', () => ({ + template: '', + scope: {}, + bindToController: false, + controllerAs: '$ctrl', + controller: function() { this.$postLink = $postLinkSpyB; } + })) + .directive('ng2', adapter.downgradeNg2Component(Ng2Component)); + + @NgModule({ + declarations: [ + adapter.upgradeNg1Component('ng1A'), adapter.upgradeNg1Component('ng1B'), + Ng2Component + ], + imports: [BrowserModule], + }) + class Ng2Module { + } + + const element = html(`
`); + adapter.bootstrap(element, ['ng1']).ready((ref) => { + expect($postLinkSpyA).toHaveBeenCalled(); + expect($postLinkSpyB).toHaveBeenCalled(); + + ref.dispose(); + }); + })); + + it('should not call `$postLink()` on scope', async(() => { + const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + const $postLinkSpy = jasmine.createSpy('$postLink'); + + @Component({selector: 'ng2', template: ' | '}) + class Ng2Component { + } + + angular.module('ng1', []) + .directive('ng1A', () => ({ + template: '', + scope: {}, + bindToController: true, + controllerAs: '$ctrl', + controller: function($scope: angular.IScope) { + Object.getPrototypeOf($scope).$postLink = $postLinkSpy; + } + })) + .directive('ng1B', () => ({ + template: '', + scope: {}, + bindToController: false, + controllerAs: '$ctrl', + controller: function($scope: angular.IScope) { + $scope['$postLink'] = $postLinkSpy; + } + })) + .directive('ng2', adapter.downgradeNg2Component(Ng2Component)); + + @NgModule({ + declarations: [ + adapter.upgradeNg1Component('ng1A'), adapter.upgradeNg1Component('ng1B'), + Ng2Component + ], + imports: [BrowserModule], + }) + class Ng2Module { + } + + const element = html(`
`); + adapter.bootstrap(element, ['ng1']).ready((ref) => { + expect($postLinkSpy).not.toHaveBeenCalled(); + ref.dispose(); + }); + })); + + it('should call `$onChanges()` on binding destination', fakeAsync(() => { + const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + const $onChangesControllerSpyA = jasmine.createSpy('$onChangesControllerA'); + const $onChangesControllerSpyB = jasmine.createSpy('$onChangesControllerB'); + const $onChangesScopeSpy = jasmine.createSpy('$onChangesScope'); + let ng2Instance: any; + + @Component({ + selector: 'ng2', + template: ' | ' + }) + class Ng2Component { + constructor() { ng2Instance = this; } + } + + angular.module('ng1', []) + .directive('ng1A', () => ({ + template: '', + scope: {valA: '<'}, + bindToController: true, + controllerAs: '$ctrl', + controller: function($scope: angular.IScope) { + this.$onChanges = $onChangesControllerSpyA; + } + })) + .directive( + 'ng1B', + () => ({ + template: '', + scope: {valB: '<'}, + bindToController: false, + controllerAs: '$ctrl', + controller: class { + $onChanges(changes: SimpleChanges) { $onChangesControllerSpyB(changes); } + } + })) + .directive('ng2', adapter.downgradeNg2Component(Ng2Component)) + .run(($rootScope: angular.IRootScopeService) => { + Object.getPrototypeOf($rootScope).$onChanges = $onChangesScopeSpy; + }); + + + @NgModule({ + declarations: [ + adapter.upgradeNg1Component('ng1A'), adapter.upgradeNg1Component('ng1B'), + Ng2Component + ], + imports: [BrowserModule], + }) + class Ng2Module { + } + + const element = html(`
`); + adapter.bootstrap(element, ['ng1']).ready((ref) => { + // Initial `$onChanges()` call + tick(); + + expect($onChangesControllerSpyA.calls.count()).toBe(1); + expect($onChangesControllerSpyA.calls.argsFor(0)[0]).toEqual({ + valA: jasmine.any(SimpleChange) + }); + + expect($onChangesControllerSpyB).not.toHaveBeenCalled(); + + expect($onChangesScopeSpy.calls.count()).toBe(1); + expect($onChangesScopeSpy.calls.argsFor(0)[0]).toEqual({ + valB: jasmine.any(SimpleChange) + }); + + $onChangesControllerSpyA.calls.reset(); + $onChangesControllerSpyB.calls.reset(); + $onChangesScopeSpy.calls.reset(); + + // `$onChanges()` call after a change + ng2Instance.val = 'new value'; + tick(); + ref.ng1RootScope.$digest(); + + expect($onChangesControllerSpyA.calls.count()).toBe(1); + expect($onChangesControllerSpyA.calls.argsFor(0)[0]).toEqual({ + valA: jasmine.objectContaining({currentValue: 'new value'}) + }); + + expect($onChangesControllerSpyB).not.toHaveBeenCalled(); + + expect($onChangesScopeSpy.calls.count()).toBe(1); + expect($onChangesScopeSpy.calls.argsFor(0)[0]).toEqual({ + valB: jasmine.objectContaining({currentValue: 'new value'}) + }); + + ref.dispose(); + }); + })); + + it('should call `$onDestroy()` on controller', fakeAsync(() => { + const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + const $onDestroySpyA = jasmine.createSpy('$onDestroyA'); + const $onDestroySpyB = jasmine.createSpy('$onDestroyB'); + let ng2ComponentInstance: Ng2Component; + + @Component({ + selector: 'ng2', + template: ` +
+ | +
+ ` + }) + class Ng2Component { + ng2Destroy: boolean = false; + constructor() { ng2ComponentInstance = this; } + } + + // On browsers that don't support `requestAnimationFrame` (IE 9, Android <= 4.3), + // `$animate` will use `setTimeout(..., 16.6)` instead. This timeout will still be on + // the queue at the end of the test, causing it to fail. + // Mocking animations (via `ngAnimateMock`) avoids the issue. + angular.module('ng1', ['ngAnimateMock']) + .directive('ng1A', () => ({ + template: '', + scope: {}, + bindToController: true, + controllerAs: '$ctrl', + controller: class {$onDestroy() { $onDestroySpyA(); }} + })) + .directive('ng1B', () => ({ + template: '', + scope: {}, + bindToController: false, + controllerAs: '$ctrl', + controller: function() { this.$onDestroy = $onDestroySpyB; } + })) + .directive('ng2', adapter.downgradeNg2Component(Ng2Component)); + + @NgModule({ + declarations: [ + adapter.upgradeNg1Component('ng1A'), adapter.upgradeNg1Component('ng1B'), + Ng2Component + ], + imports: [BrowserModule], + }) + class Ng2Module { + } + + const element = html(`
`); + adapter.bootstrap(element, ['ng1']).ready((ref) => { + const $rootScope = ref.ng1RootScope as any; + + $rootScope.ng1Destroy = false; + tick(); + $rootScope.$digest(); + + expect($onDestroySpyA).not.toHaveBeenCalled(); + expect($onDestroySpyB).not.toHaveBeenCalled(); + + $rootScope.ng1Destroy = true; + tick(); + $rootScope.$digest(); + + expect($onDestroySpyA).toHaveBeenCalled(); + expect($onDestroySpyB).toHaveBeenCalled(); + + $onDestroySpyA.calls.reset(); + $onDestroySpyB.calls.reset(); + + $rootScope.ng1Destroy = false; + tick(); + $rootScope.$digest(); + + expect($onDestroySpyA).not.toHaveBeenCalled(); + expect($onDestroySpyB).not.toHaveBeenCalled(); + + ng2ComponentInstance.ng2Destroy = true; + tick(); + $rootScope.$digest(); + + expect($onDestroySpyA).toHaveBeenCalled(); + expect($onDestroySpyB).toHaveBeenCalled(); + + ref.dispose(); + }); + })); + + it('should not call `$onDestroy()` on scope', fakeAsync(() => { + const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + const $onDestroySpy = jasmine.createSpy('$onDestroy'); + let ng2ComponentInstance: Ng2Component; + + @Component({ + selector: 'ng2', + template: ` +
+ | +
+ ` + }) + class Ng2Component { + ng2Destroy: boolean = false; + constructor() { ng2ComponentInstance = this; } + } + + // On browsers that don't support `requestAnimationFrame` (IE 9, Android <= 4.3), + // `$animate` will use `setTimeout(..., 16.6)` instead. This timeout will still be on + // the queue at the end of the test, causing it to fail. + // Mocking animations (via `ngAnimateMock`) avoids the issue. + angular.module('ng1', ['ngAnimateMock']) + .directive('ng1A', () => ({ + template: '', + scope: {}, + bindToController: true, + controllerAs: '$ctrl', + controller: function($scope: angular.IScope) { + Object.getPrototypeOf($scope).$onDestroy = $onDestroySpy; + } + })) + .directive('ng1B', () => ({ + template: '', + scope: {}, + bindToController: false, + controllerAs: '$ctrl', + controller: function($scope: angular.IScope) { + $scope['$onDestroy'] = $onDestroySpy; + } + })) + .directive('ng2', adapter.downgradeNg2Component(Ng2Component)); + + @NgModule({ + declarations: [ + adapter.upgradeNg1Component('ng1A'), adapter.upgradeNg1Component('ng1B'), + Ng2Component + ], + imports: [BrowserModule], + }) + class Ng2Module { + } + + const element = html(`
`); + adapter.bootstrap(element, ['ng1']).ready((ref) => { + const $rootScope = ref.ng1RootScope as any; + + $rootScope.ng1Destroy = false; + tick(); + $rootScope.$digest(); + + $rootScope.ng1Destroy = true; + tick(); + $rootScope.$digest(); + + $rootScope.ng1Destroy = false; + tick(); + $rootScope.$digest(); + + ng2ComponentInstance.ng2Destroy = true; + tick(); + $rootScope.$digest(); + + expect($onDestroySpy).not.toHaveBeenCalled(); + + ref.dispose(); + }); + })); + }); it('should bind input properties (<) of components', async(() => { const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));