diff --git a/goldens/public-api/upgrade/static/static.d.ts b/goldens/public-api/upgrade/static/static.d.ts
index 47b7425ce9258..d21bc57848499 100644
--- a/goldens/public-api/upgrade/static/static.d.ts
+++ b/goldens/public-api/upgrade/static/static.d.ts
@@ -35,7 +35,8 @@ export declare class UpgradeModule {
ngZone: NgZone;
constructor(
injector: Injector,
- ngZone: NgZone);
+ ngZone: NgZone,
+ platformRef: PlatformRef);
bootstrap(element: Element, modules?: string[], config?: any): void;
}
diff --git a/packages/upgrade/src/common/src/constants.ts b/packages/upgrade/src/common/src/constants.ts
index 7c9c5d0c8f766..640d23adf0ea0 100644
--- a/packages/upgrade/src/common/src/constants.ts
+++ b/packages/upgrade/src/common/src/constants.ts
@@ -15,6 +15,7 @@ export const $INJECTOR = '$injector';
export const $INTERVAL = '$interval';
export const $PARSE = '$parse';
export const $PROVIDE = '$provide';
+export const $ROOT_ELEMENT = '$rootElement';
export const $ROOT_SCOPE = '$rootScope';
export const $SCOPE = '$scope';
export const $TEMPLATE_CACHE = '$templateCache';
diff --git a/packages/upgrade/src/common/src/util.ts b/packages/upgrade/src/common/src/util.ts
index 5dbee8cb834ad..3152528191d38 100644
--- a/packages/upgrade/src/common/src/util.ts
+++ b/packages/upgrade/src/common/src/util.ts
@@ -8,8 +8,8 @@
import {Injector, Type} from '@angular/core';
-import {element as angularElement, IInjectorService, INgModelController} from './angular1';
-import {DOWNGRADED_MODULE_COUNT_KEY, UPGRADE_APP_TYPE_KEY} from './constants';
+import {element as angularElement, IAugmentedJQuery, IInjectorService, INgModelController, IRootScopeService} from './angular1';
+import {$ROOT_ELEMENT, $ROOT_SCOPE, DOWNGRADED_MODULE_COUNT_KEY, UPGRADE_APP_TYPE_KEY} from './constants';
const DIRECTIVE_PREFIX_REGEXP = /^(?:x|data)[:\-_]/i;
const DIRECTIVE_SPECIAL_CHARS_REGEXP = /[:\-_]+(.)/g;
@@ -44,6 +44,17 @@ export function controllerKey(name: string): string {
return '$' + name + 'Controller';
}
+// Destroy an AngularJS app given the app `$injector`.
+//
+// NOTE: Destroying an app is not officially supported by AngularJS, but we do our best.
+export function destroyApp($injector: IInjectorService): void {
+ const $rootElement: IAugmentedJQuery = $injector.get($ROOT_ELEMENT);
+ const $rootScope: IRootScopeService = $injector.get($ROOT_SCOPE);
+
+ $rootScope.$destroy();
+ cleanData($rootElement[0]);
+}
+
export function directiveNormalize(name: string): string {
return name.replace(DIRECTIVE_PREFIX_REGEXP, '')
.replace(DIRECTIVE_SPECIAL_CHARS_REGEXP, (_, letter) => letter.toUpperCase());
diff --git a/packages/upgrade/src/dynamic/src/upgrade_adapter.ts b/packages/upgrade/src/dynamic/src/upgrade_adapter.ts
index 0008620b12ff1..bf5edf0f2d685 100644
--- a/packages/upgrade/src/dynamic/src/upgrade_adapter.ts
+++ b/packages/upgrade/src/dynamic/src/upgrade_adapter.ts
@@ -13,7 +13,7 @@ import {bootstrap, element as angularElement, IAngularBootstrapConfig, IAugmente
import {$$TESTABILITY, $COMPILE, $INJECTOR, $ROOT_SCOPE, COMPILER_KEY, INJECTOR_KEY, LAZY_MODULE_REF, NG_ZONE_KEY, UPGRADE_APP_TYPE_KEY} from '../../common/src/constants';
import {downgradeComponent} from '../../common/src/downgrade_component';
import {downgradeInjectable} from '../../common/src/downgrade_injectable';
-import {controllerKey, Deferred, LazyModuleRef, onError, UpgradeAppType} from '../../common/src/util';
+import {controllerKey, Deferred, destroyApp, LazyModuleRef, onError, UpgradeAppType} from '../../common/src/util';
import {UpgradeNg1ComponentAdapterBuilder} from './upgrade_ng1_adapter';
@@ -619,6 +619,12 @@ export class UpgradeAdapter {
rootScope.$on('$destroy', () => {
subscription.unsubscribe();
});
+
+ // Destroy the AngularJS app once the Angular `PlatformRef` is destroyed.
+ // This does not happen in a typical SPA scenario, but it might be useful for
+ // other usecases where desposing of an Angular/AngularJS app is necessary (such
+ // as Hot Module Replacement (HMR)).
+ platformRef.onDestroy(() => destroyApp(ng1Injector));
});
})
.catch((e) => this.ng2BootstrapDeferred.reject(e));
diff --git a/packages/upgrade/src/dynamic/test/upgrade_spec.ts b/packages/upgrade/src/dynamic/test/upgrade_spec.ts
index 3d33cc59be02d..70bbbc66e38f9 100644
--- a/packages/upgrade/src/dynamic/test/upgrade_spec.ts
+++ b/packages/upgrade/src/dynamic/test/upgrade_spec.ts
@@ -86,7 +86,7 @@ withEachNg1Version(() => {
});
}));
- it('supports the compilerOptions argument', waitForAsync(() => {
+ it('should support the compilerOptions argument', waitForAsync(() => {
const platformRef = platformBrowserDynamic();
spyOn(platformRef, 'bootstrapModule').and.callThrough();
spyOn(platformRef, 'bootstrapModuleFactory').and.callThrough();
@@ -120,6 +120,64 @@ withEachNg1Version(() => {
ref.dispose();
});
}));
+
+ it('should destroy the AngularJS app when `PlatformRef` is destroyed', waitForAsync(() => {
+ const platformRef = platformBrowserDynamic();
+ const adapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
+ const ng1Module = angular.module_('ng1', []);
+
+ @Component({selector: 'ng2', template: 'NG2'})
+ class Ng2Component {
+ }
+
+ @NgModule({
+ declarations: [Ng2Component],
+ imports: [BrowserModule],
+ })
+ class Ng2Module {
+ ngDoBootstrap() {}
+ }
+
+ ng1Module.component('ng1', {template: ''});
+ ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2Component));
+
+ const element = html('
');
+
+ adapter.bootstrap(element, [ng1Module.name]).ready(ref => {
+ const $rootScope: angular.IRootScopeService = ref.ng1Injector.get($ROOT_SCOPE);
+ const rootScopeDestroySpy = spyOn($rootScope, '$destroy');
+
+ const appElem = angular.element(element);
+ const ng1Elem = angular.element(element.querySelector('ng1') as Element);
+ const ng2Elem = angular.element(element.querySelector('ng2') as Element);
+ const ng2ChildElem = angular.element(element.querySelector('ng2 span') as Element);
+
+ // Attach data to all elements.
+ appElem.data!('testData', 1);
+ ng1Elem.data!('testData', 2);
+ ng2Elem.data!('testData', 3);
+ ng2ChildElem.data!('testData', 4);
+
+ // Verify data can be retrieved.
+ expect(appElem.data!('testData')).toBe(1);
+ expect(ng1Elem.data!('testData')).toBe(2);
+ expect(ng2Elem.data!('testData')).toBe(3);
+ expect(ng2ChildElem.data!('testData')).toBe(4);
+
+ expect(rootScopeDestroySpy).not.toHaveBeenCalled();
+
+ // Destroy `PlatformRef`.
+ platformRef.destroy();
+
+ // Verify `$rootScope` has been destroyed and data has been cleaned up.
+ expect(rootScopeDestroySpy).toHaveBeenCalled();
+
+ expect(appElem.data!('testData')).toBeUndefined();
+ expect(ng1Elem.data!('testData')).toBeUndefined();
+ expect(ng2Elem.data!('testData')).toBeUndefined();
+ expect(ng2ChildElem.data!('testData')).toBeUndefined();
+ });
+ }));
});
describe('bootstrap errors', () => {
diff --git a/packages/upgrade/static/src/downgrade_module.ts b/packages/upgrade/static/src/downgrade_module.ts
index bde2fe8db93c2..070fd23d7e4fd 100644
--- a/packages/upgrade/static/src/downgrade_module.ts
+++ b/packages/upgrade/static/src/downgrade_module.ts
@@ -6,12 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {Injector, NgModuleFactory, NgModuleRef, StaticProvider} from '@angular/core';
+import {Injector, NgModuleFactory, NgModuleRef, PlatformRef, StaticProvider} from '@angular/core';
import {platformBrowser} from '@angular/platform-browser';
import {IInjectorService, IProvideService, module_ as angularModule} from '../../src/common/src/angular1';
import {$INJECTOR, $PROVIDE, DOWNGRADED_MODULE_COUNT_KEY, INJECTOR_KEY, LAZY_MODULE_REF, UPGRADE_APP_TYPE_KEY, UPGRADE_MODULE_NAME} from '../../src/common/src/constants';
-import {getDowngradedModuleCount, isFunction, LazyModuleRef, UpgradeAppType} from '../../src/common/src/util';
+import {destroyApp, getDowngradedModuleCount, isFunction, LazyModuleRef, UpgradeAppType} from '../../src/common/src/util';
import {angular1Providers, setTempInjectorRef} from './angular1_providers';
import {NgAdapterInjector} from './util';
@@ -167,6 +167,12 @@ export function downgradeModule(moduleFactoryOrBootstrapFn: NgModuleFactory destroyApp($injector));
+
return injector;
})
};
diff --git a/packages/upgrade/static/src/upgrade_module.ts b/packages/upgrade/static/src/upgrade_module.ts
index b3e32bc9861c3..2ae8df10a50a5 100644
--- a/packages/upgrade/static/src/upgrade_module.ts
+++ b/packages/upgrade/static/src/upgrade_module.ts
@@ -6,11 +6,11 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {Injector, isDevMode, NgModule, NgZone, Testability} from '@angular/core';
+import {Injector, isDevMode, NgModule, NgZone, PlatformRef, Testability} from '@angular/core';
import {bootstrap, element as angularElement, IInjectorService, IIntervalService, IProvideService, ITestabilityService, module_ as angularModule} from '../../src/common/src/angular1';
import {$$TESTABILITY, $DELEGATE, $INJECTOR, $INTERVAL, $PROVIDE, INJECTOR_KEY, LAZY_MODULE_REF, UPGRADE_APP_TYPE_KEY, UPGRADE_MODULE_NAME} from '../../src/common/src/constants';
-import {controllerKey, LazyModuleRef, UpgradeAppType} from '../../src/common/src/util';
+import {controllerKey, destroyApp, LazyModuleRef, UpgradeAppType} from '../../src/common/src/util';
import {angular1Providers, setTempInjectorRef} from './angular1_providers';
import {NgAdapterInjector} from './util';
@@ -155,7 +155,13 @@ export class UpgradeModule {
/** The root `Injector` for the upgrade application. */
injector: Injector,
/** The bootstrap zone for the upgrade application */
- public ngZone: NgZone) {
+ public ngZone: NgZone,
+ /**
+ * The owning `NgModuleRef`s `PlatformRef` instance.
+ * This is used to tie the lifecycle of the bootstrapped AngularJS apps to that of the Angular
+ * `PlatformRef`.
+ */
+ private platformRef: PlatformRef) {
this.injector = new NgAdapterInjector(injector);
}
@@ -242,6 +248,7 @@ export class UpgradeModule {
$INJECTOR,
($injector: IInjectorService) => {
this.$injector = $injector;
+ const $rootScope = $injector.get('$rootScope');
// Initialize the ng1 $injector provider
setTempInjectorRef($injector);
@@ -250,10 +257,15 @@ export class UpgradeModule {
// Put the injector on the DOM, so that it can be "required"
angularElement(element).data!(controllerKey(INJECTOR_KEY), this.injector);
+ // Destroy the AngularJS app once the Angular `PlatformRef` is destroyed.
+ // This does not happen in a typical SPA scenario, but it might be useful for
+ // other usecases where desposing of an Angular/AngularJS app is necessary (such
+ // as Hot Module Replacement (HMR)).
+ this.platformRef.onDestroy(() => destroyApp($injector));
+
// Wire up the ng1 rootScope to run a digest cycle whenever the zone settles
// We need to do this in the next tick so that we don't prevent the bootup stabilizing
setTimeout(() => {
- const $rootScope = $injector.get('$rootScope');
const subscription = this.ngZone.onMicrotaskEmpty.subscribe(() => {
if ($rootScope.$$phase) {
if (isDevMode()) {
diff --git a/packages/upgrade/static/test/integration/downgrade_component_spec.ts b/packages/upgrade/static/test/integration/downgrade_component_spec.ts
index ec88d423d158a..5076831f8f9b6 100644
--- a/packages/upgrade/static/test/integration/downgrade_component_spec.ts
+++ b/packages/upgrade/static/test/integration/downgrade_component_spec.ts
@@ -13,6 +13,7 @@ import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {downgradeComponent, UpgradeComponent, UpgradeModule} from '@angular/upgrade/static';
import * as angular from '../../../src/common/src/angular1';
+import {$ROOT_SCOPE} from '../../../src/common/src/constants';
import {html, multiTrim, withEachNg1Version} from '../../../src/common/test/helpers/common_test_helpers';
import {$apply, bootstrap} from './static_test_helpers';
@@ -648,6 +649,66 @@ withEachNg1Version(() => {
});
}));
+ it('should destroy the AngularJS app when `PlatformRef` is destroyed', waitForAsync(() => {
+ @Component({selector: 'ng2', template: 'NG2'})
+ class Ng2Component {
+ }
+
+ @NgModule({
+ declarations: [Ng2Component],
+ entryComponents: [Ng2Component],
+ imports: [BrowserModule, UpgradeModule],
+ })
+ class Ng2Module {
+ ngDoBootstrap() {}
+ }
+
+ const ng1Module = angular.module_('ng1', [])
+ .component('ng1', {template: ''})
+ .directive('ng2', downgradeComponent({component: Ng2Component}));
+
+ const element = html('
');
+ const platformRef = platformBrowserDynamic();
+
+ platformRef.bootstrapModule(Ng2Module).then(ref => {
+ const upgrade = ref.injector.get(UpgradeModule);
+ upgrade.bootstrap(element, [ng1Module.name]);
+
+ const $rootScope: angular.IRootScopeService = upgrade.$injector.get($ROOT_SCOPE);
+ const rootScopeDestroySpy = spyOn($rootScope, '$destroy');
+
+ const appElem = angular.element(element);
+ const ng1Elem = angular.element(element.querySelector('ng1') as Element);
+ const ng2Elem = angular.element(element.querySelector('ng2') as Element);
+ const ng2ChildElem = angular.element(element.querySelector('ng2 span') as Element);
+
+ // Attach data to all elements.
+ appElem.data!('testData', 1);
+ ng1Elem.data!('testData', 2);
+ ng2Elem.data!('testData', 3);
+ ng2ChildElem.data!('testData', 4);
+
+ // Verify data can be retrieved.
+ expect(appElem.data!('testData')).toBe(1);
+ expect(ng1Elem.data!('testData')).toBe(2);
+ expect(ng2Elem.data!('testData')).toBe(3);
+ expect(ng2ChildElem.data!('testData')).toBe(4);
+
+ expect(rootScopeDestroySpy).not.toHaveBeenCalled();
+
+ // Destroy `PlatformRef`.
+ platformRef.destroy();
+
+ // Verify `$rootScope` has been destroyed and data has been cleaned up.
+ expect(rootScopeDestroySpy).toHaveBeenCalled();
+
+ expect(appElem.data!('testData')).toBeUndefined();
+ expect(ng1Elem.data!('testData')).toBeUndefined();
+ expect(ng2Elem.data!('testData')).toBeUndefined();
+ expect(ng2ChildElem.data!('testData')).toBeUndefined();
+ });
+ }));
+
it('should work when compiled outside the dom (by fallback to the root ng2.injector)',
waitForAsync(() => {
@Component({selector: 'ng2', template: 'test'})
diff --git a/packages/upgrade/static/test/integration/downgrade_module_spec.ts b/packages/upgrade/static/test/integration/downgrade_module_spec.ts
index cc34ec57b7f5f..1b2f4b9f4f0dd 100644
--- a/packages/upgrade/static/test/integration/downgrade_module_spec.ts
+++ b/packages/upgrade/static/test/integration/downgrade_module_spec.ts
@@ -1353,6 +1353,68 @@ withEachNg1Version(() => {
setTimeout(() => expect($injectorFromNg2).toBe($injectorFromNg1));
}));
+ it('should destroy the AngularJS app when `PlatformRef` is destroyed', waitForAsync(() => {
+ @Component({selector: 'ng2', template: 'NG2'})
+ class Ng2Component {
+ }
+
+ @NgModule({
+ declarations: [Ng2Component],
+ entryComponents: [Ng2Component],
+ imports: [BrowserModule],
+ })
+ class Ng2Module {
+ ngDoBootstrap() {}
+ }
+
+ const bootstrapFn = (extraProviders: StaticProvider[]) =>
+ platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module);
+ const lazyModuleName = downgradeModule(bootstrapFn);
+ const ng1Module =
+ angular.module_('ng1', [lazyModuleName])
+ .component('ng1', {template: ''})
+ .directive(
+ 'ng2', downgradeComponent({component: Ng2Component, propagateDigest}));
+
+ const element = html('
');
+ const $injector = angular.bootstrap(element, [ng1Module.name]);
+
+ setTimeout(() => { // Wait for the module to be bootstrapped.
+ const $rootScope: angular.IRootScopeService = $injector.get($ROOT_SCOPE);
+ const rootScopeDestroySpy = spyOn($rootScope, '$destroy');
+
+ const appElem = angular.element(element);
+ const ng1Elem = angular.element(element.querySelector('ng1') as Element);
+ const ng2Elem = angular.element(element.querySelector('ng2') as Element);
+ const ng2ChildElem = angular.element(element.querySelector('ng2 span') as Element);
+
+ // Attach data to all elements.
+ appElem.data!('testData', 1);
+ ng1Elem.data!('testData', 2);
+ ng2Elem.data!('testData', 3);
+ ng2ChildElem.data!('testData', 4);
+
+ // Verify data can be retrieved.
+ expect(appElem.data!('testData')).toBe(1);
+ expect(ng1Elem.data!('testData')).toBe(2);
+ expect(ng2Elem.data!('testData')).toBe(3);
+ expect(ng2ChildElem.data!('testData')).toBe(4);
+
+ expect(rootScopeDestroySpy).not.toHaveBeenCalled();
+
+ // Destroy `PlatformRef`.
+ getPlatform()?.destroy();
+
+ // Verify `$rootScope` has been destroyed and data has been cleaned up.
+ expect(rootScopeDestroySpy).toHaveBeenCalled();
+
+ expect(appElem.data!('testData')).toBeUndefined();
+ expect(ng1Elem.data!('testData')).toBeUndefined();
+ expect(ng2Elem.data!('testData')).toBeUndefined();
+ expect(ng2ChildElem.data!('testData')).toBeUndefined();
+ });
+ }));
+
describe('(common error)', () => {
let Ng2CompA: Type;
let Ng2CompB: Type;