Skip to content

Commit

Permalink
feat(core): introduce createApplication API (#46475)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
pkozlowski-opensource authored and thePunderWoman committed Jul 18, 2022
1 parent 6fed377 commit 4b377d3
Show file tree
Hide file tree
Showing 8 changed files with 76 additions and 38 deletions.
3 changes: 3 additions & 0 deletions goldens/public-api/platform-browser/index.md
Expand Up @@ -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;

Expand Down
56 changes: 32 additions & 24 deletions packages/core/src/application_ref.ts
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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;
});
});
Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/core_private_export.ts
Expand Up @@ -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';
Expand Down
12 changes: 9 additions & 3 deletions packages/core/test/application_ref_spec.ts
Expand Up @@ -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();
}))));
});

Expand Down
32 changes: 26 additions & 6 deletions packages/platform-browser/src/browser.ts
Expand Up @@ -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';
Expand All @@ -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
Expand Down Expand Up @@ -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
};
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/platform-browser/src/platform-browser.ts
Expand Up @@ -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';
Expand Down
4 changes: 2 additions & 2 deletions packages/platform-server/src/utils.ts
Expand Up @@ -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';

Expand Down Expand Up @@ -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}));
}

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/platform-server/test/integration_spec.ts
Expand Up @@ -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'}) :
Expand Down

0 comments on commit 4b377d3

Please sign in to comment.