From 2a672a97abe3844a605c3ce0b67e063044510642 Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Tue, 7 Aug 2018 17:38:35 +0300 Subject: [PATCH] fix(upgrade): trigger `$destroy` event on upgraded component element (#25357) Fixes #25334 PR Close #25357 --- packages/upgrade/src/common/angular1.ts | 1 + packages/upgrade/src/common/upgrade_helper.ts | 1 + packages/upgrade/test/dynamic/upgrade_spec.ts | 90 +++++++++++++++++++ .../integration/upgrade_component_spec.ts | 63 ++++++++++++- 4 files changed, 154 insertions(+), 1 deletion(-) diff --git a/packages/upgrade/src/common/angular1.ts b/packages/upgrade/src/common/angular1.ts index 3cbe8897d2..bd7d22e366 100644 --- a/packages/upgrade/src/common/angular1.ts +++ b/packages/upgrade/src/common/angular1.ts @@ -127,6 +127,7 @@ export type IAugmentedJQuery = Node[] & { controller?: (name: string) => any; isolateScope?: () => IScope; injector?: () => IInjectorService; + triggerHandler?: (eventTypeOrObject: string | Event, extraParameters?: any[]) => IAugmentedJQuery; remove?: () => void; removeData?: () => void; }; diff --git a/packages/upgrade/src/common/upgrade_helper.ts b/packages/upgrade/src/common/upgrade_helper.ts index 8782de9c9e..999f83f1d0 100644 --- a/packages/upgrade/src/common/upgrade_helper.ts +++ b/packages/upgrade/src/common/upgrade_helper.ts @@ -124,6 +124,7 @@ export class UpgradeHelper { controllerInstance.$onDestroy(); } $scope.$destroy(); + this.$element.triggerHandler !('$destroy'); } prepareTransclusion(): angular.ILinkFn|undefined { diff --git a/packages/upgrade/test/dynamic/upgrade_spec.ts b/packages/upgrade/test/dynamic/upgrade_spec.ts index c99717d826..2e17c04410 100644 --- a/packages/upgrade/test/dynamic/upgrade_spec.ts +++ b/packages/upgrade/test/dynamic/upgrade_spec.ts @@ -2132,6 +2132,96 @@ withEachNg1Version(() => { })); }); + describe('destroying the upgraded component', () => { + it('should destroy `componentScope`', fakeAsync(() => { + const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + const scopeDestroyListener = jasmine.createSpy('scopeDestroyListener'); + 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']) + .component('ng1', { + controller: function($scope: angular.IScope) { + $scope.$on('$destroy', scopeDestroyListener); + }, + }) + .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; + + expect(scopeDestroyListener).not.toHaveBeenCalled(); + + ng2ComponentInstance.ng2Destroy = true; + tick(); + $rootScope.$digest(); + + expect(scopeDestroyListener).toHaveBeenCalledTimes(1); + }); + })); + + it('should emit `$destroy` on `$element`', fakeAsync(() => { + const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + const elementDestroyListener = jasmine.createSpy('elementDestroyListener'); + 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']) + .component('ng1', { + controller: function($element: angular.IAugmentedJQuery) { + $element.on !('$destroy', elementDestroyListener); + }, + }) + .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; + + expect(elementDestroyListener).not.toHaveBeenCalled(); + + ng2ComponentInstance.ng2Destroy = true; + tick(); + $rootScope.$digest(); + + expect(elementDestroyListener).toHaveBeenCalledTimes(1); + }); + })); + }); + describe('linking', () => { it('should run the pre-linking after instantiating the controller', async(() => { const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); diff --git a/packages/upgrade/test/static/integration/upgrade_component_spec.ts b/packages/upgrade/test/static/integration/upgrade_component_spec.ts index 9c0ee94db2..7e4e623477 100644 --- a/packages/upgrade/test/static/integration/upgrade_component_spec.ts +++ b/packages/upgrade/test/static/integration/upgrade_component_spec.ts @@ -3629,7 +3629,68 @@ withEachNg1Version(() => { ng2ComponentAInstance.destroyIt = true; $digest(adapter); - expect(scopeDestroyListener).toHaveBeenCalled(); + expect(scopeDestroyListener).toHaveBeenCalledTimes(1); + }); + })); + + it('should emit `$destroy` on `$element`', async(() => { + const elementDestroyListener = jasmine.createSpy('elementDestroyListener'); + let ng2ComponentAInstance: Ng2ComponentA; + + // Define `ng1Component` + const ng1Component: angular.IComponent = { + controller: class { + constructor($element: angular.IAugmentedJQuery) { + $element.on !('$destroy', elementDestroyListener); + } + } + }; + + // Define `Ng1ComponentFacade` + @Directive({selector: 'ng1'}) + class Ng1ComponentFacade extends UpgradeComponent { + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1', elementRef, injector); + } + } + + // Define `Ng2Component` + @Component({selector: 'ng2A', template: ''}) + class Ng2ComponentA { + destroyIt = false; + + constructor() { ng2ComponentAInstance = this; } + } + + @Component({selector: 'ng2B', template: ''}) + class Ng2ComponentB { + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1Module', []) + .component('ng1', ng1Component) + .directive('ng2A', downgradeComponent({component: Ng2ComponentA})); + + // Define `Ng2Module` + @NgModule({ + declarations: [Ng1ComponentFacade, Ng2ComponentA, Ng2ComponentB], + entryComponents: [Ng2ComponentA], + imports: [BrowserModule, UpgradeModule] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + // Bootstrap + const element = html(``); + + bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => { + expect(elementDestroyListener).not.toHaveBeenCalled(); + + ng2ComponentAInstance.destroyIt = true; + $digest(adapter); + + expect(elementDestroyListener).toHaveBeenCalledTimes(1); }); }));