Skip to content

Commit 71e606d

Browse files
pkozlowski-opensourcealxhub
authored andcommitted
feat(core): expose EnvironmentInjector on ApplicationRef (#46665)
There are legitimate cases where access to an EnvironmentInjector of a bootstrapped application is required / convenient. It will be also required for support of standalone components with Angular elements. PR Close #46665
1 parent 33ce388 commit 71e606d

4 files changed

Lines changed: 92 additions & 76 deletions

File tree

goldens/public-api/core/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export class ApplicationRef {
8484
destroy(): void;
8585
get destroyed(): boolean;
8686
detachView(viewRef: ViewRef): void;
87+
get injector(): EnvironmentInjector;
8788
readonly isStable: Observable<boolean>;
8889
tick(): void;
8990
get viewCount(): number;

packages/core/src/application_ref.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -742,15 +742,17 @@ export class ApplicationRef {
742742
// TODO(issue/24571): remove '!'.
743743
public readonly isStable!: Observable<boolean>;
744744

745-
/** @internal */
746-
get injector(): Injector {
745+
/**
746+
* The `EnvironmentInjector` used to create this application.
747+
*/
748+
get injector(): EnvironmentInjector {
747749
return this._injector;
748750
}
749751

750752
/** @internal */
751753
constructor(
752754
private _zone: NgZone,
753-
private _injector: Injector,
755+
private _injector: EnvironmentInjector,
754756
private _exceptionHandler: ErrorHandler,
755757
) {
756758
this._onMicrotaskEmptySubscription = this._zone.onMicrotaskEmpty.subscribe({

packages/core/test/application_ref_spec.ts

Lines changed: 84 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,16 @@
88

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

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

@@ -224,23 +223,23 @@ class SomeComponent {
224223
// that the instance of the `ApplicationRef` class is created on that injector (vs in the
225224
// app-level injector). It is needed to verify `ApplicationRef.destroy` scenarios, which
226225
// includes destroying an underlying injector.
227-
function createApplicationRefInjector(parentInjector: Injector) {
228-
@NgModule()
229-
class RootModule {
230-
}
226+
function createApplicationRefInjector(parentInjector: EnvironmentInjector) {
231227
const extraProviders = [{provide: ApplicationRef, useClass: ApplicationRef}];
232-
return createInjector(RootModule, parentInjector, extraProviders);
228+
229+
return createEnvironmentInjector(extraProviders, parentInjector);
233230
}
234231

235-
function createApplicationRef(parentInjector: Injector) {
232+
function createApplicationRef(parentInjector: EnvironmentInjector) {
236233
const injector = createApplicationRefInjector(parentInjector);
237234
return injector.get(ApplicationRef);
238235
}
236+
239237
it('should cleanup the DOM',
240238
withModule(
241239
{providers},
242-
waitForAsync(
243-
inject([Injector, DOCUMENT], (parentInjector: Injector, doc: Document) => {
240+
waitForAsync(inject(
241+
[EnvironmentInjector, DOCUMENT],
242+
(parentInjector: EnvironmentInjector, doc: Document) => {
244243
createRootEl();
245244

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

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

272272
it('should invoke all registered `onDestroy` callbacks (internal API)',
273-
withModule({providers}, waitForAsync(inject([Injector], (parentInjector: Injector) => {
274-
const onDestroyA = jasmine.createSpy('onDestroyA');
275-
const onDestroyB = jasmine.createSpy('onDestroyB');
276-
createRootEl();
277-
278-
const appRef =
279-
createApplicationRef(parentInjector) as unknown as ApplicationRef &
280-
{onDestroy: Function};
281-
appRef.bootstrap(SomeComponent);
282-
appRef.onDestroy(onDestroyA);
283-
appRef.onDestroy(onDestroyB);
284-
appRef.destroy();
285-
286-
expect(onDestroyA).toHaveBeenCalledTimes(1);
287-
expect(onDestroyB).toHaveBeenCalledTimes(1);
288-
}))));
273+
withModule(
274+
{providers},
275+
waitForAsync(inject([EnvironmentInjector], (parentInjector: EnvironmentInjector) => {
276+
const onDestroyA = jasmine.createSpy('onDestroyA');
277+
const onDestroyB = jasmine.createSpy('onDestroyB');
278+
createRootEl();
279+
280+
const appRef = createApplicationRef(parentInjector) as unknown as ApplicationRef &
281+
{onDestroy: Function};
282+
appRef.bootstrap(SomeComponent);
283+
appRef.onDestroy(onDestroyA);
284+
appRef.onDestroy(onDestroyB);
285+
appRef.destroy();
286+
287+
expect(onDestroyA).toHaveBeenCalledTimes(1);
288+
expect(onDestroyB).toHaveBeenCalledTimes(1);
289+
}))));
289290

290291
it('should allow to unsubscribe a registered `onDestroy` callback (internal API)',
291-
withModule({providers}, waitForAsync(inject([Injector], (parentInjector: Injector) => {
292-
createRootEl();
292+
withModule(
293+
{providers},
294+
waitForAsync(inject([EnvironmentInjector], (parentInjector: EnvironmentInjector) => {
295+
createRootEl();
293296

294-
const appRef =
295-
createApplicationRef(parentInjector) as unknown as ApplicationRef &
296-
{onDestroy: Function};
297-
appRef.bootstrap(SomeComponent);
297+
const appRef = createApplicationRef(parentInjector) as unknown as ApplicationRef &
298+
{onDestroy: Function};
299+
appRef.bootstrap(SomeComponent);
298300

299-
const onDestroyA = jasmine.createSpy('onDestroyA');
300-
const onDestroyB = jasmine.createSpy('onDestroyB');
301-
const unsubscribeOnDestroyA = appRef.onDestroy(onDestroyA);
302-
const unsubscribeOnDestroyB = appRef.onDestroy(onDestroyB);
301+
const onDestroyA = jasmine.createSpy('onDestroyA');
302+
const onDestroyB = jasmine.createSpy('onDestroyB');
303+
const unsubscribeOnDestroyA = appRef.onDestroy(onDestroyA);
304+
const unsubscribeOnDestroyB = appRef.onDestroy(onDestroyB);
303305

304-
// Unsubscribe registered listeners.
305-
unsubscribeOnDestroyA();
306-
unsubscribeOnDestroyB();
306+
// Unsubscribe registered listeners.
307+
unsubscribeOnDestroyA();
308+
unsubscribeOnDestroyB();
307309

308-
appRef.destroy();
310+
appRef.destroy();
309311

310-
expect(onDestroyA).not.toHaveBeenCalled();
311-
expect(onDestroyB).not.toHaveBeenCalled();
312-
}))));
312+
expect(onDestroyA).not.toHaveBeenCalled();
313+
expect(onDestroyB).not.toHaveBeenCalled();
314+
}))));
313315

314316
it('should correctly update the `destroyed` flag',
315-
withModule({providers}, waitForAsync(inject([Injector], (parentInjector: Injector) => {
316-
createRootEl();
317+
withModule(
318+
{providers},
319+
waitForAsync(inject([EnvironmentInjector], (parentInjector: EnvironmentInjector) => {
320+
createRootEl();
317321

318-
const appRef = createApplicationRef(parentInjector);
319-
appRef.bootstrap(SomeComponent);
322+
const appRef = createApplicationRef(parentInjector);
323+
appRef.bootstrap(SomeComponent);
320324

321-
expect(appRef.destroyed).toBeFalse();
325+
expect(appRef.destroyed).toBeFalse();
322326

323-
appRef.destroy();
327+
appRef.destroy();
324328

325-
expect(appRef.destroyed).toBeTrue();
326-
}))));
329+
expect(appRef.destroyed).toBeTrue();
330+
}))));
327331

328332
it('should also destroy underlying injector',
329-
withModule({providers}, waitForAsync(inject([Injector], (parentInjector: Injector) => {
330-
// This is a temporary type to represent an instance of an R3Injector, which
331-
// can be destroyed.
332-
// The type will be replaced with a different one once destroyable injector
333-
// type is available.
334-
type DestroyableInjector = Injector&{destroy?: Function, destroyed?: boolean};
335-
336-
createRootEl();
333+
withModule(
334+
{providers},
335+
waitForAsync(inject([EnvironmentInjector], (parentInjector: EnvironmentInjector) => {
336+
createRootEl();
337337

338-
const injector =
339-
createApplicationRefInjector(parentInjector) as DestroyableInjector;
338+
const injector = createApplicationRefInjector(parentInjector);
340339

341-
const appRef = injector.get(ApplicationRef);
342-
appRef.bootstrap(SomeComponent);
340+
const appRef = injector.get(ApplicationRef);
341+
appRef.bootstrap(SomeComponent);
343342

344-
expect(appRef.destroyed).toBeFalse();
345-
expect(injector.destroyed).toBeFalse();
343+
expect(appRef.destroyed).toBeFalse();
344+
expect((injector as any).destroyed).toBeFalse();
346345

347-
appRef.destroy();
346+
appRef.destroy();
348347

349-
expect(appRef.destroyed).toBeTrue();
350-
expect(injector.destroyed).toBeTrue();
351-
}))));
348+
expect(appRef.destroyed).toBeTrue();
349+
expect((injector as any).destroyed).toBeTrue();
350+
}))));
352351
});
353352

354353
describe('bootstrapModule', () => {
@@ -825,6 +824,20 @@ class SomeComponent {
825824
}));
826825
});
827826
});
827+
828+
describe('injector', () => {
829+
it('should expose an EnvironmentInjector', () => {
830+
@Component({})
831+
class TestCmp {
832+
constructor(readonly envInjector: EnvironmentInjector) {}
833+
}
834+
835+
const fixture = TestBed.createComponent(TestCmp);
836+
const appRef = TestBed.inject(ApplicationRef);
837+
838+
expect(appRef.injector).toBe(fixture.componentInstance.envInjector);
839+
});
840+
});
828841
}
829842

830843
class MockConsole {

packages/platform-server/src/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

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

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

0 commit comments

Comments
 (0)