Skip to content

Commit 4b377d3

Browse files
feat(core): introduce createApplication API (#46475)
The `createApplication` function makes it possible to create an application instance (represented by the `ApplicationRef`) without bootstrapping any components. It is useful in the situations where ones wants to decouple and delay components rendering and / or render multiple root components in one application. Angular elements can use this API to create custom element types with an environment linked to a created application. PR Close #46475
1 parent 6fed377 commit 4b377d3

File tree

8 files changed

+76
-38
lines changed

8 files changed

+76
-38
lines changed

goldens/public-api/platform-browser/index.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ export class By {
6262
static directive(type: Type<any>): Predicate<DebugNode>;
6363
}
6464

65+
// @public
66+
export function createApplication(options?: ApplicationConfig): Promise<ApplicationRef>;
67+
6568
// @public
6669
export function disableDebugTools(): void;
6770

packages/core/src/application_ref.ts

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,8 @@ export function runPlatformInitializers(injector: Injector): void {
177177
}
178178

179179
/**
180-
* Internal bootstrap application API that implements the core bootstrap logic.
180+
* Internal create application API that implements the core application creation logic and optional
181+
* bootstrap logic.
181182
*
182183
* Platforms (such as `platform-browser`) may require different set of application and platform
183184
* providers for an application to function correctly. As a result, platforms may use this function
@@ -186,17 +187,20 @@ export function runPlatformInitializers(injector: Injector): void {
186187
*
187188
* @returns A promise that returns an `ApplicationRef` instance once resolved.
188189
*/
189-
export function internalBootstrapApplication(config: {
190-
rootComponent: Type<unknown>,
190+
export function internalCreateApplication(config: {
191+
rootComponent?: Type<unknown>,
191192
appProviders?: Array<Provider|ImportedNgModuleProviders>,
192193
platformProviders?: Provider[],
193194
}): Promise<ApplicationRef> {
194195
const {rootComponent, appProviders, platformProviders} = config;
195-
NG_DEV_MODE && assertStandaloneComponentType(rootComponent);
196+
197+
if (NG_DEV_MODE && rootComponent !== undefined) {
198+
assertStandaloneComponentType(rootComponent);
199+
}
196200

197201
const platformInjector = createOrReusePlatformInjector(platformProviders as StaticProvider[]);
198202

199-
const ngZone = new NgZone(getNgZoneOptions());
203+
const ngZone = getNgZone('zone.js', getNgZoneOptions());
200204

201205
return ngZone.run(() => {
202206
// Create root application injector based on a set of providers configured at the platform
@@ -205,10 +209,11 @@ export function internalBootstrapApplication(config: {
205209
{provide: NgZone, useValue: ngZone}, //
206210
...(appProviders || []), //
207211
];
208-
const appInjector = createEnvironmentInjector(
212+
213+
const envInjector = createEnvironmentInjector(
209214
allAppProviders, platformInjector as EnvironmentInjector, 'Environment Injector');
210215

211-
const exceptionHandler: ErrorHandler|null = appInjector.get(ErrorHandler, null);
216+
const exceptionHandler: ErrorHandler|null = envInjector.get(ErrorHandler, null);
212217
if (NG_DEV_MODE && !exceptionHandler) {
213218
throw new RuntimeError(
214219
RuntimeErrorCode.ERROR_HANDLER_NOT_FOUND,
@@ -223,27 +228,30 @@ export function internalBootstrapApplication(config: {
223228
}
224229
});
225230
});
231+
232+
// If the whole platform is destroyed, invoke the `destroy` method
233+
// for all bootstrapped applications as well.
234+
const destroyListener = () => envInjector.destroy();
235+
const onPlatformDestroyListeners = platformInjector.get(PLATFORM_DESTROY_LISTENERS);
236+
onPlatformDestroyListeners.add(destroyListener);
237+
238+
envInjector.onDestroy(() => {
239+
onErrorSubscription.unsubscribe();
240+
onPlatformDestroyListeners.delete(destroyListener);
241+
});
242+
226243
return _callAndReportToErrorHandler(exceptionHandler!, ngZone, () => {
227-
const initStatus = appInjector.get(ApplicationInitStatus);
244+
const initStatus = envInjector.get(ApplicationInitStatus);
228245
initStatus.runInitializers();
246+
229247
return initStatus.donePromise.then(() => {
230-
const localeId = appInjector.get(LOCALE_ID, DEFAULT_LOCALE_ID);
248+
const localeId = envInjector.get(LOCALE_ID, DEFAULT_LOCALE_ID);
231249
setLocaleId(localeId || DEFAULT_LOCALE_ID);
232250

233-
const appRef = appInjector.get(ApplicationRef);
234-
235-
// If the whole platform is destroyed, invoke the `destroy` method
236-
// for all bootstrapped applications as well.
237-
const destroyListener = () => appRef.destroy();
238-
const onPlatformDestroyListeners = platformInjector.get(PLATFORM_DESTROY_LISTENERS, null);
239-
onPlatformDestroyListeners?.add(destroyListener);
240-
241-
appRef.onDestroy(() => {
242-
onPlatformDestroyListeners?.delete(destroyListener);
243-
onErrorSubscription.unsubscribe();
244-
});
245-
246-
appRef.bootstrap(rootComponent);
251+
const appRef = envInjector.get(ApplicationRef);
252+
if (rootComponent !== undefined) {
253+
appRef.bootstrap(rootComponent);
254+
}
247255
return appRef;
248256
});
249257
});
@@ -493,7 +501,7 @@ export class PlatformRef {
493501
}
494502

495503
private _moduleDoBootstrap(moduleRef: InternalNgModuleRef<any>): void {
496-
const appRef = moduleRef.injector.get(ApplicationRef) as ApplicationRef;
504+
const appRef = moduleRef.injector.get(ApplicationRef);
497505
if (moduleRef._bootstrapComponents.length > 0) {
498506
moduleRef._bootstrapComponents.forEach(f => appRef.bootstrap(f));
499507
} else if (moduleRef.instance.ngDoBootstrap) {

packages/core/src/core_private_export.ts

Lines changed: 1 addition & 1 deletion
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-
export {ALLOW_MULTIPLE_PLATFORMS as ɵALLOW_MULTIPLE_PLATFORMS, internalBootstrapApplication as ɵinternalBootstrapApplication} from './application_ref';
9+
export {ALLOW_MULTIPLE_PLATFORMS as ɵALLOW_MULTIPLE_PLATFORMS, internalCreateApplication as ɵinternalCreateApplication} from './application_ref';
1010
export {APP_ID_RANDOM_PROVIDER as ɵAPP_ID_RANDOM_PROVIDER} from './application_tokens';
1111
export {defaultIterableDiffers as ɵdefaultIterableDiffers, defaultKeyValueDiffers as ɵdefaultKeyValueDiffers} from './change_detection/change_detection';
1212
export {ChangeDetectorStatus as ɵChangeDetectorStatus, isDefaultChangeDetectionStrategy as ɵisDefaultChangeDetectionStrategy} from './change_detection/constants';

packages/core/test/application_ref_spec.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -333,20 +333,26 @@ class SomeComponent {
333333
withModule(
334334
{providers},
335335
waitForAsync(inject([EnvironmentInjector], (parentInjector: EnvironmentInjector) => {
336+
// This is a temporary type to represent an instance of an R3Injector, which
337+
// can be destroyed.
338+
// The type will be replaced with a different one once destroyable injector
339+
// type is available.
340+
type DestroyableInjector = EnvironmentInjector&{destroyed?: boolean};
341+
336342
createRootEl();
337343

338-
const injector = createApplicationRefInjector(parentInjector);
344+
const injector = createApplicationRefInjector(parentInjector) as DestroyableInjector;
339345

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

343349
expect(appRef.destroyed).toBeFalse();
344-
expect((injector as any).destroyed).toBeFalse();
350+
expect(injector.destroyed).toBeFalse();
345351

346352
appRef.destroy();
347353

348354
expect(appRef.destroyed).toBeTrue();
349-
expect((injector as any).destroyed).toBeTrue();
355+
expect(injector.destroyed).toBeTrue();
350356
}))));
351357
});
352358

packages/platform-browser/src/browser.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {CommonModule, DOCUMENT, XhrFactory, ɵPLATFORM_BROWSER_ID as PLATFORM_BROWSER_ID} from '@angular/common';
10-
import {APP_ID, ApplicationModule, ApplicationRef, createPlatformFactory, ErrorHandler, ImportedNgModuleProviders, Inject, InjectionToken, ModuleWithProviders, NgModule, NgZone, Optional, PLATFORM_ID, PLATFORM_INITIALIZER, platformCore, PlatformRef, Provider, RendererFactory2, SkipSelf, StaticProvider, Testability, TestabilityRegistry, Type, ɵINJECTOR_SCOPE as INJECTOR_SCOPE, ɵinternalBootstrapApplication as internalBootstrapApplication, ɵsetDocument, ɵTESTABILITY as TESTABILITY, ɵTESTABILITY_GETTER as TESTABILITY_GETTER} from '@angular/core';
10+
import {APP_ID, ApplicationModule, ApplicationRef, createPlatformFactory, ErrorHandler, ImportedNgModuleProviders, Inject, InjectionToken, ModuleWithProviders, NgModule, NgZone, Optional, PLATFORM_ID, PLATFORM_INITIALIZER, platformCore, PlatformRef, Provider, RendererFactory2, SkipSelf, StaticProvider, Testability, TestabilityRegistry, Type, ɵINJECTOR_SCOPE as INJECTOR_SCOPE, ɵinternalCreateApplication as internalCreateApplication, ɵsetDocument, ɵTESTABILITY as TESTABILITY, ɵTESTABILITY_GETTER as TESTABILITY_GETTER} from '@angular/core';
1111

1212
import {BrowserDomAdapter} from './browser/browser_adapter';
1313
import {SERVER_TRANSITION_PROVIDERS, TRANSITION_ID} from './browser/server-transition';
@@ -22,7 +22,7 @@ import {DomSharedStylesHost, SharedStylesHost} from './dom/shared_styles_host';
2222
const NG_DEV_MODE = typeof ngDevMode === 'undefined' || !!ngDevMode;
2323

2424
/**
25-
* Set of config options available during the bootstrap operation via `bootstrapApplication` call.
25+
* Set of config options available during the application bootstrap operation.
2626
*
2727
* @developerPreview
2828
* @publicApi
@@ -96,14 +96,34 @@ export interface ApplicationConfig {
9696
*/
9797
export function bootstrapApplication(
9898
rootComponent: Type<unknown>, options?: ApplicationConfig): Promise<ApplicationRef> {
99-
return internalBootstrapApplication({
100-
rootComponent,
99+
return internalCreateApplication({rootComponent, ...createProvidersConfig(options)});
100+
}
101+
102+
/**
103+
* Create an instance of an Angular application without bootstrapping any components. This is useful
104+
* for the situation where one wants to decouple application environment creation (a platform and
105+
* associated injectors) from rendering components on a screen. Components can be subsequently
106+
* bootstrapped on the returned `ApplicationRef`.
107+
*
108+
* @param options Extra configuration for the application environment, see `ApplicationConfig` for
109+
* additional info.
110+
* @returns A promise that returns an `ApplicationRef` instance once resolved.
111+
*
112+
* @publicApi
113+
* @developerPreview
114+
*/
115+
export function createApplication(options?: ApplicationConfig) {
116+
return internalCreateApplication(createProvidersConfig(options));
117+
}
118+
119+
function createProvidersConfig(options?: ApplicationConfig) {
120+
return {
101121
appProviders: [
102122
...BROWSER_MODULE_PROVIDERS,
103123
...(options?.providers ?? []),
104124
],
105-
platformProviders: INTERNAL_BROWSER_PLATFORM_PROVIDERS,
106-
});
125+
platformProviders: INTERNAL_BROWSER_PLATFORM_PROVIDERS
126+
};
107127
}
108128

109129
/**

packages/platform-browser/src/platform-browser.ts

Lines changed: 1 addition & 1 deletion
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-
export {ApplicationConfig, bootstrapApplication, BrowserModule, platformBrowser, provideProtractorTestingSupport} from './browser';
9+
export {ApplicationConfig, bootstrapApplication, BrowserModule, createApplication, platformBrowser, provideProtractorTestingSupport} from './browser';
1010
export {Meta, MetaDefinition} from './browser/meta';
1111
export {Title} from './browser/title';
1212
export {disableDebugTools, enableDebugTools} from './browser/tools/tools';

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

@@ -152,7 +152,7 @@ export function renderApplication<T>(rootComponent: Type<T>, options: {
152152
importProvidersFrom(ServerModule),
153153
...(options.providers ?? []),
154154
];
155-
return _render(platform, internalBootstrapApplication({rootComponent, appProviders}));
155+
return _render(platform, internalCreateApplication({rootComponent, appProviders}));
156156
}
157157

158158
/**

packages/platform-server/test/integration_spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -740,7 +740,8 @@ describe('platform-server integration', () => {
740740

741741
// Run the set of tests with regular and standalone components.
742742
[true, false].forEach((isStandalone: boolean) => {
743-
it('using renderModule should work', waitForAsync(() => {
743+
it(`using ${isStandalone ? 'renderApplication' : 'renderModule'} should work`,
744+
waitForAsync(() => {
744745
const options = {document: doc};
745746
const bootstrap = isStandalone ?
746747
renderApplication(MyAsyncServerAppStandalone, {...options, appId: 'simple-cmp'}) :

0 commit comments

Comments
 (0)