Skip to content

Commit

Permalink
feat(core): introduce createRootEnvironment API
Browse files Browse the repository at this point in the history
The `createRootEnvironment` makes it possible to create an
application instance (represented by the `ApplicationRef`)
without bootstrapping any components. It is useful in the
situations where ones want 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 environnement linked to a
created application.
  • Loading branch information
pkozlowski-opensource committed Jul 6, 2022
1 parent 84376ba commit 26173a7
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 34 deletions.
3 changes: 3 additions & 0 deletions goldens/public-api/platform-browser/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ export class By {
static directive(type: Type<any>): Predicate<DebugNode>;
}

// @public
export function createRootEnvironment(options?: ApplicationConfig): Promise<ApplicationRef>;

// @public
export function disableDebugTools(): void;

Expand Down
2 changes: 1 addition & 1 deletion goldens/size-tracking/integration-payloads.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"standalone-bootstrap": {
"uncompressed": {
"runtime": 1090,
"main": 83013,
"main": 83875,
"polyfills": 33945
}
},
Expand Down
53 changes: 30 additions & 23 deletions packages/core/src/application_ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,16 +187,19 @@ export function runPlatformInitializers(injector: Injector): void {
* @returns A promise that returns an `ApplicationRef` instance once resolved.
*/
export function internalBootstrapApplication(config: {
rootComponent: Type<unknown>,
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 +208,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 +227,30 @@ export function internalBootstrapApplication(config: {
}
});
});
return _callAndReportToErrorHandler(exceptionHandler!, ngZone, () => {
const initStatus = appInjector.get(ApplicationInitStatus);
initStatus.runInitializers();
return initStatus.donePromise.then(() => {
const localeId = appInjector.get(LOCALE_ID, DEFAULT_LOCALE_ID);
setLocaleId(localeId || DEFAULT_LOCALE_ID);

const appRef = appInjector.get(ApplicationRef);
envInjector.onDestroy(() => onErrorSubscription.unsubscribe());

// 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);
// 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(() => {
onPlatformDestroyListeners.delete(destroyListener);
});

appRef.onDestroy(() => {
onPlatformDestroyListeners?.delete(destroyListener);
onErrorSubscription.unsubscribe();
});
return _callAndReportToErrorHandler(exceptionHandler!, ngZone, () => {
const initStatus = envInjector.get(ApplicationInitStatus);
initStatus.runInitializers();

appRef.bootstrap(rootComponent);
const localeId = envInjector.get(LOCALE_ID, DEFAULT_LOCALE_ID);
setLocaleId(localeId || DEFAULT_LOCALE_ID);

return initStatus.donePromise.then(() => {
const appRef = envInjector.get(ApplicationRef);
if (rootComponent !== undefined) {
appRef.bootstrap(rootComponent);
}
return appRef;
});
});
Expand Down Expand Up @@ -491,7 +498,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
12 changes: 9 additions & 3 deletions packages/core/test/application_ref_spec.ts
Original file line number Diff line number Diff line change
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
30 changes: 25 additions & 5 deletions packages/platform-browser/src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 internalBootstrapApplication({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 environnement 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 environnement, see `ApplicationConfig` for
* additional info.
* @returns A promise that returns an `ApplicationRef` instance once resolved.
*
* @publicApi
* @developerPreview
*/
export function createRootEnvironment(options?: ApplicationConfig) {
return internalBootstrapApplication(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
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
*/

export {ApplicationConfig, bootstrapApplication, BrowserModule, platformBrowser, provideProtractorTestingSupport} from './browser';
export {ApplicationConfig, bootstrapApplication, BrowserModule, createRootEnvironment, platformBrowser, provideProtractorTestingSupport} from './browser';
export {Meta, MetaDefinition} from './browser/meta';
export {Title} from './browser/title';
export {disableDebugTools, enableDebugTools} from './browser/tools/tools';
Expand Down
3 changes: 2 additions & 1 deletion packages/platform-server/test/integration_spec.ts
Original file line number Diff line number Diff line change
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 26173a7

Please sign in to comment.