Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions goldens/public-api/core/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export class ApplicationRef {
destroy(): void;
get destroyed(): boolean;
detachView(viewRef: ViewRef): void;
get injector(): EnvironmentInjector;
readonly isStable: Observable<boolean>;
tick(): void;
get viewCount(): number;
Expand Down
8 changes: 5 additions & 3 deletions packages/core/src/application_ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -740,15 +740,17 @@ export class ApplicationRef {
// TODO(issue/24571): remove '!'.
public readonly isStable!: Observable<boolean>;

/** @internal */
get injector(): Injector {
/**
* The `EnvironmentInjector` used to create this application.
*/
get injector(): EnvironmentInjector {
return this._injector;
}

/** @internal */
constructor(
private _zone: NgZone,
private _injector: Injector,
private _injector: EnvironmentInjector,
private _exceptionHandler: ErrorHandler,
) {
this._onMicrotaskEmptySubscription = this._zone.onMicrotaskEmpty.subscribe({
Expand Down
155 changes: 84 additions & 71 deletions packages/core/test/application_ref_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,16 @@

import {DOCUMENT, ɵgetDOM as getDOM} from '@angular/common';
import {ResourceLoader} from '@angular/compiler';
import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, Compiler, CompilerFactory, Component, InjectionToken, Injector, LOCALE_ID, NgModule, NgZone, PlatformRef, RendererFactory2, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core';
import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, Compiler, CompilerFactory, Component, EnvironmentInjector, InjectionToken, LOCALE_ID, NgModule, NgZone, PlatformRef, RendererFactory2, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core';
import {ErrorHandler} from '@angular/core/src/error_handler';
import {ComponentRef} from '@angular/core/src/linker/component_factory';
import {getLocaleId} from '@angular/core/src/render3';
import {createEnvironmentInjector, getLocaleId} from '@angular/core/src/render3';
import {BrowserModule} from '@angular/platform-browser';
import {DomRendererFactory2} from '@angular/platform-browser/src/dom/dom_renderer';
import {createTemplate, dispatchEvent, getContent} from '@angular/platform-browser/testing/src/browser_util';
import {expect} from '@angular/platform-browser/testing/src/matchers';

import {ApplicationRef} from '../src/application_ref';
import {createInjector} from '../src/di/create_injector';
import {NoopNgZone} from '../src/zone/ng_zone';
import {ComponentFixtureNoNgZone, inject, TestBed, waitForAsync, withModule} from '../testing';

Expand Down Expand Up @@ -224,23 +223,23 @@ class SomeComponent {
// that the instance of the `ApplicationRef` class is created on that injector (vs in the
// app-level injector). It is needed to verify `ApplicationRef.destroy` scenarios, which
// includes destroying an underlying injector.
function createApplicationRefInjector(parentInjector: Injector) {
@NgModule()
class RootModule {
}
function createApplicationRefInjector(parentInjector: EnvironmentInjector) {
const extraProviders = [{provide: ApplicationRef, useClass: ApplicationRef}];
return createInjector(RootModule, parentInjector, extraProviders);

return createEnvironmentInjector(extraProviders, parentInjector);
}

function createApplicationRef(parentInjector: Injector) {
function createApplicationRef(parentInjector: EnvironmentInjector) {
const injector = createApplicationRefInjector(parentInjector);
return injector.get(ApplicationRef);
}

it('should cleanup the DOM',
withModule(
{providers},
waitForAsync(
inject([Injector, DOCUMENT], (parentInjector: Injector, doc: Document) => {
waitForAsync(inject(
[EnvironmentInjector, DOCUMENT],
(parentInjector: EnvironmentInjector, doc: Document) => {
createRootEl();

const appRef = createApplicationRef(parentInjector);
Expand All @@ -258,7 +257,8 @@ class SomeComponent {

it('should throw when trying to call `destroy` method on already destroyed ApplicationRef',
withModule(
{providers}, waitForAsync(inject([Injector], (parentInjector: Injector) => {
{providers},
waitForAsync(inject([EnvironmentInjector], (parentInjector: EnvironmentInjector) => {
createRootEl();
const appRef = createApplicationRef(parentInjector);
appRef.bootstrap(SomeComponent);
Expand All @@ -270,85 +270,84 @@ class SomeComponent {
}))));

it('should invoke all registered `onDestroy` callbacks (internal API)',
withModule({providers}, waitForAsync(inject([Injector], (parentInjector: Injector) => {
const onDestroyA = jasmine.createSpy('onDestroyA');
const onDestroyB = jasmine.createSpy('onDestroyB');
createRootEl();

const appRef =
createApplicationRef(parentInjector) as unknown as ApplicationRef &
{onDestroy: Function};
appRef.bootstrap(SomeComponent);
appRef.onDestroy(onDestroyA);
appRef.onDestroy(onDestroyB);
appRef.destroy();

expect(onDestroyA).toHaveBeenCalledTimes(1);
expect(onDestroyB).toHaveBeenCalledTimes(1);
}))));
withModule(
{providers},
waitForAsync(inject([EnvironmentInjector], (parentInjector: EnvironmentInjector) => {
const onDestroyA = jasmine.createSpy('onDestroyA');
const onDestroyB = jasmine.createSpy('onDestroyB');
createRootEl();

const appRef = createApplicationRef(parentInjector) as unknown as ApplicationRef &
{onDestroy: Function};
appRef.bootstrap(SomeComponent);
appRef.onDestroy(onDestroyA);
appRef.onDestroy(onDestroyB);
appRef.destroy();

expect(onDestroyA).toHaveBeenCalledTimes(1);
expect(onDestroyB).toHaveBeenCalledTimes(1);
}))));

it('should allow to unsubscribe a registered `onDestroy` callback (internal API)',
withModule({providers}, waitForAsync(inject([Injector], (parentInjector: Injector) => {
createRootEl();
withModule(
{providers},
waitForAsync(inject([EnvironmentInjector], (parentInjector: EnvironmentInjector) => {
createRootEl();

const appRef =
createApplicationRef(parentInjector) as unknown as ApplicationRef &
{onDestroy: Function};
appRef.bootstrap(SomeComponent);
const appRef = createApplicationRef(parentInjector) as unknown as ApplicationRef &
{onDestroy: Function};
appRef.bootstrap(SomeComponent);

const onDestroyA = jasmine.createSpy('onDestroyA');
const onDestroyB = jasmine.createSpy('onDestroyB');
const unsubscribeOnDestroyA = appRef.onDestroy(onDestroyA);
const unsubscribeOnDestroyB = appRef.onDestroy(onDestroyB);
const onDestroyA = jasmine.createSpy('onDestroyA');
const onDestroyB = jasmine.createSpy('onDestroyB');
const unsubscribeOnDestroyA = appRef.onDestroy(onDestroyA);
const unsubscribeOnDestroyB = appRef.onDestroy(onDestroyB);

// Unsubscribe registered listeners.
unsubscribeOnDestroyA();
unsubscribeOnDestroyB();
// Unsubscribe registered listeners.
unsubscribeOnDestroyA();
unsubscribeOnDestroyB();

appRef.destroy();
appRef.destroy();

expect(onDestroyA).not.toHaveBeenCalled();
expect(onDestroyB).not.toHaveBeenCalled();
}))));
expect(onDestroyA).not.toHaveBeenCalled();
expect(onDestroyB).not.toHaveBeenCalled();
}))));

it('should correctly update the `destroyed` flag',
withModule({providers}, waitForAsync(inject([Injector], (parentInjector: Injector) => {
createRootEl();
withModule(
{providers},
waitForAsync(inject([EnvironmentInjector], (parentInjector: EnvironmentInjector) => {
createRootEl();

const appRef = createApplicationRef(parentInjector);
appRef.bootstrap(SomeComponent);
const appRef = createApplicationRef(parentInjector);
appRef.bootstrap(SomeComponent);

expect(appRef.destroyed).toBeFalse();
expect(appRef.destroyed).toBeFalse();

appRef.destroy();
appRef.destroy();

expect(appRef.destroyed).toBeTrue();
}))));
expect(appRef.destroyed).toBeTrue();
}))));

it('should also destroy underlying injector',
withModule({providers}, waitForAsync(inject([Injector], (parentInjector: Injector) => {
// This is a temporary type to represent an instance of an R3Injector, which
// can be destroyed.
// The type will be replaced with a different one once destroyable injector
// type is available.
type DestroyableInjector = Injector&{destroy?: Function, destroyed?: boolean};

createRootEl();
withModule(
{providers},
waitForAsync(inject([EnvironmentInjector], (parentInjector: EnvironmentInjector) => {
createRootEl();

const injector =
createApplicationRefInjector(parentInjector) as DestroyableInjector;
const injector = createApplicationRefInjector(parentInjector);

const appRef = injector.get(ApplicationRef);
appRef.bootstrap(SomeComponent);
const appRef = injector.get(ApplicationRef);
appRef.bootstrap(SomeComponent);

expect(appRef.destroyed).toBeFalse();
expect(injector.destroyed).toBeFalse();
expect(appRef.destroyed).toBeFalse();
expect((injector as any).destroyed).toBeFalse();

appRef.destroy();
appRef.destroy();

expect(appRef.destroyed).toBeTrue();
expect(injector.destroyed).toBeTrue();
}))));
expect(appRef.destroyed).toBeTrue();
expect((injector as any).destroyed).toBeTrue();
}))));
});

describe('bootstrapModule', () => {
Expand Down Expand Up @@ -825,6 +824,20 @@ class SomeComponent {
}));
});
});

describe('injector', () => {
it('should expose an EnvironmentInjector', () => {
@Component({})
class TestCmp {
constructor(readonly envInjector: EnvironmentInjector) {}
}

const fixture = TestBed.createComponent(TestCmp);
const appRef = TestBed.inject(ApplicationRef);

expect(appRef.injector).toBe(fixture.componentInstance.envInjector);
});
});
}

class MockConsole {
Expand Down
4 changes: 2 additions & 2 deletions packages/platform-server/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {ApplicationRef, ImportedNgModuleProviders, importProvidersFrom, Injector, NgModuleFactory, NgModuleRef, PlatformRef, Provider, StaticProvider, Type, ɵinternalBootstrapApplication as internalBootstrapApplication, ɵisPromise} from '@angular/core';
import {ApplicationRef, ImportedNgModuleProviders, importProvidersFrom, NgModuleFactory, NgModuleRef, PlatformRef, Provider, StaticProvider, Type, ɵinternalBootstrapApplication as internalBootstrapApplication, ɵisPromise} from '@angular/core';
import {BrowserModule, ɵTRANSITION_ID} from '@angular/platform-browser';
import {first} from 'rxjs/operators';

Expand Down Expand Up @@ -34,7 +34,7 @@ function _render<T>(
platform: PlatformRef,
bootstrapPromise: Promise<NgModuleRef<T>|ApplicationRef>): Promise<string> {
return bootstrapPromise.then((moduleOrApplicationRef) => {
const environmentInjector = (moduleOrApplicationRef as {injector: Injector}).injector;
const environmentInjector = moduleOrApplicationRef.injector;
const transitionId = environmentInjector.get(ɵTRANSITION_ID, null);
if (!transitionId) {
throw new Error(
Expand Down