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
feat(core): introduce EnvironmentInjector.runInContext API
This commit introduces a new API on `EnvironmentInjector` to run a function
in the context of the injector. This makes `inject()` available within the
body of the function to inject dependencies. We expect this functionality to
be very useful for designing ergonomic APIs both in and out of Angular, as
it should allow for a smaller and more functional API style.

Note that it's possible to implement nearly identical functionality in user
code already today:

```typescript
function runInContext<T>(injector: EnvironmentInjector, fn: () => T): T {
  const token = new InjectionToken<T>('TOKEN');
  const tmpInjector = createEnvironmentInjector([
    {provide: token, useFactory: () => fn()},
  ], injector);

  return tmpInjector.get(token);
}
```

The factory provider for `token` in this example runs in the context of the
`tmpInjector`, giving it access to `inject` that can retrieve values from
`injector` as well. This is nearly identical, although because of the child
injector `self` and `skipSelf` injection options don't function correctly.
  • Loading branch information
alxhub committed Jun 30, 2022
commit d8c336ad982fe910b0e3ff78fa3b9bfff7703240
1 change: 1 addition & 0 deletions goldens/public-api/core/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,7 @@ export abstract class EnvironmentInjector implements Injector {
abstract get<T>(token: ProviderToken<T>, notFoundValue?: T, flags?: InjectFlags): T;
// @deprecated (undocumented)
abstract get(token: any, notFoundValue?: any): any;
abstract runInContext<ReturnT>(fn: () => ReturnT): ReturnT;
}

// @public
Expand Down
25 changes: 25 additions & 0 deletions packages/core/src/di/r3_injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,18 @@ export abstract class EnvironmentInjector implements Injector {
*/
abstract get(token: any, notFoundValue?: any): any;

/**
* Runs the given function in the context of this `EnvironmentInjector`.
*
* Within the function's stack frame, `inject` can be used to inject dependencies from this
* injector. Note that `inject` is only usable synchronously, and cannot be used in any
* asynchronous callbacks or after any `await` points.
*
* @param fn the closure to be run in the context of this injector
* @returns the return value of the function, if any
*/
abstract runInContext<ReturnT>(fn: () => ReturnT): ReturnT;

@alfaproject alfaproject Jul 1, 2022 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not that it matters but only T or T as a prefix like TReturn/TRet/TRes is more canonical


abstract destroy(): void;

/**
Expand Down Expand Up @@ -180,6 +192,19 @@ export class R3Injector extends EnvironmentInjector {
this._onDestroyHooks.push(callback);
}

override runInContext<ReturnT>(fn: () => ReturnT): ReturnT {
this.assertNotDestroyed();

const previousInjector = setCurrentInjector(this);
const previousInjectImplementation = setInjectImplementation(undefined);
try {
return fn();
} finally {
setCurrentInjector(previousInjector);
setInjectImplementation(previousInjectImplementation);
}
}

override get<T>(
token: ProviderToken<T>, notFoundValue: any = THROW_IF_NOT_FOUND,
flags = InjectFlags.Default): T {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The get method can take advantage of the runInContext function now (to reuse the code) :)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hahaha :) I don't think we want the additional stack frame, but that'd be clever.

Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/render3/ng_module_ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ export class NgModuleRef<T> extends viewEngine_NgModuleRef<T> implements Interna
return this._r3Injector.get(token, notFoundValue, injectFlags);
}

runInContext<ReturnT>(fn: () => ReturnT): ReturnT {
return this.injector.runInContext(fn);
}

override destroy(): void {
ngDevMode && assertDefined(this.destroyCbs, 'NgModule already destroyed');
const injector = this._r3Injector;
Expand Down
92 changes: 91 additions & 1 deletion packages/core/test/acceptance/environment_injector_spec.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 {Component, ComponentFactoryResolver, createEnvironmentInjector, ENVIRONMENT_INITIALIZER, EnvironmentInjector, InjectionToken, INJECTOR, Injector, NgModuleRef} from '@angular/core';
import {Component, ComponentFactoryResolver, createEnvironmentInjector, ENVIRONMENT_INITIALIZER, EnvironmentInjector, inject, InjectFlags, InjectionToken, INJECTOR, Injector, NgModuleRef, ViewContainerRef} from '@angular/core';
import {R3Injector} from '@angular/core/src/di/r3_injector';
import {TestBed} from '@angular/core/testing';

Expand Down Expand Up @@ -119,4 +119,94 @@ describe('environment injector', () => {
});
expect(injector.get(EnvScopedToken, false)).toBeTrue();
});

describe('runInContext()', () => {
Comment thread
AndrewKushnir marked this conversation as resolved.
Outdated
it('should return the function\'s return value', () => {
const injector = TestBed.inject(EnvironmentInjector);
const returnValue = injector.runInContext(() => 3);
expect(returnValue).toBe(3);
});

it('should make inject() available', () => {
const TOKEN = new InjectionToken<string>('TOKEN');
const injector = createEnvironmentInjector(
[{provide: TOKEN, useValue: 'from injector'}], TestBed.inject(EnvironmentInjector));

const result = injector.runInContext(() => inject(TOKEN));
expect(result).toEqual('from injector');
});

it('should properly clean up after the function returns', () => {
const TOKEN = new InjectionToken<string>('TOKEN');
const injector = TestBed.inject(EnvironmentInjector);
injector.runInContext(() => {});
expect(() => inject(TOKEN, InjectFlags.Optional)).toThrow();
});

it('should properly clean up after the function throws', () => {
const TOKEN = new InjectionToken<string>('TOKEN');
const injector = TestBed.inject(EnvironmentInjector);
expect(() => injector.runInContext(() => {
throw new Error('crashes!');
})).toThrow();
expect(() => inject(TOKEN, InjectFlags.Optional)).toThrow();
});

it('should set the correct inject implementation', () => {
const TOKEN = new InjectionToken<string>('TOKEN', {
providedIn: 'root',
factory: () => 'from root',
});

@Component({
standalone: true,
template: '',
providers: [{provide: TOKEN, useValue: 'from component'}],
})
class TestCmp {
envInjector = inject(EnvironmentInjector);

tokenFromComponent = inject(TOKEN);
tokenFromEnvContext = this.envInjector.runInContext(() => inject(TOKEN));

// Attempt to inject ViewContainerRef within the environment injector's context. This should
// not be available, so the result should be `null`.
vcrFromEnvContext =
this.envInjector.runInContext(() => inject(ViewContainerRef, InjectFlags.Optional));
}

const instance = TestBed.createComponent(TestCmp).componentInstance;
expect(instance.tokenFromComponent).toEqual('from component');
expect(instance.tokenFromEnvContext).toEqual('from root');
expect(instance.vcrFromEnvContext).toBeNull();
});

it('should be reentrant', () => {
const TOKEN = new InjectionToken<string>('TOKEN', {
providedIn: 'root',
factory: () => 'from root',
});

const parentInjector = TestBed.inject(EnvironmentInjector);
const childInjector =
createEnvironmentInjector([{provide: TOKEN, useValue: 'from child'}], parentInjector);

const results = parentInjector.runInContext(() => {
const fromParentBefore = inject(TOKEN);
const fromChild = childInjector.runInContext(() => inject(TOKEN));
const fromParentAfter = inject(TOKEN);
return {fromParentBefore, fromChild, fromParentAfter};
});

expect(results.fromParentBefore).toEqual('from root');
expect(results.fromChild).toEqual('from child');
expect(results.fromParentAfter).toEqual('from root');
});

it('should not function on a destroyed injector', () => {
const injector = createEnvironmentInjector([], TestBed.inject(EnvironmentInjector));
injector.destroy();
expect(() => injector.runInContext(() => {})).toThrow();
});
});
});