mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(core): Add a injectAsync helper function
The commit introduces a new function to assist users who want to lazy load services and use the DI system to create them.
Example:
```ts
import {injectAsync} from 'angular/core';
class MyCmp {
someSvc = injectAsync(() => import('..'));
async onClick() {
(await this.someSvc()).handleClick();
}
}
```
This commit is contained in:
committed by
Alon Mishne
parent
0c56679049
commit
444b024d49
@@ -1,94 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {
|
||||
DestroyRef,
|
||||
ENVIRONMENT_INITIALIZER,
|
||||
EnvironmentInjector,
|
||||
Injector,
|
||||
Provider,
|
||||
ProviderToken,
|
||||
Type,
|
||||
createEnvironmentInjector,
|
||||
inject,
|
||||
Service,
|
||||
} from '@angular/core';
|
||||
|
||||
/**
|
||||
* inject a service asynchronously
|
||||
*
|
||||
* @param: injector. If the injector is a NodeInjector the loaded module will be destroyed alonside its injector
|
||||
*/
|
||||
export async function injectAsync<T>(
|
||||
injector: Injector,
|
||||
providerLoader: () => Promise<ProviderToken<T>>,
|
||||
): Promise<T> {
|
||||
const injectImpl = injector.get(InjectAsyncImpl);
|
||||
return injectImpl.get(injector, providerLoader);
|
||||
}
|
||||
|
||||
@Service()
|
||||
class InjectAsyncImpl<T> {
|
||||
private overrides = new WeakMap(); // no need to cleanup
|
||||
override<T>(type: Type<T>, mock: Type<unknown>) {
|
||||
this.overrides.set(type, mock);
|
||||
}
|
||||
|
||||
async get(injector: Injector, providerLoader: () => Promise<ProviderToken<T>>): Promise<T> {
|
||||
const type = await providerLoader();
|
||||
|
||||
// Check if we have overrides, O(1), low overhead
|
||||
if (this.overrides.has(type)) {
|
||||
const override = this.overrides.get(type);
|
||||
return new override();
|
||||
}
|
||||
|
||||
if (!(injector instanceof EnvironmentInjector)) {
|
||||
// this is the DestroyRef of the component
|
||||
const destroyRef = injector.get(DestroyRef);
|
||||
|
||||
// This is the parent injector of the injector we're creating
|
||||
const environmentInjector = injector.get(EnvironmentInjector);
|
||||
|
||||
// Creating an environment injector to destroy it afterwards
|
||||
const newInjector = createEnvironmentInjector([type as Provider], environmentInjector);
|
||||
|
||||
// Destroy the injector to trigger DestroyRef.onDestroy on our service
|
||||
destroyRef.onDestroy(() => {
|
||||
newInjector.destroy();
|
||||
});
|
||||
|
||||
// We want to create the new instance of our service with our new injector
|
||||
injector = newInjector;
|
||||
}
|
||||
|
||||
return injector.get(type)!;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to mock the lazy loaded module in `injectAsync`
|
||||
*
|
||||
* @usage
|
||||
* TestBed.configureTestingModule({
|
||||
* providers: [
|
||||
* mockAsyncProvider(SandboxService, fakeSandboxService)
|
||||
* ]
|
||||
* });
|
||||
*/
|
||||
export function mockAsyncProvider<T>(type: Type<T>, mock: Type<unknown>) {
|
||||
return [
|
||||
{
|
||||
provide: ENVIRONMENT_INITIALIZER,
|
||||
multi: true,
|
||||
useValue: () => {
|
||||
inject(InjectAsyncImpl).override(type, mock);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -6,12 +6,12 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {EnvironmentInjector} from '@angular/core';
|
||||
|
||||
import {injectAsync} from '../core/services/inject-async';
|
||||
import {EnvironmentInjector, injectAsync, runInInjectionContext} from '@angular/core';
|
||||
|
||||
export function injectEmbeddedTutorialManager(injector: EnvironmentInjector) {
|
||||
return injectAsync(injector, () =>
|
||||
import('./embedded-tutorial-manager.service').then((c) => c.EmbeddedTutorialManager),
|
||||
return runInInjectionContext(injector, () =>
|
||||
injectAsync(() =>
|
||||
import('./embedded-tutorial-manager.service').then((c) => c.EmbeddedTutorialManager),
|
||||
)(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,12 +6,10 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {EnvironmentInjector} from '@angular/core';
|
||||
|
||||
import {injectAsync} from '../core/services/inject-async';
|
||||
import {EnvironmentInjector, injectAsync, runInInjectionContext} from '@angular/core';
|
||||
|
||||
export function injectNodeRuntimeSandbox(injector: EnvironmentInjector) {
|
||||
return injectAsync(injector, () =>
|
||||
import('./node-runtime-sandbox.service').then((c) => c.NodeRuntimeSandbox),
|
||||
return runInInjectionContext(injector, () =>
|
||||
injectAsync(() => import('./node-runtime-sandbox.service').then((c) => c.NodeRuntimeSandbox))(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,9 +12,8 @@ import {WINDOW} from '@angular/docs';
|
||||
import {EmbeddedTutorialManager} from '../../editor';
|
||||
import {NodeRuntimeSandbox} from '../../editor/node-runtime-sandbox.service';
|
||||
|
||||
import TutorialPlayground from './playground.component';
|
||||
import {provideRouter} from '@angular/router';
|
||||
import {mockAsyncProvider} from '../../core/services/inject-async';
|
||||
import TutorialPlayground from './playground.component';
|
||||
|
||||
describe('TutorialPlayground', () => {
|
||||
let component: TutorialPlayground;
|
||||
@@ -44,8 +43,8 @@ describe('TutorialPlayground', () => {
|
||||
provide: WINDOW,
|
||||
useValue: fakeWindow,
|
||||
},
|
||||
mockAsyncProvider(NodeRuntimeSandbox, FakeNodeRuntimeSandbox),
|
||||
mockAsyncProvider(EmbeddedTutorialManager, FakeEmbeddedTutorialManager),
|
||||
{provide: NodeRuntimeSandbox, useClass: FakeNodeRuntimeSandbox},
|
||||
{provide: EmbeddedTutorialManager, useClass: FakeEmbeddedTutorialManager},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
effect,
|
||||
EnvironmentInjector,
|
||||
inject,
|
||||
injectAsync,
|
||||
input,
|
||||
PLATFORM_ID,
|
||||
signal,
|
||||
@@ -24,7 +25,6 @@ import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
import {IconComponent, PlaygroundTemplate} from '@angular/docs';
|
||||
import {forkJoin, switchMap, tap} from 'rxjs';
|
||||
|
||||
import {injectAsync} from '../../core/services/inject-async';
|
||||
import {injectNodeRuntimeSandbox} from '../../editor/index';
|
||||
import type {NodeRuntimeSandbox} from '../../editor/node-runtime-sandbox.service';
|
||||
|
||||
@@ -47,6 +47,10 @@ export default class PlaygroundComponent {
|
||||
private readonly router = inject(Router);
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
|
||||
private editorTutorialManager = injectAsync(() =>
|
||||
import('../../editor/index').then((c) => c.EmbeddedTutorialManager),
|
||||
);
|
||||
|
||||
readonly templates: PlaygroundTemplate[] = PLAYGROUND_ROUTE_DATA_JSON.templates;
|
||||
readonly defaultTemplate = PLAYGROUND_ROUTE_DATA_JSON.defaultTemplate;
|
||||
readonly starterTemplate = PLAYGROUND_ROUTE_DATA_JSON.starterTemplate;
|
||||
@@ -106,10 +110,11 @@ export default class PlaygroundComponent {
|
||||
}
|
||||
|
||||
private async loadTemplate(tutorialPath: string) {
|
||||
const embeddedTutorialManager = await injectAsync(this.environmentInjector, () =>
|
||||
import('../../editor/index').then((c) => c.EmbeddedTutorialManager),
|
||||
);
|
||||
|
||||
await embeddedTutorialManager.fetchAndSetTutorialFiles(tutorialPath);
|
||||
try {
|
||||
const embeddedTutorialManager = await this.editorTutorialManager();
|
||||
await embeddedTutorialManager.fetchAndSetTutorialFiles(tutorialPath);
|
||||
} catch (err) {
|
||||
console.error('Failed to load tutorial files', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {DOCS_VIEWER_SELECTOR, DocViewer, WINDOW, TutorialConfig, TutorialType} from '@angular/docs';
|
||||
import {DOCS_VIEWER_SELECTOR, DocViewer, TutorialConfig, TutorialType, WINDOW} from '@angular/docs';
|
||||
|
||||
import {Component, input, Input, signal} from '@angular/core';
|
||||
import {Component, input, signal} from '@angular/core';
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {provideRouter} from '@angular/router';
|
||||
import {of} from 'rxjs';
|
||||
@@ -16,7 +16,6 @@ import {of} from 'rxjs';
|
||||
import {EMBEDDED_EDITOR_SELECTOR, EmbeddedEditor, EmbeddedTutorialManager} from '../../editor';
|
||||
import {NodeRuntimeSandbox} from '../../editor/node-runtime-sandbox.service';
|
||||
|
||||
import {mockAsyncProvider} from '../../core/services/inject-async';
|
||||
import Tutorial from './tutorial.component';
|
||||
|
||||
@Component({
|
||||
@@ -99,7 +98,7 @@ describe('Tutorial', () => {
|
||||
provide: EmbeddedTutorialManager,
|
||||
useValue: fakeEmbeddedTutorialManager,
|
||||
},
|
||||
mockAsyncProvider(NodeRuntimeSandbox, FakeNodeRuntimeSandbox),
|
||||
{provide: NodeRuntimeSandbox, useClass: FakeNodeRuntimeSandbox},
|
||||
],
|
||||
});
|
||||
TestBed.overrideComponent(Tutorial, {
|
||||
|
||||
@@ -150,15 +150,18 @@ export default class Tutorial {
|
||||
|
||||
this.embeddedTutorialManager.revealAnswer();
|
||||
|
||||
const nodeRuntimeSandbox = await injectNodeRuntimeSandbox(this.environmentInjector);
|
||||
try {
|
||||
const nodeRuntimeSandbox = await injectNodeRuntimeSandbox(this.environmentInjector);
|
||||
await Promise.all(
|
||||
Object.entries(this.embeddedTutorialManager.answerFiles()).map(([path, contents]) =>
|
||||
nodeRuntimeSandbox.writeFile(path, contents as string | Uint8Array),
|
||||
),
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(this.embeddedTutorialManager.answerFiles()).map(([path, contents]) =>
|
||||
nodeRuntimeSandbox.writeFile(path, contents as string | Uint8Array),
|
||||
),
|
||||
);
|
||||
|
||||
this.answerRevealed.set(true);
|
||||
this.answerRevealed.set(true);
|
||||
} catch (err) {
|
||||
console.error('Failed to reveal answer', err);
|
||||
}
|
||||
}
|
||||
|
||||
async handleResetAnswer() {
|
||||
@@ -166,13 +169,16 @@ export default class Tutorial {
|
||||
|
||||
this.embeddedTutorialManager.resetRevealAnswer();
|
||||
|
||||
const nodeRuntimeSandbox = await injectNodeRuntimeSandbox(this.environmentInjector);
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(this.embeddedTutorialManager.tutorialFiles()).map(([path, contents]) =>
|
||||
nodeRuntimeSandbox.writeFile(path, contents as string | Uint8Array),
|
||||
),
|
||||
);
|
||||
try {
|
||||
const nodeRuntimeSandbox = await injectNodeRuntimeSandbox(this.environmentInjector);
|
||||
await Promise.all(
|
||||
Object.entries(this.embeddedTutorialManager.tutorialFiles()).map(([path, contents]) =>
|
||||
nodeRuntimeSandbox.writeFile(path, contents as string | Uint8Array),
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('Failed to reset answer', err);
|
||||
}
|
||||
|
||||
this.answerRevealed.set(false);
|
||||
}
|
||||
@@ -195,9 +201,13 @@ export default class Tutorial {
|
||||
(routeData.type === TutorialType.EDITOR || routeData.type === TutorialType.CLI) &&
|
||||
this.isBrowser
|
||||
) {
|
||||
await this.setEditorTutorialData(
|
||||
tutorialNavigationItem.path.replace(`${PAGE_PREFIX.TUTORIALS}/`, ''),
|
||||
);
|
||||
try {
|
||||
await this.setEditorTutorialData(
|
||||
tutorialNavigationItem.path.replace(`${PAGE_PREFIX.TUTORIALS}/`, ''),
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('Failed to load embedded editor tutorial data', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user