GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — fix(upgrade): fix HMR for hybrid applications (#40045) · angular/angular@c4c7509

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
  • goldens/public-api/upgrade/static
    • static.d.ts
  • packages/upgrade
    • src
      • common/src
        • constants.ts
        • util.ts
      • dynamic
        • src
          • upgrade_adapter.ts
        • test
          • upgrade_spec.ts
    • static
      • src
        • downgrade_module.ts
        • upgrade_module.ts
      • test/integration
        • downgrade_component_spec.ts
        • downgrade_module_spec.ts
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎goldens/public-api/upgrade/static/static.d.ts‎

Copy file name to clipboardExpand all lines: goldens/public-api/upgrade/static/static.d.ts
+2-1Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ export declare class UpgradeModule {
3535
ngZone: NgZone;
3636
constructor(
3737
injector: Injector,
38-
ngZone: NgZone);
38+
ngZone: NgZone,
39+
platformRef: PlatformRef);
3940
bootstrap(element: Element, modules?: string[], config?: any): void;
4041
}
4142

Collapse file

‎packages/upgrade/src/common/src/constants.ts‎

Copy file name to clipboardExpand all lines: packages/upgrade/src/common/src/constants.ts
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const $INJECTOR = '$injector';
1515
export const $INTERVAL = '$interval';
1616
export const $PARSE = '$parse';
1717
export const $PROVIDE = '$provide';
18+
export const $ROOT_ELEMENT = '$rootElement';
1819
export const $ROOT_SCOPE = '$rootScope';
1920
export const $SCOPE = '$scope';
2021
export const $TEMPLATE_CACHE = '$templateCache';
Collapse file

‎packages/upgrade/src/common/src/util.ts‎

Copy file name to clipboardExpand all lines: packages/upgrade/src/common/src/util.ts
+19-2Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88

99
import {Injector, Type} from '@angular/core';
1010

11-
import {element as angularElement, IInjectorService, INgModelController} from './angular1';
12-
import {DOWNGRADED_MODULE_COUNT_KEY, UPGRADE_APP_TYPE_KEY} from './constants';
11+
import {element as angularElement, IAugmentedJQuery, IInjectorService, INgModelController, IRootScopeService} from './angular1';
12+
import {$ROOT_ELEMENT, $ROOT_SCOPE, DOWNGRADED_MODULE_COUNT_KEY, UPGRADE_APP_TYPE_KEY} from './constants';
1313

1414
const DIRECTIVE_PREFIX_REGEXP = /^(?:x|data)[:\-_]/i;
1515
const DIRECTIVE_SPECIAL_CHARS_REGEXP = /[:\-_]+(.)/g;
@@ -48,6 +48,23 @@ export function controllerKey(name: string): string {
4848
return '$' + name + 'Controller';
4949
}
5050

51+
/**
52+
* Destroy an AngularJS app given the app `$injector`.
53+
*
54+
* NOTE: Destroying an app is not officially supported by AngularJS, but try to do our best by
55+
* destroying `$rootScope` and clean the jqLite/jQuery data on `$rootElement` and all
56+
* descendants.
57+
*
58+
* @param $injector The `$injector` of the AngularJS app to destroy.
59+
*/
60+
export function destroyApp($injector: IInjectorService): void {
61+
const $rootElement: IAugmentedJQuery = $injector.get($ROOT_ELEMENT);
62+
const $rootScope: IRootScopeService = $injector.get($ROOT_SCOPE);
63+
64+
$rootScope.$destroy();
65+
cleanData($rootElement[0]);
66+
}
67+
5168
export function directiveNormalize(name: string): string {
5269
return name.replace(DIRECTIVE_PREFIX_REGEXP, '')
5370
.replace(DIRECTIVE_SPECIAL_CHARS_REGEXP, (_, letter) => letter.toUpperCase());
Collapse file

‎packages/upgrade/src/dynamic/src/upgrade_adapter.ts‎

Copy file name to clipboardExpand all lines: packages/upgrade/src/dynamic/src/upgrade_adapter.ts
+8-1Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {bootstrap, element as angularElement, IAngularBootstrapConfig, IAugmente
1313
import {$$TESTABILITY, $COMPILE, $INJECTOR, $ROOT_SCOPE, COMPILER_KEY, INJECTOR_KEY, LAZY_MODULE_REF, NG_ZONE_KEY, UPGRADE_APP_TYPE_KEY} from '../../common/src/constants';
1414
import {downgradeComponent} from '../../common/src/downgrade_component';
1515
import {downgradeInjectable} from '../../common/src/downgrade_injectable';
16-
import {controllerKey, Deferred, LazyModuleRef, onError, UpgradeAppType} from '../../common/src/util';
16+
import {controllerKey, Deferred, destroyApp, LazyModuleRef, onError, UpgradeAppType} from '../../common/src/util';
1717

1818
import {UpgradeNg1ComponentAdapterBuilder} from './upgrade_ng1_adapter';
1919

@@ -619,6 +619,13 @@ export class UpgradeAdapter {
619619
rootScope.$on('$destroy', () => {
620620
subscription.unsubscribe();
621621
});
622+
623+
// Destroy the AngularJS app once the Angular `PlatformRef` is destroyed.
624+
// This does not happen in a typical SPA scenario, but it might be useful for
625+
// other use-cases where disposing of an Angular/AngularJS app is necessary
626+
// (such as Hot Module Replacement (HMR)).
627+
// See https://github.com/angular/angular/issues/39935.
628+
platformRef.onDestroy(() => destroyApp(ng1Injector));
622629
});
623630
})
624631
.catch((e) => this.ng2BootstrapDeferred.reject(e));
Collapse file

‎packages/upgrade/src/dynamic/test/upgrade_spec.ts‎

Copy file name to clipboardExpand all lines: packages/upgrade/src/dynamic/test/upgrade_spec.ts
+59-1Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ withEachNg1Version(() => {
8686
});
8787
}));
8888

89-
it('supports the compilerOptions argument', waitForAsync(() => {
89+
it('should support the compilerOptions argument', waitForAsync(() => {
9090
const platformRef = platformBrowserDynamic();
9191
spyOn(platformRef, 'bootstrapModule').and.callThrough();
9292
spyOn(platformRef, 'bootstrapModuleFactory').and.callThrough();
@@ -120,6 +120,64 @@ withEachNg1Version(() => {
120120
ref.dispose();
121121
});
122122
}));
123+
124+
it('should destroy the AngularJS app when `PlatformRef` is destroyed', waitForAsync(() => {
125+
const platformRef = platformBrowserDynamic();
126+
const adapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
127+
const ng1Module = angular.module_('ng1', []);
128+
129+
@Component({selector: 'ng2', template: '<span>NG2</span>'})
130+
class Ng2Component {
131+
}
132+
133+
@NgModule({
134+
declarations: [Ng2Component],
135+
imports: [BrowserModule],
136+
})
137+
class Ng2Module {
138+
ngDoBootstrap() {}
139+
}
140+
141+
ng1Module.component('ng1', {template: '<ng2></ng2>'});
142+
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2Component));
143+
144+
const element = html('<div><ng1></ng1></div>');
145+
146+
adapter.bootstrap(element, [ng1Module.name]).ready(ref => {
147+
const $rootScope: angular.IRootScopeService = ref.ng1Injector.get($ROOT_SCOPE);
148+
const rootScopeDestroySpy = spyOn($rootScope, '$destroy');
149+
150+
const appElem = angular.element(element);
151+
const ng1Elem = angular.element(element.querySelector('ng1') as Element);
152+
const ng2Elem = angular.element(element.querySelector('ng2') as Element);
153+
const ng2ChildElem = angular.element(element.querySelector('ng2 span') as Element);
154+
155+
// Attach data to all elements.
156+
appElem.data!('testData', 1);
157+
ng1Elem.data!('testData', 2);
158+
ng2Elem.data!('testData', 3);
159+
ng2ChildElem.data!('testData', 4);
160+
161+
// Verify data can be retrieved.
162+
expect(appElem.data!('testData')).toBe(1);
163+
expect(ng1Elem.data!('testData')).toBe(2);
164+
expect(ng2Elem.data!('testData')).toBe(3);
165+
expect(ng2ChildElem.data!('testData')).toBe(4);
166+
167+
expect(rootScopeDestroySpy).not.toHaveBeenCalled();
168+
169+
// Destroy `PlatformRef`.
170+
platformRef.destroy();
171+
172+
// Verify `$rootScope` has been destroyed and data has been cleaned up.
173+
expect(rootScopeDestroySpy).toHaveBeenCalled();
174+
175+
expect(appElem.data!('testData')).toBeUndefined();
176+
expect(ng1Elem.data!('testData')).toBeUndefined();
177+
expect(ng2Elem.data!('testData')).toBeUndefined();
178+
expect(ng2ChildElem.data!('testData')).toBeUndefined();
179+
});
180+
}));
123181
});
124182

125183
describe('bootstrap errors', () => {
Collapse file

‎packages/upgrade/static/src/downgrade_module.ts‎

Copy file name to clipboardExpand all lines: packages/upgrade/static/src/downgrade_module.ts
+9-2Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Injector, NgModuleFactory, NgModuleRef, StaticProvider} from '@angular/core';
9+
import {Injector, NgModuleFactory, NgModuleRef, PlatformRef, StaticProvider} from '@angular/core';
1010
import {platformBrowser} from '@angular/platform-browser';
1111

1212
import {IInjectorService, IProvideService, module_ as angularModule} from '../../src/common/src/angular1';
1313
import {$INJECTOR, $PROVIDE, DOWNGRADED_MODULE_COUNT_KEY, INJECTOR_KEY, LAZY_MODULE_REF, UPGRADE_APP_TYPE_KEY, UPGRADE_MODULE_NAME} from '../../src/common/src/constants';
14-
import {getDowngradedModuleCount, isFunction, LazyModuleRef, UpgradeAppType} from '../../src/common/src/util';
14+
import {destroyApp, getDowngradedModuleCount, isFunction, LazyModuleRef, UpgradeAppType} from '../../src/common/src/util';
1515

1616
import {angular1Providers, setTempInjectorRef} from './angular1_providers';
1717
import {NgAdapterInjector} from './util';
@@ -167,6 +167,13 @@ export function downgradeModule<T>(moduleFactoryOrBootstrapFn: NgModuleFactory<T
167167
injector = result.injector = new NgAdapterInjector(ref.injector);
168168
injector.get($INJECTOR);
169169

170+
// Destroy the AngularJS app once the Angular `PlatformRef` is destroyed.
171+
// This does not happen in a typical SPA scenario, but it might be useful for
172+
// other use-cases where disposing of an Angular/AngularJS app is necessary
173+
// (such as Hot Module Replacement (HMR)).
174+
// See https://github.com/angular/angular/issues/39935.
175+
injector.get(PlatformRef).onDestroy(() => destroyApp($injector));
176+
170177
return injector;
171178
})
172179
};
Collapse file

‎packages/upgrade/static/src/upgrade_module.ts‎

Copy file name to clipboardExpand all lines: packages/upgrade/static/src/upgrade_module.ts
+17-4Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Injector, isDevMode, NgModule, NgZone, Testability} from '@angular/core';
9+
import {Injector, isDevMode, NgModule, NgZone, PlatformRef, Testability} from '@angular/core';
1010

1111
import {bootstrap, element as angularElement, IInjectorService, IIntervalService, IProvideService, ITestabilityService, module_ as angularModule} from '../../src/common/src/angular1';
1212
import {$$TESTABILITY, $DELEGATE, $INJECTOR, $INTERVAL, $PROVIDE, INJECTOR_KEY, LAZY_MODULE_REF, UPGRADE_APP_TYPE_KEY, UPGRADE_MODULE_NAME} from '../../src/common/src/constants';
13-
import {controllerKey, LazyModuleRef, UpgradeAppType} from '../../src/common/src/util';
13+
import {controllerKey, destroyApp, LazyModuleRef, UpgradeAppType} from '../../src/common/src/util';
1414

1515
import {angular1Providers, setTempInjectorRef} from './angular1_providers';
1616
import {NgAdapterInjector} from './util';
@@ -155,7 +155,13 @@ export class UpgradeModule {
155155
/** The root `Injector` for the upgrade application. */
156156
injector: Injector,
157157
/** The bootstrap zone for the upgrade application */
158-
public ngZone: NgZone) {
158+
public ngZone: NgZone,
159+
/**
160+
* The owning `NgModuleRef`s `PlatformRef` instance.
161+
* This is used to tie the lifecycle of the bootstrapped AngularJS apps to that of the Angular
162+
* `PlatformRef`.
163+
*/
164+
private platformRef: PlatformRef) {
159165
this.injector = new NgAdapterInjector(injector);
160166
}
161167

@@ -242,6 +248,7 @@ export class UpgradeModule {
242248
$INJECTOR,
243249
($injector: IInjectorService) => {
244250
this.$injector = $injector;
251+
const $rootScope = $injector.get('$rootScope');
245252

246253
// Initialize the ng1 $injector provider
247254
setTempInjectorRef($injector);
@@ -250,10 +257,16 @@ export class UpgradeModule {
250257
// Put the injector on the DOM, so that it can be "required"
251258
angularElement(element).data!(controllerKey(INJECTOR_KEY), this.injector);
252259

260+
// Destroy the AngularJS app once the Angular `PlatformRef` is destroyed.
261+
// This does not happen in a typical SPA scenario, but it might be useful for
262+
// other use-cases where disposing of an Angular/AngularJS app is necessary
263+
// (such as Hot Module Replacement (HMR)).
264+
// See https://github.com/angular/angular/issues/39935.
265+
this.platformRef.onDestroy(() => destroyApp($injector));
266+
253267
// Wire up the ng1 rootScope to run a digest cycle whenever the zone settles
254268
// We need to do this in the next tick so that we don't prevent the bootup stabilizing
255269
setTimeout(() => {
256-
const $rootScope = $injector.get('$rootScope');
257270
const subscription = this.ngZone.onMicrotaskEmpty.subscribe(() => {
258271
if ($rootScope.$$phase) {
259272
if (isDevMode()) {
Collapse file

‎packages/upgrade/static/test/integration/downgrade_component_spec.ts‎

Copy file name to clipboardExpand all lines: packages/upgrade/static/test/integration/downgrade_component_spec.ts
+61Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
1313
import {downgradeComponent, UpgradeComponent, UpgradeModule} from '@angular/upgrade/static';
1414

1515
import * as angular from '../../../src/common/src/angular1';
16+
import {$ROOT_SCOPE} from '../../../src/common/src/constants';
1617
import {html, multiTrim, withEachNg1Version} from '../../../src/common/test/helpers/common_test_helpers';
1718

1819
import {$apply, bootstrap} from './static_test_helpers';
@@ -648,6 +649,66 @@ withEachNg1Version(() => {
648649
});
649650
}));
650651

652+
it('should destroy the AngularJS app when `PlatformRef` is destroyed', waitForAsync(() => {
653+
@Component({selector: 'ng2', template: '<span>NG2</span>'})
654+
class Ng2Component {
655+
}
656+
657+
@NgModule({
658+
declarations: [Ng2Component],
659+
entryComponents: [Ng2Component],
660+
imports: [BrowserModule, UpgradeModule],
661+
})
662+
class Ng2Module {
663+
ngDoBootstrap() {}
664+
}
665+
666+
const ng1Module = angular.module_('ng1', [])
667+
.component('ng1', {template: '<ng2></ng2>'})
668+
.directive('ng2', downgradeComponent({component: Ng2Component}));
669+
670+
const element = html('<div><ng1></ng1></div>');
671+
const platformRef = platformBrowserDynamic();
672+
673+
platformRef.bootstrapModule(Ng2Module).then(ref => {
674+
const upgrade = ref.injector.get(UpgradeModule);
675+
upgrade.bootstrap(element, [ng1Module.name]);
676+
677+
const $rootScope: angular.IRootScopeService = upgrade.$injector.get($ROOT_SCOPE);
678+
const rootScopeDestroySpy = spyOn($rootScope, '$destroy');
679+
680+
const appElem = angular.element(element);
681+
const ng1Elem = angular.element(element.querySelector('ng1') as Element);
682+
const ng2Elem = angular.element(element.querySelector('ng2') as Element);
683+
const ng2ChildElem = angular.element(element.querySelector('ng2 span') as Element);
684+
685+
// Attach data to all elements.
686+
appElem.data!('testData', 1);
687+
ng1Elem.data!('testData', 2);
688+
ng2Elem.data!('testData', 3);
689+
ng2ChildElem.data!('testData', 4);
690+
691+
// Verify data can be retrieved.
692+
expect(appElem.data!('testData')).toBe(1);
693+
expect(ng1Elem.data!('testData')).toBe(2);
694+
expect(ng2Elem.data!('testData')).toBe(3);
695+
expect(ng2ChildElem.data!('testData')).toBe(4);
696+
697+
expect(rootScopeDestroySpy).not.toHaveBeenCalled();
698+
699+
// Destroy `PlatformRef`.
700+
platformRef.destroy();
701+
702+
// Verify `$rootScope` has been destroyed and data has been cleaned up.
703+
expect(rootScopeDestroySpy).toHaveBeenCalled();
704+
705+
expect(appElem.data!('testData')).toBeUndefined();
706+
expect(ng1Elem.data!('testData')).toBeUndefined();
707+
expect(ng2Elem.data!('testData')).toBeUndefined();
708+
expect(ng2ChildElem.data!('testData')).toBeUndefined();
709+
});
710+
}));
711+
651712
it('should work when compiled outside the dom (by fallback to the root ng2.injector)',
652713
waitForAsync(() => {
653714
@Component({selector: 'ng2', template: 'test'})
Collapse file

‎packages/upgrade/static/test/integration/downgrade_module_spec.ts‎

Copy file name to clipboardExpand all lines: packages/upgrade/static/test/integration/downgrade_module_spec.ts
+62Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1353,6 +1353,68 @@ withEachNg1Version(() => {
13531353
setTimeout(() => expect($injectorFromNg2).toBe($injectorFromNg1));
13541354
}));
13551355

1356+
it('should destroy the AngularJS app when `PlatformRef` is destroyed', waitForAsync(() => {
1357+
@Component({selector: 'ng2', template: '<span>NG2</span>'})
1358+
class Ng2Component {
1359+
}
1360+
1361+
@NgModule({
1362+
declarations: [Ng2Component],
1363+
entryComponents: [Ng2Component],
1364+
imports: [BrowserModule],
1365+
})
1366+
class Ng2Module {
1367+
ngDoBootstrap() {}
1368+
}
1369+
1370+
const bootstrapFn = (extraProviders: StaticProvider[]) =>
1371+
platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module);
1372+
const lazyModuleName = downgradeModule<Ng2Module>(bootstrapFn);
1373+
const ng1Module =
1374+
angular.module_('ng1', [lazyModuleName])
1375+
.component('ng1', {template: '<ng2></ng2>'})
1376+
.directive(
1377+
'ng2', downgradeComponent({component: Ng2Component, propagateDigest}));
1378+
1379+
const element = html('<div><ng1></ng1></div>');
1380+
const $injector = angular.bootstrap(element, [ng1Module.name]);
1381+
1382+
setTimeout(() => { // Wait for the module to be bootstrapped.
1383+
const $rootScope: angular.IRootScopeService = $injector.get($ROOT_SCOPE);
1384+
const rootScopeDestroySpy = spyOn($rootScope, '$destroy');
1385+
1386+
const appElem = angular.element(element);
1387+
const ng1Elem = angular.element(element.querySelector('ng1') as Element);
1388+
const ng2Elem = angular.element(element.querySelector('ng2') as Element);
1389+
const ng2ChildElem = angular.element(element.querySelector('ng2 span') as Element);
1390+
1391+
// Attach data to all elements.
1392+
appElem.data!('testData', 1);
1393+
ng1Elem.data!('testData', 2);
1394+
ng2Elem.data!('testData', 3);
1395+
ng2ChildElem.data!('testData', 4);
1396+
1397+
// Verify data can be retrieved.
1398+
expect(appElem.data!('testData')).toBe(1);
1399+
expect(ng1Elem.data!('testData')).toBe(2);
1400+
expect(ng2Elem.data!('testData')).toBe(3);
1401+
expect(ng2ChildElem.data!('testData')).toBe(4);
1402+
1403+
expect(rootScopeDestroySpy).not.toHaveBeenCalled();
1404+
1405+
// Destroy `PlatformRef`.
1406+
getPlatform()?.destroy();
1407+
1408+
// Verify `$rootScope` has been destroyed and data has been cleaned up.
1409+
expect(rootScopeDestroySpy).toHaveBeenCalled();
1410+
1411+
expect(appElem.data!('testData')).toBeUndefined();
1412+
expect(ng1Elem.data!('testData')).toBeUndefined();
1413+
expect(ng2Elem.data!('testData')).toBeUndefined();
1414+
expect(ng2ChildElem.data!('testData')).toBeUndefined();
1415+
});
1416+
}));
1417+
13561418
describe('(common error)', () => {
13571419
let Ng2CompA: Type<any>;
13581420
let Ng2CompB: Type<any>;

0 commit comments

Comments
0 (0)
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.