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
This commit is contained in:
Pawel Kozlowski 2022-07-06 09:18:29 +02:00 committed by Jessica Janiuk
parent 6fed377140
commit 4b377d3a6d
8 changed files with 76 additions and 38 deletions

View file

@ -62,6 +62,9 @@ export class By {
static directive(type: Type<any>): Predicate<DebugNode>;
}
// @public
export function createApplication(options?: ApplicationConfig): Promise<ApplicationRef>;
// @public
export function disableDebugTools(): void;

View file

@ -177,7 +177,8 @@ export function runPlatformInitializers(injector: Injector): void {
}
/**
* Internal bootstrap application API that implements the core bootstrap logic.
* Internal create application API that implements the core application creation logic and optional
* bootstrap logic.
*
* Platforms (such as `platform-browser`) may require different set of application and platform
* 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 {
*
* @returns A promise that returns an `ApplicationRef` instance once resolved.
*/
export function internalBootstrapApplication(config: {
rootComponent: Type<unknown>,
export function internalCreateApplication(config: {
rootComponent?: Type<unknown>,
appProviders?: Array<Provider|ImportedNgModuleProviders>,
platformProviders?: Provider[],
}): Promise<ApplicationRef> {
const {rootComponent, appProviders, platformProviders} = config;
NG_DEV_MODE && assertStandaloneComponentType(rootComponent);
if (NG_DEV_MODE && rootComponent !== undefined) {
assertStandaloneComponentType(rootComponent);
}
const platformInjector = createOrReusePlatformInjector(platformProviders as StaticProvider[]);
const ngZone = new NgZone(getNgZoneOptions());
const ngZone = getNgZone('zone.js', getNgZoneOptions());
return ngZone.run(() => {
// Create root application injector based on a set of providers configured at the platform
@ -205,10 +209,11 @@ export function internalBootstrapApplication(config: {
{provide: NgZone, useValue: ngZone}, //
...(appProviders || []), //
];
const appInjector = createEnvironmentInjector(
const envInjector = createEnvironmentInjector(
allAppProviders, platformInjector as EnvironmentInjector, 'Environment Injector');
const exceptionHandler: ErrorHandler|null = appInjector.get(ErrorHandler, null);
const exceptionHandler: ErrorHandler|null = envInjector.get(ErrorHandler, null);
if (NG_DEV_MODE && !exceptionHandler) {
throw new RuntimeError(
RuntimeErrorCode.ERROR_HANDLER_NOT_FOUND,
@ -223,27 +228,30 @@ export function internalBootstrapApplication(config: {
}
});
});
// If the whole platform is destroyed, invoke the `destroy` method
// for all bootstrapped applications as well.
const destroyListener = () => envInjector.destroy();
const onPlatformDestroyListeners = platformInjector.get(PLATFORM_DESTROY_LISTENERS);
onPlatformDestroyListeners.add(destroyListener);
envInjector.onDestroy(() => {
onErrorSubscription.unsubscribe();
onPlatformDestroyListeners.delete(destroyListener);
});
return _callAndReportToErrorHandler(exceptionHandler!, ngZone, () => {
const initStatus = appInjector.get(ApplicationInitStatus);
const initStatus = envInjector.get(ApplicationInitStatus);
initStatus.runInitializers();
return initStatus.donePromise.then(() => {
const localeId = appInjector.get(LOCALE_ID, DEFAULT_LOCALE_ID);
const localeId = envInjector.get(LOCALE_ID, DEFAULT_LOCALE_ID);
setLocaleId(localeId || DEFAULT_LOCALE_ID);
const appRef = appInjector.get(ApplicationRef);
// If the whole platform is destroyed, invoke the `destroy` method
// for all bootstrapped applications as well.
const destroyListener = () => appRef.destroy();
const onPlatformDestroyListeners = platformInjector.get(PLATFORM_DESTROY_LISTENERS, null);
onPlatformDestroyListeners?.add(destroyListener);
appRef.onDestroy(() => {
onPlatformDestroyListeners?.delete(destroyListener);
onErrorSubscription.unsubscribe();
});
appRef.bootstrap(rootComponent);
const appRef = envInjector.get(ApplicationRef);
if (rootComponent !== undefined) {
appRef.bootstrap(rootComponent);
}
return appRef;
});
});
@ -493,7 +501,7 @@ export class PlatformRef {
}
private _moduleDoBootstrap(moduleRef: InternalNgModuleRef<any>): void {
const appRef = moduleRef.injector.get(ApplicationRef) as ApplicationRef;
const appRef = moduleRef.injector.get(ApplicationRef);
if (moduleRef._bootstrapComponents.length > 0) {
moduleRef._bootstrapComponents.forEach(f => appRef.bootstrap(f));
} else if (moduleRef.instance.ngDoBootstrap) {

View file

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
export {ALLOW_MULTIPLE_PLATFORMS as ɵALLOW_MULTIPLE_PLATFORMS, internalBootstrapApplication as ɵinternalBootstrapApplication} from './application_ref';
export {ALLOW_MULTIPLE_PLATFORMS as ɵALLOW_MULTIPLE_PLATFORMS, internalCreateApplication as ɵinternalCreateApplication} from './application_ref';
export {APP_ID_RANDOM_PROVIDER as ɵAPP_ID_RANDOM_PROVIDER} from './application_tokens';
export {defaultIterableDiffers as ɵdefaultIterableDiffers, defaultKeyValueDiffers as ɵdefaultKeyValueDiffers} from './change_detection/change_detection';
export {ChangeDetectorStatus as ɵChangeDetectorStatus, isDefaultChangeDetectionStrategy as ɵisDefaultChangeDetectionStrategy} from './change_detection/constants';

View file

@ -333,20 +333,26 @@ class SomeComponent {
withModule(
{providers},
waitForAsync(inject([EnvironmentInjector], (parentInjector: EnvironmentInjector) => {
// 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 = EnvironmentInjector&{destroyed?: boolean};
createRootEl();
const injector = createApplicationRefInjector(parentInjector);
const injector = createApplicationRefInjector(parentInjector) as DestroyableInjector;
const appRef = injector.get(ApplicationRef);
appRef.bootstrap(SomeComponent);
expect(appRef.destroyed).toBeFalse();
expect((injector as any).destroyed).toBeFalse();
expect(injector.destroyed).toBeFalse();
appRef.destroy();
expect(appRef.destroyed).toBeTrue();
expect((injector as any).destroyed).toBeTrue();
expect(injector.destroyed).toBeTrue();
}))));
});

View file

@ -7,7 +7,7 @@
*/
import {CommonModule, DOCUMENT, XhrFactory, ɵPLATFORM_BROWSER_ID as PLATFORM_BROWSER_ID} from '@angular/common';
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';
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';
import {BrowserDomAdapter} from './browser/browser_adapter';
import {SERVER_TRANSITION_PROVIDERS, TRANSITION_ID} from './browser/server-transition';
@ -22,7 +22,7 @@ import {DomSharedStylesHost, SharedStylesHost} from './dom/shared_styles_host';
const NG_DEV_MODE = typeof ngDevMode === 'undefined' || !!ngDevMode;
/**
* Set of config options available during the bootstrap operation via `bootstrapApplication` call.
* Set of config options available during the application bootstrap operation.
*
* @developerPreview
* @publicApi
@ -96,14 +96,34 @@ export interface ApplicationConfig {
*/
export function bootstrapApplication(
rootComponent: Type<unknown>, options?: ApplicationConfig): Promise<ApplicationRef> {
return internalBootstrapApplication({
rootComponent,
return internalCreateApplication({rootComponent, ...createProvidersConfig(options)});
}
/**
* Create an instance of an Angular application without bootstrapping any components. This is useful
* for the situation where one wants to decouple application environment creation (a platform and
* associated injectors) from rendering components on a screen. Components can be subsequently
* bootstrapped on the returned `ApplicationRef`.
*
* @param options Extra configuration for the application environment, see `ApplicationConfig` for
* additional info.
* @returns A promise that returns an `ApplicationRef` instance once resolved.
*
* @publicApi
* @developerPreview
*/
export function createApplication(options?: ApplicationConfig) {
return internalCreateApplication(createProvidersConfig(options));
}
function createProvidersConfig(options?: ApplicationConfig) {
return {
appProviders: [
...BROWSER_MODULE_PROVIDERS,
...(options?.providers ?? []),
],
platformProviders: INTERNAL_BROWSER_PLATFORM_PROVIDERS,
});
platformProviders: INTERNAL_BROWSER_PLATFORM_PROVIDERS
};
}
/**

View file

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
export {ApplicationConfig, bootstrapApplication, BrowserModule, platformBrowser, provideProtractorTestingSupport} from './browser';
export {ApplicationConfig, bootstrapApplication, BrowserModule, createApplication, platformBrowser, provideProtractorTestingSupport} from './browser';
export {Meta, MetaDefinition} from './browser/meta';
export {Title} from './browser/title';
export {disableDebugTools, enableDebugTools} from './browser/tools/tools';

View file

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ApplicationRef, ImportedNgModuleProviders, importProvidersFrom, NgModuleFactory, NgModuleRef, PlatformRef, Provider, StaticProvider, Type, ɵinternalBootstrapApplication as internalBootstrapApplication, ɵisPromise} from '@angular/core';
import {ApplicationRef, ImportedNgModuleProviders, importProvidersFrom, NgModuleFactory, NgModuleRef, PlatformRef, Provider, StaticProvider, Type, ɵinternalCreateApplication as internalCreateApplication, ɵisPromise} from '@angular/core';
import {BrowserModule, ɵTRANSITION_ID} from '@angular/platform-browser';
import {first} from 'rxjs/operators';
@ -152,7 +152,7 @@ export function renderApplication<T>(rootComponent: Type<T>, options: {
importProvidersFrom(ServerModule),
...(options.providers ?? []),
];
return _render(platform, internalBootstrapApplication({rootComponent, appProviders}));
return _render(platform, internalCreateApplication({rootComponent, appProviders}));
}
/**

View file

@ -740,7 +740,8 @@ describe('platform-server integration', () => {
// Run the set of tests with regular and standalone components.
[true, false].forEach((isStandalone: boolean) => {
it('using renderModule should work', waitForAsync(() => {
it(`using ${isStandalone ? 'renderApplication' : 'renderModule'} should work`,
waitForAsync(() => {
const options = {document: doc};
const bootstrap = isStandalone ?
renderApplication(MyAsyncServerAppStandalone, {...options, appId: 'simple-cmp'}) :