diff --git a/goldens/public-api/upgrade/static/static.d.ts b/goldens/public-api/upgrade/static/static.d.ts
index 47b7425ce925..d21bc5784849 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 7c9c5d0c8f76..640d23adf0ea 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/downgrade_component_adapter.ts b/packages/upgrade/src/common/src/downgrade_component_adapter.ts
index 5022a58aaca2..8711624f3bad 100644
--- a/packages/upgrade/src/common/src/downgrade_component_adapter.ts
+++ b/packages/upgrade/src/common/src/downgrade_component_adapter.ts
@@ -8,10 +8,10 @@
import {ApplicationRef, ChangeDetectorRef, ComponentFactory, ComponentRef, EventEmitter, Injector, OnChanges, SimpleChange, SimpleChanges, StaticProvider, Testability, TestabilityRegistry, Type} from '@angular/core';
-import {element as angularElement, IAttributes, IAugmentedJQuery, ICompileService, INgModelController, IParseService, IScope} from './angular1';
+import {IAttributes, IAugmentedJQuery, ICompileService, INgModelController, IParseService, IScope} from './angular1';
import {PropertyBinding} from './component_info';
import {$SCOPE} from './constants';
-import {getTypeName, hookupNgModel, strictEquals} from './util';
+import {cleanData, getTypeName, hookupNgModel, strictEquals} from './util';
const INITIAL_VALUE = {
__UNINITIALIZED__: true
@@ -241,12 +241,7 @@ export class DowngradeComponentAdapter {
//
// To ensure the element is always properly cleaned up, we manually call `cleanData()` on
// this element and its descendants before destroying the `ComponentRef`.
- //
- // NOTE:
- // `cleanData()` also will invoke the AngularJS `$destroy` event on the element:
- // https://github.com/angular/angular.js/blob/2e72ea13fa98bebf6ed4b5e3c45eaf5f990ed16f/src/Angular.js#L1932-L1945
- angularElement.cleanData(this.element);
- angularElement.cleanData((this.element[0] as Element).querySelectorAll('*'));
+ cleanData(this.element[0]);
destroyComponentRef();
}
diff --git a/packages/upgrade/src/common/src/upgrade_helper.ts b/packages/upgrade/src/common/src/upgrade_helper.ts
index 0d520eb9e6a4..71a0c56f9233 100644
--- a/packages/upgrade/src/common/src/upgrade_helper.ts
+++ b/packages/upgrade/src/common/src/upgrade_helper.ts
@@ -10,7 +10,7 @@ import {ElementRef, Injector, SimpleChanges} from '@angular/core';
import {DirectiveRequireProperty, element as angularElement, IAugmentedJQuery, ICloneAttachFunction, ICompileService, IController, IControllerService, IDirective, IHttpBackendService, IInjectorService, ILinkFn, IScope, ITemplateCacheService, SingleOrListOrMap} from './angular1';
import {$COMPILE, $CONTROLLER, $HTTP_BACKEND, $INJECTOR, $TEMPLATE_CACHE} from './constants';
-import {controllerKey, directiveNormalize, isFunction} from './util';
+import {cleanData, controllerKey, directiveNormalize, isFunction} from './util';
@@ -125,15 +125,7 @@ export class UpgradeHelper {
controllerInstance.$onDestroy();
}
$scope.$destroy();
-
- // Clean the jQuery/jqLite data on the component+child elements.
- // Equivelent to how jQuery/jqLite invoke `cleanData` on an Element (this.element)
- // https://github.com/jquery/jquery/blob/e743cbd28553267f955f71ea7248377915613fd9/src/manipulation.js#L223
- // https://github.com/angular/angular.js/blob/26ddc5f830f902a3d22f4b2aab70d86d4d688c82/src/jqLite.js#L306-L312
- // `cleanData` will invoke the AngularJS `$destroy` DOM event
- // https://github.com/angular/angular.js/blob/26ddc5f830f902a3d22f4b2aab70d86d4d688c82/src/Angular.js#L1911-L1924
- angularElement.cleanData([this.element]);
- angularElement.cleanData(this.element.querySelectorAll('*'));
+ cleanData(this.element);
}
prepareTransclusion(): ILinkFn|undefined {
diff --git a/packages/upgrade/src/common/src/util.ts b/packages/upgrade/src/common/src/util.ts
index 1c772f9bb421..1ac7ecb573d8 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 {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;
@@ -25,10 +25,46 @@ export function onError(e: any) {
throw e;
}
+/**
+ * Clean the jqLite/jQuery data on the element and all its descendants.
+ * Equivalent to how jqLite/jQuery invoke `cleanData()` on an Element when removed:
+ * https://github.com/angular/angular.js/blob/2e72ea13fa98bebf6ed4b5e3c45eaf5f990ed16f/src/jqLite.js#L349-L355
+ * https://github.com/jquery/jquery/blob/6984d1747623dbc5e87fd6c261a5b6b1628c107c/src/manipulation.js#L182
+ *
+ * NOTE:
+ * `cleanData()` will also invoke the AngularJS `$destroy` DOM event on the element:
+ * https://github.com/angular/angular.js/blob/2e72ea13fa98bebf6ed4b5e3c45eaf5f990ed16f/src/Angular.js#L1932-L1945
+ *
+ * @param node The DOM node whose data needs to be cleaned.
+ */
+export function cleanData(node: Node): void {
+ angularElement.cleanData([node]);
+ if (isParentNode(node)) {
+ angularElement.cleanData(node.querySelectorAll('*'));
+ }
+}
+
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 try to do our best by
+ * destroying `$rootScope` and clean the jqLite/jQuery data on `$rootElement` and all
+ * descendants.
+ *
+ * @param $injector The `$injector` of the AngularJS app to destroy.
+ */
+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());
@@ -53,6 +89,10 @@ export function isFunction(value: any): value is Function {
return typeof value === 'function';
}
+function isParentNode(node: Node|ParentNode): node is ParentNode {
+ return isFunction((node as unknown as ParentNode).querySelectorAll);
+}
+
export function validateInjectionKey(
$injector: IInjectorService, downgradedModule: string, injectionKey: string,
attemptedAction: string): void {
diff --git a/packages/upgrade/src/dynamic/src/upgrade_adapter.ts b/packages/upgrade/src/dynamic/src/upgrade_adapter.ts
index bbb304e494c0..beb411505fe3 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';
@@ -505,7 +505,6 @@ export class UpgradeAdapter {
const delayApplyExps: Function[] = [];
let original$applyFn: Function;
let rootScopePrototype: any;
- let rootScope: IRootScopeService;
const upgradeAdapter = this;
const ng1Module = this.ng1Module = angularModule(this.idPrefix, modules);
const platformRef = platformBrowserDynamic();
@@ -533,7 +532,7 @@ export class UpgradeAdapter {
} else {
throw new Error('Failed to find \'$apply\' on \'$rootScope\'!');
}
- return rootScope = rootScopeDelegate;
+ return rootScopeDelegate;
}
]);
if (ng1Injector.has($$TESTABILITY)) {
@@ -620,6 +619,13 @@ 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 use-cases where disposing of an Angular/AngularJS app is necessary
+ // (such as Hot Module Replacement (HMR)).
+ // See https://github.com/angular/angular/issues/39935.
+ 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 3d33cc59be02..70bbbc66e38f 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 bde2fe8db93c..cfec3e94d020 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,13 @@ 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 899d5ab59c4c..1e54a7834a8c 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);
}
@@ -170,111 +176,115 @@ export class UpgradeModule {
const INIT_MODULE_NAME = UPGRADE_MODULE_NAME + '.init';
// Create an ng1 module to bootstrap
- const initModule =
- angularModule(INIT_MODULE_NAME, [])
+ angularModule(INIT_MODULE_NAME, [])
- .constant(UPGRADE_APP_TYPE_KEY, UpgradeAppType.Static)
+ .constant(UPGRADE_APP_TYPE_KEY, UpgradeAppType.Static)
- .value(INJECTOR_KEY, this.injector)
+ .value(INJECTOR_KEY, this.injector)
- .factory(
- LAZY_MODULE_REF,
- [INJECTOR_KEY, (injector: Injector) => ({injector} as LazyModuleRef)])
+ .factory(
+ LAZY_MODULE_REF, [INJECTOR_KEY, (injector: Injector) => ({injector} as LazyModuleRef)])
- .config([
- $PROVIDE, $INJECTOR,
- ($provide: IProvideService, $injector: IInjectorService) => {
- if ($injector.has($$TESTABILITY)) {
- $provide.decorator($$TESTABILITY, [
- $DELEGATE,
- (testabilityDelegate: ITestabilityService) => {
- const originalWhenStable: Function = testabilityDelegate.whenStable;
- const injector = this.injector;
- // Cannot use arrow function below because we need the context
- const newWhenStable = function(callback: Function) {
- originalWhenStable.call(testabilityDelegate, function() {
- const ng2Testability: Testability = injector.get(Testability);
- if (ng2Testability.isStable()) {
- callback();
- } else {
- ng2Testability.whenStable(
- newWhenStable.bind(testabilityDelegate, callback));
- }
- });
- };
+ .config([
+ $PROVIDE, $INJECTOR,
+ ($provide: IProvideService, $injector: IInjectorService) => {
+ if ($injector.has($$TESTABILITY)) {
+ $provide.decorator($$TESTABILITY, [
+ $DELEGATE,
+ (testabilityDelegate: ITestabilityService) => {
+ const originalWhenStable: Function = testabilityDelegate.whenStable;
+ const injector = this.injector;
+ // Cannot use arrow function below because we need the context
+ const newWhenStable = function(callback: Function) {
+ originalWhenStable.call(testabilityDelegate, function() {
+ const ng2Testability: Testability = injector.get(Testability);
+ if (ng2Testability.isStable()) {
+ callback();
+ } else {
+ ng2Testability.whenStable(
+ newWhenStable.bind(testabilityDelegate, callback));
+ }
+ });
+ };
- testabilityDelegate.whenStable = newWhenStable;
- return testabilityDelegate;
- }
- ]);
+ testabilityDelegate.whenStable = newWhenStable;
+ return testabilityDelegate;
}
+ ]);
+ }
- if ($injector.has($INTERVAL)) {
- $provide.decorator($INTERVAL, [
- $DELEGATE,
- (intervalDelegate: IIntervalService) => {
- // Wrap the $interval service so that setInterval is called outside NgZone,
- // but the callback is still invoked within it. This is so that $interval
- // won't block stability, which preserves the behavior from AngularJS.
- let wrappedInterval =
- (fn: Function, delay: number, count?: number, invokeApply?: boolean,
- ...pass: any[]) => {
- return this.ngZone.runOutsideAngular(() => {
- return intervalDelegate((...args: any[]) => {
- // Run callback in the next VM turn - $interval calls
- // $rootScope.$apply, and running the callback in NgZone will
- // cause a '$digest already in progress' error if it's in the
- // same vm turn.
- setTimeout(() => {
- this.ngZone.run(() => fn(...args));
- });
- }, delay, count, invokeApply, ...pass);
+ if ($injector.has($INTERVAL)) {
+ $provide.decorator($INTERVAL, [
+ $DELEGATE,
+ (intervalDelegate: IIntervalService) => {
+ // Wrap the $interval service so that setInterval is called outside NgZone,
+ // but the callback is still invoked within it. This is so that $interval
+ // won't block stability, which preserves the behavior from AngularJS.
+ let wrappedInterval =
+ (fn: Function, delay: number, count?: number, invokeApply?: boolean,
+ ...pass: any[]) => {
+ return this.ngZone.runOutsideAngular(() => {
+ return intervalDelegate((...args: any[]) => {
+ // Run callback in the next VM turn - $interval calls
+ // $rootScope.$apply, and running the callback in NgZone will
+ // cause a '$digest already in progress' error if it's in the
+ // same vm turn.
+ setTimeout(() => {
+ this.ngZone.run(() => fn(...args));
});
- };
+ }, delay, count, invokeApply, ...pass);
+ });
+ };
- (wrappedInterval as any)['cancel'] = intervalDelegate.cancel;
- return wrappedInterval;
- }
- ]);
+ (wrappedInterval as any)['cancel'] = intervalDelegate.cancel;
+ return wrappedInterval;
}
- }
- ])
+ ]);
+ }
+ }
+ ])
- .run([
- $INJECTOR,
- ($injector: IInjectorService) => {
- this.$injector = $injector;
+ .run([
+ $INJECTOR,
+ ($injector: IInjectorService) => {
+ this.$injector = $injector;
+ const $rootScope = $injector.get('$rootScope');
- // Initialize the ng1 $injector provider
- setTempInjectorRef($injector);
- this.injector.get($INJECTOR);
+ // Initialize the ng1 $injector provider
+ setTempInjectorRef($injector);
+ this.injector.get($INJECTOR);
- // Put the injector on the DOM, so that it can be "required"
- angularElement(element).data!(controllerKey(INJECTOR_KEY), this.injector);
+ // Put the injector on the DOM, so that it can be "required"
+ angularElement(element).data!(controllerKey(INJECTOR_KEY), this.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()) {
- console.warn(
- 'A digest was triggered while one was already in progress. This may mean that something is triggering digests outside the Angular zone.');
- }
+ // 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 use-cases where disposing of an Angular/AngularJS app is necessary
+ // (such as Hot Module Replacement (HMR)).
+ // See https://github.com/angular/angular/issues/39935.
+ this.platformRef.onDestroy(() => destroyApp($injector));
- return $rootScope.$evalAsync();
- }
+ // 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 subscription = this.ngZone.onMicrotaskEmpty.subscribe(() => {
+ if ($rootScope.$$phase) {
+ if (isDevMode()) {
+ console.warn(
+ 'A digest was triggered while one was already in progress. This may mean that something is triggering digests outside the Angular zone.');
+ }
+
+ return $rootScope.$evalAsync();
+ }
- return $rootScope.$digest();
- });
- $rootScope.$on('$destroy', () => {
- subscription.unsubscribe();
- });
- }, 0);
- }
- ]);
+ return $rootScope.$digest();
+ });
+ $rootScope.$on('$destroy', () => {
+ subscription.unsubscribe();
+ });
+ }, 0);
+ }
+ ]);
const upgradeModule = angularModule(UPGRADE_MODULE_NAME, [INIT_MODULE_NAME].concat(modules));
diff --git a/packages/upgrade/static/test/integration/downgrade_component_spec.ts b/packages/upgrade/static/test/integration/downgrade_component_spec.ts
index ec88d423d158..5076831f8f9b 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 cc34ec57b7f5..1b2f4b9f4f0d 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;