Skip to content

Commit 6da3867

Browse files
jbedardjasonaden
authored andcommitted
fix(upgrade): properly destroy upgraded component elements and descendants (#26209)
Fixes #26208 PR Close #26209
1 parent c9488b5 commit 6da3867

File tree

4 files changed

+310
-11
lines changed

4 files changed

+310
-11
lines changed

packages/upgrade/src/common/angular1.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -214,24 +214,29 @@ export interface INgModelController {
214214
$name: string;
215215
}
216216

217-
function noNg() {
217+
function noNg(): never {
218218
throw new Error('AngularJS v1.x is not loaded!');
219219
}
220220

221+
const noNgElement: typeof angular.element = () => noNg();
222+
noNgElement.cleanData = noNg;
221223

222224
let angular: {
223225
bootstrap: (e: Element, modules: (string | IInjectable)[], config?: IAngularBootstrapConfig) =>
224226
IInjectorService,
225227
module: (prefix: string, dependencies?: string[]) => IModule,
226-
element: (e: string | Element | Document | IAugmentedJQuery) => IAugmentedJQuery,
228+
element: {
229+
(e: string | Element | Document | IAugmentedJQuery): IAugmentedJQuery;
230+
cleanData: (nodes: Node[] | NodeList) => void;
231+
},
227232
version: {major: number},
228233
resumeBootstrap: () => void,
229234
getTestability: (e: Element) => ITestabilityService
230-
} = <any>{
235+
} = {
231236
bootstrap: noNg,
232237
module: noNg,
233-
element: noNg,
234-
version: undefined,
238+
element: noNgElement,
239+
version: undefined as any,
235240
resumeBootstrap: noNg,
236241
getTestability: noNg
237242
};
@@ -282,6 +287,7 @@ export const module: typeof angular.module = (prefix, dependencies?) =>
282287
angular.module(prefix, dependencies);
283288

284289
export const element: typeof angular.element = e => angular.element(e);
290+
element.cleanData = nodes => angular.element.cleanData(nodes);
285291

286292
export const resumeBootstrap: typeof angular.resumeBootstrap = () => angular.resumeBootstrap();
287293

packages/upgrade/src/common/upgrade_helper.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,15 @@ export class UpgradeHelper {
124124
controllerInstance.$onDestroy();
125125
}
126126
$scope.$destroy();
127-
this.$element.triggerHandler !('$destroy');
127+
128+
// Clean the jQuery/jqLite data on the component+child elements.
129+
// Equivelent to how jQuery/jqLite invoke `cleanData` on an Element (this.element)
130+
// https://github.com/jquery/jquery/blob/e743cbd28553267f955f71ea7248377915613fd9/src/manipulation.js#L223
131+
// https://github.com/angular/angular.js/blob/26ddc5f830f902a3d22f4b2aab70d86d4d688c82/src/jqLite.js#L306-L312
132+
// `cleanData` will invoke the AngularJS `$destroy` DOM event
133+
// https://github.com/angular/angular.js/blob/26ddc5f830f902a3d22f4b2aab70d86d4d688c82/src/Angular.js#L1911-L1924
134+
angular.element.cleanData([this.element]);
135+
angular.element.cleanData(this.element.querySelectorAll('*'));
128136
}
129137

130138
prepareTransclusion(): angular.ILinkFn|undefined {

packages/upgrade/test/dynamic/upgrade_spec.ts

Lines changed: 145 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {BrowserModule} from '@angular/platform-browser';
1212
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
1313
import * as angular from '@angular/upgrade/src/common/angular1';
1414
import {UpgradeAdapter, UpgradeAdapterRef} from '@angular/upgrade/src/dynamic/upgrade_adapter';
15+
1516
import {$apply, $digest, html, multiTrim, withEachNg1Version} from './test_helpers';
1617

1718
declare global {
@@ -2177,9 +2178,10 @@ withEachNg1Version(() => {
21772178
});
21782179
}));
21792180

2180-
it('should emit `$destroy` on `$element`', fakeAsync(() => {
2181+
it('should emit `$destroy` on `$element` and descendants', fakeAsync(() => {
21812182
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
21822183
const elementDestroyListener = jasmine.createSpy('elementDestroyListener');
2184+
const descendantDestroyListener = jasmine.createSpy('descendantDestroyListener');
21832185
let ng2ComponentInstance: Ng2Component;
21842186

21852187
@Component({selector: 'ng2', template: '<div *ngIf="!ng2Destroy"><ng1></ng1></div>'})
@@ -2194,9 +2196,13 @@ withEachNg1Version(() => {
21942196
// Mocking animations (via `ngAnimateMock`) avoids the issue.
21952197
angular.module('ng1', ['ngAnimateMock'])
21962198
.component('ng1', {
2197-
controller: function($element: angular.IAugmentedJQuery) {
2198-
$element.on !('$destroy', elementDestroyListener);
2199+
controller: class {
2200+
constructor(private $element: angular.IAugmentedJQuery) {} $onInit() {
2201+
this.$element.on !('$destroy', elementDestroyListener);
2202+
this.$element.contents !().on !('$destroy', descendantDestroyListener);
2203+
}
21992204
},
2205+
template: '<div></div>'
22002206
})
22012207
.directive('ng2', adapter.downgradeNg2Component(Ng2Component));
22022208

@@ -2210,14 +2216,150 @@ withEachNg1Version(() => {
22102216
const element = html('<ng2></ng2>');
22112217
adapter.bootstrap(element, ['ng1']).ready((ref) => {
22122218
const $rootScope = ref.ng1RootScope as any;
2219+
tick();
2220+
$rootScope.$digest();
22132221

22142222
expect(elementDestroyListener).not.toHaveBeenCalled();
2223+
expect(descendantDestroyListener).not.toHaveBeenCalled();
22152224

22162225
ng2ComponentInstance.ng2Destroy = true;
22172226
tick();
22182227
$rootScope.$digest();
22192228

22202229
expect(elementDestroyListener).toHaveBeenCalledTimes(1);
2230+
expect(descendantDestroyListener).toHaveBeenCalledTimes(1);
2231+
});
2232+
}));
2233+
2234+
it('should clear data on `$element` and descendants', fakeAsync(() => {
2235+
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
2236+
let ng1ComponentElement: angular.IAugmentedJQuery;
2237+
let ng2ComponentAInstance: Ng2ComponentA;
2238+
2239+
// Define `ng1Component`
2240+
const ng1Component: angular.IComponent = {
2241+
controller: class {
2242+
constructor(private $element: angular.IAugmentedJQuery) {} $onInit() {
2243+
this.$element.data !('test', 1);
2244+
this.$element.contents !().data !('test', 2);
2245+
2246+
ng1ComponentElement = this.$element;
2247+
}
2248+
},
2249+
template: '<div></div>'
2250+
};
2251+
2252+
// Define `Ng2Component`
2253+
@Component({selector: 'ng2A', template: '<ng2B *ngIf="!destroyIt"></ng2B>'})
2254+
class Ng2ComponentA {
2255+
destroyIt = false;
2256+
2257+
constructor() { ng2ComponentAInstance = this; }
2258+
}
2259+
2260+
@Component({selector: 'ng2B', template: '<ng1></ng1>'})
2261+
class Ng2ComponentB {
2262+
}
2263+
2264+
// Define `ng1Module`
2265+
angular.module('ng1Module', [])
2266+
.component('ng1', ng1Component)
2267+
.directive('ng2A', adapter.downgradeNg2Component(Ng2ComponentA));
2268+
2269+
// Define `Ng2Module`
2270+
@NgModule({
2271+
declarations: [adapter.upgradeNg1Component('ng1'), Ng2ComponentA, Ng2ComponentB],
2272+
entryComponents: [Ng2ComponentA],
2273+
imports: [BrowserModule]
2274+
})
2275+
class Ng2Module {
2276+
ngDoBootstrap() {}
2277+
}
2278+
2279+
// Bootstrap
2280+
const element = html(`<ng2-a></ng2-a>`);
2281+
2282+
adapter.bootstrap(element, ['ng1Module']).ready((ref) => {
2283+
const $rootScope = ref.ng1RootScope as any;
2284+
tick();
2285+
$rootScope.$digest();
2286+
expect(ng1ComponentElement.data !('test')).toBe(1);
2287+
expect(ng1ComponentElement.contents !().data !('test')).toBe(2);
2288+
2289+
ng2ComponentAInstance.destroyIt = true;
2290+
tick();
2291+
$rootScope.$digest();
2292+
2293+
expect(ng1ComponentElement.data !('test')).toBeUndefined();
2294+
expect(ng1ComponentElement.contents !().data !('test')).toBeUndefined();
2295+
});
2296+
}));
2297+
2298+
it('should clear dom listeners on `$element` and descendants`', fakeAsync(() => {
2299+
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
2300+
const elementClickListener = jasmine.createSpy('elementClickListener');
2301+
const descendantClickListener = jasmine.createSpy('descendantClickListener');
2302+
let ng1DescendantElement: angular.IAugmentedJQuery;
2303+
let ng2ComponentAInstance: Ng2ComponentA;
2304+
2305+
// Define `ng1Component`
2306+
const ng1Component: angular.IComponent = {
2307+
controller: class {
2308+
constructor(private $element: angular.IAugmentedJQuery) {} $onInit() {
2309+
ng1DescendantElement = this.$element.contents !();
2310+
2311+
this.$element.on !('click', elementClickListener);
2312+
ng1DescendantElement.on !('click', descendantClickListener);
2313+
}
2314+
},
2315+
template: '<div></div>'
2316+
};
2317+
2318+
// Define `Ng2Component`
2319+
@Component({selector: 'ng2A', template: '<ng2B *ngIf="!destroyIt"></ng2B>'})
2320+
class Ng2ComponentA {
2321+
destroyIt = false;
2322+
2323+
constructor() { ng2ComponentAInstance = this; }
2324+
}
2325+
2326+
@Component({selector: 'ng2B', template: '<ng1></ng1>'})
2327+
class Ng2ComponentB {
2328+
}
2329+
2330+
// Define `ng1Module`
2331+
angular.module('ng1Module', [])
2332+
.component('ng1', ng1Component)
2333+
.directive('ng2A', adapter.downgradeNg2Component(Ng2ComponentA));
2334+
2335+
// Define `Ng2Module`
2336+
@NgModule({
2337+
declarations: [adapter.upgradeNg1Component('ng1'), Ng2ComponentA, Ng2ComponentB],
2338+
entryComponents: [Ng2ComponentA],
2339+
imports: [BrowserModule]
2340+
})
2341+
class Ng2Module {
2342+
ngDoBootstrap() {}
2343+
}
2344+
2345+
// Bootstrap
2346+
const element = html(`<ng2-a></ng2-a>`);
2347+
2348+
adapter.bootstrap(element, ['ng1Module']).ready((ref) => {
2349+
const $rootScope = ref.ng1RootScope as any;
2350+
tick();
2351+
$rootScope.$digest();
2352+
(ng1DescendantElement[0] as HTMLElement).click();
2353+
expect(elementClickListener).toHaveBeenCalledTimes(1);
2354+
expect(descendantClickListener).toHaveBeenCalledTimes(1);
2355+
2356+
ng2ComponentAInstance.destroyIt = true;
2357+
tick();
2358+
$rootScope.$digest();
2359+
2360+
(ng1DescendantElement[0] as HTMLElement).click();
2361+
expect(elementClickListener).toHaveBeenCalledTimes(1);
2362+
expect(descendantClickListener).toHaveBeenCalledTimes(1);
22212363
});
22222364
}));
22232365
});

0 commit comments

Comments
 (0)