diff --git a/packages/core/src/application_ref.ts b/packages/core/src/application_ref.ts
index d7f024bbb2..ace2cec25b 100644
--- a/packages/core/src/application_ref.ts
+++ b/packages/core/src/application_ref.ts
@@ -804,7 +804,6 @@ export class ApplicationRef {
/** @internal */
ngOnDestroy() {
- // TODO(alxhub): Dispose of the NgZone.
this._views.slice().forEach((view) => view.destroy());
this._onMicrotaskEmptySubscription.unsubscribe();
}
diff --git a/packages/core/test/acceptance/bootstrap_spec.ts b/packages/core/test/acceptance/bootstrap_spec.ts
index 7024c3b6c5..020470d1a8 100644
--- a/packages/core/test/acceptance/bootstrap_spec.ts
+++ b/packages/core/test/acceptance/bootstrap_spec.ts
@@ -6,7 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {COMPILER_OPTIONS, Component, destroyPlatform, NgModule, ViewEncapsulation} from '@angular/core';
+import {ApplicationRef, COMPILER_OPTIONS, Component, destroyPlatform, NgModule, TestabilityRegistry, ViewEncapsulation} from '@angular/core';
+import {expect} from '@angular/core/testing/src/testing_internal';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {onlyInIvy, withBody} from '@angular/private/testing';
@@ -151,6 +152,81 @@ describe('bootstrap', () => {
ngModuleRef.destroy();
}));
+ describe('ApplicationRef cleanup', () => {
+ it('should cleanup ApplicationRef when Injector is destroyed',
+ withBody('', async () => {
+ const TestModule = createComponentAndModule();
+
+ const ngModuleRef = await platformBrowserDynamic().bootstrapModule(TestModule);
+ const appRef = ngModuleRef.injector.get(ApplicationRef);
+ const testabilityRegistry = ngModuleRef.injector.get(TestabilityRegistry);
+
+ expect(appRef.components.length).toBe(1);
+ expect(testabilityRegistry.getAllRootElements().length).toBe(1);
+
+ ngModuleRef.destroy(); // also destroys an Injector instance.
+
+ expect(appRef.components.length).toBe(0);
+ expect(testabilityRegistry.getAllRootElements().length).toBe(0);
+ }));
+
+ it('should cleanup ApplicationRef when ComponentRef is destroyed',
+ withBody('', async () => {
+ const TestModule = createComponentAndModule();
+
+ const ngModuleRef = await platformBrowserDynamic().bootstrapModule(TestModule);
+ const appRef = ngModuleRef.injector.get(ApplicationRef);
+ const testabilityRegistry = ngModuleRef.injector.get(TestabilityRegistry);
+ const componentRef = appRef.components[0];
+
+ expect(appRef.components.length).toBe(1);
+ expect(testabilityRegistry.getAllRootElements().length).toBe(1);
+
+ componentRef.destroy();
+
+ expect(appRef.components.length).toBe(0);
+ expect(testabilityRegistry.getAllRootElements().length).toBe(0);
+ }));
+
+ it('should not throw in case ComponentRef is destroyed and Injector is destroyed after that',
+ withBody('', async () => {
+ const TestModule = createComponentAndModule();
+
+ const ngModuleRef = await platformBrowserDynamic().bootstrapModule(TestModule);
+ const appRef = ngModuleRef.injector.get(ApplicationRef);
+ const testabilityRegistry = ngModuleRef.injector.get(TestabilityRegistry);
+ const componentRef = appRef.components[0];
+
+ expect(appRef.components.length).toBe(1);
+ expect(testabilityRegistry.getAllRootElements().length).toBe(1);
+
+ componentRef.destroy();
+ ngModuleRef.destroy(); // also destroys an Injector instance.
+
+ expect(appRef.components.length).toBe(0);
+ expect(testabilityRegistry.getAllRootElements().length).toBe(0);
+ }));
+
+ it('should not throw in case Injector is destroyed and ComponentRef is destroyed after that',
+ withBody('', async () => {
+ const TestModule = createComponentAndModule();
+
+ const ngModuleRef = await platformBrowserDynamic().bootstrapModule(TestModule);
+ const appRef = ngModuleRef.injector.get(ApplicationRef);
+ const testabilityRegistry = ngModuleRef.injector.get(TestabilityRegistry);
+ const componentRef = appRef.components[0];
+
+ expect(appRef.components.length).toBe(1);
+ expect(testabilityRegistry.getAllRootElements().length).toBe(1);
+
+ ngModuleRef.destroy(); // also destroys an Injector instance.
+ componentRef.destroy();
+
+ expect(appRef.components.length).toBe(0);
+ expect(testabilityRegistry.getAllRootElements().length).toBe(0);
+ }));
+ });
+
onlyInIvy('options cannot be changed in Ivy').describe('changing bootstrap options', () => {
beforeEach(() => {
spyOn(console, 'error');
diff --git a/packages/core/test/acceptance/component_spec.ts b/packages/core/test/acceptance/component_spec.ts
index 4023e77118..4637595771 100644
--- a/packages/core/test/acceptance/component_spec.ts
+++ b/packages/core/test/acceptance/component_spec.ts
@@ -303,6 +303,57 @@ describe('component', () => {
expect(wrapperEls.length).toBe(2); // other elements are preserved
});
+ it('should invoke `onDestroy` callbacks of dynamically created component', () => {
+ let wasOnDestroyCalled = false;
+ @Component({
+ selector: '[comp]',
+ template: 'comp content',
+ })
+ class DynamicComponent {
+ }
+
+ @NgModule({
+ declarations: [DynamicComponent],
+ entryComponents: [DynamicComponent], // needed only for ViewEngine
+ })
+ class TestModule {
+ }
+
+ @Component({
+ selector: 'button',
+ template: '
',
+ })
+ class App {
+ @ViewChild('anchor', {read: ViewContainerRef}) anchor!: ViewContainerRef;
+
+ constructor(private cfr: ComponentFactoryResolver, private injector: Injector) {}
+
+ create() {
+ const factory = this.cfr.resolveComponentFactory(DynamicComponent);
+ const componentRef = factory.create(this.injector);
+ componentRef.onDestroy(() => {
+ wasOnDestroyCalled = true;
+ });
+ this.anchor.insert(componentRef.hostView);
+ }
+
+ clear() {
+ this.anchor.clear();
+ }
+ }
+
+ TestBed.configureTestingModule({imports: [TestModule], declarations: [App]});
+ const fixture = TestBed.createComponent(App);
+ fixture.detectChanges();
+
+ // Add ComponentRef to ViewContainerRef instance.
+ fixture.componentInstance.create();
+ // Clear ViewContainerRef to invoke `onDestroy` callbacks on ComponentRef.
+ fixture.componentInstance.clear();
+
+ expect(wasOnDestroyCalled).toBeTrue();
+ });
+
describe('invalid host element', () => {
it('should throw when is used as a host element for a Component', () => {
@Component({