diff --git a/goldens/public-api/platform-browser/index.md b/goldens/public-api/platform-browser/index.md index c07d557e08da5..c0383cc324114 100644 --- a/goldens/public-api/platform-browser/index.md +++ b/goldens/public-api/platform-browser/index.md @@ -62,6 +62,9 @@ export class By { static directive(type: Type): Predicate; } +// @public +export function createApplication(options?: ApplicationConfig): Promise; + // @public export function disableDebugTools(): void; diff --git a/packages/core/src/application_ref.ts b/packages/core/src/application_ref.ts index 36777bfbaac37..f1f359dd14d61 100644 --- a/packages/core/src/application_ref.ts +++ b/packages/core/src/application_ref.ts @@ -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, +export function internalCreateApplication(config: { + rootComponent?: Type, appProviders?: Array, platformProviders?: Provider[], }): Promise { 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): 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) { diff --git a/packages/core/src/core_private_export.ts b/packages/core/src/core_private_export.ts index 8365cc408f386..cc9f4e37deaa2 100644 --- a/packages/core/src/core_private_export.ts +++ b/packages/core/src/core_private_export.ts @@ -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'; diff --git a/packages/core/test/application_ref_spec.ts b/packages/core/test/application_ref_spec.ts index 1ebc542f81806..060b600f05eb1 100644 --- a/packages/core/test/application_ref_spec.ts +++ b/packages/core/test/application_ref_spec.ts @@ -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(); })))); }); diff --git a/packages/platform-browser/src/browser.ts b/packages/platform-browser/src/browser.ts index 4842fe0b829ee..1ae8c9cc547f0 100644 --- a/packages/platform-browser/src/browser.ts +++ b/packages/platform-browser/src/browser.ts @@ -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, options?: ApplicationConfig): Promise { - 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 + }; } /** diff --git a/packages/platform-browser/src/platform-browser.ts b/packages/platform-browser/src/platform-browser.ts index cef84560c34c8..b73d101fe8993 100644 --- a/packages/platform-browser/src/platform-browser.ts +++ b/packages/platform-browser/src/platform-browser.ts @@ -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'; diff --git a/packages/platform-server/src/utils.ts b/packages/platform-server/src/utils.ts index c103a35be2859..896b5775cd551 100644 --- a/packages/platform-server/src/utils.ts +++ b/packages/platform-server/src/utils.ts @@ -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(rootComponent: Type, options: { importProvidersFrom(ServerModule), ...(options.providers ?? []), ]; - return _render(platform, internalBootstrapApplication({rootComponent, appProviders})); + return _render(platform, internalCreateApplication({rootComponent, appProviders})); } /** diff --git a/packages/platform-server/test/integration_spec.ts b/packages/platform-server/test/integration_spec.ts index fa02aaed104bb..36cd64d8ddfc9 100644 --- a/packages/platform-server/test/integration_spec.ts +++ b/packages/platform-server/test/integration_spec.ts @@ -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'}) :