/** * @license * Copyright Google Inc. All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import {Observable, Observer, Subscription, merge} from 'rxjs'; import {share} from 'rxjs/operators'; import {ApplicationInitStatus} from './application_init'; import {APP_BOOTSTRAP_LISTENER, PLATFORM_INITIALIZER} from './application_tokens'; import {getCompilerFacade} from './compiler/compiler_facade'; import {Console} from './console'; import {Injectable, InjectionToken, Injector, StaticProvider} from './di'; import {ErrorHandler} from './error_handler'; import {DEFAULT_LOCALE_ID} from './i18n/localization'; import {LOCALE_ID} from './i18n/tokens'; import {Type} from './interface/type'; import {ivyEnabled} from './ivy_switch'; import {COMPILER_OPTIONS, CompilerFactory, CompilerOptions} from './linker/compiler'; import {ComponentFactory, ComponentRef} from './linker/component_factory'; import {ComponentFactoryBoundToModule, ComponentFactoryResolver} from './linker/component_factory_resolver'; import {InternalNgModuleRef, NgModuleFactory, NgModuleRef} from './linker/ng_module_factory'; import {InternalViewRef, ViewRef} from './linker/view_ref'; import {isComponentResourceResolutionQueueEmpty, resolveComponentResources} from './metadata/resource_loading'; import {WtfScopeFn, wtfCreateScope, wtfLeave} from './profile/profile'; import {assertNgModuleType} from './render3/assert'; import {ComponentFactory as R3ComponentFactory} from './render3/component_ref'; import {setLocaleId} from './render3/i18n'; import {NgModuleFactory as R3NgModuleFactory} from './render3/ng_module_ref'; import {Testability, TestabilityRegistry} from './testability/testability'; import {isDevMode} from './util/is_dev_mode'; import {isPromise} from './util/lang'; import {scheduleMicroTask} from './util/microtask'; import {stringify} from './util/stringify'; import {NgZone, NoopNgZone} from './zone/ng_zone'; let _platform: PlatformRef; let compileNgModuleFactory: (injector: Injector, options: CompilerOptions, moduleType: Type) => Promise> = compileNgModuleFactory__PRE_R3__; function compileNgModuleFactory__PRE_R3__( injector: Injector, options: CompilerOptions, moduleType: Type): Promise> { const compilerFactory: CompilerFactory = injector.get(CompilerFactory); const compiler = compilerFactory.createCompiler([options]); return compiler.compileModuleAsync(moduleType); } export function compileNgModuleFactory__POST_R3__( injector: Injector, options: CompilerOptions, moduleType: Type): Promise> { ngDevMode && assertNgModuleType(moduleType); const moduleFactory = new R3NgModuleFactory(moduleType); if (isComponentResourceResolutionQueueEmpty()) { return Promise.resolve(moduleFactory); } const compilerOptions = injector.get(COMPILER_OPTIONS, []).concat(options); const compilerProviders = _mergeArrays(compilerOptions.map(o => o.providers !)); // In case there are no compiler providers, we just return the module factory as // there won't be any resource loader. This can happen with Ivy, because AOT compiled // modules can be still passed through "bootstrapModule". In that case we shouldn't // unnecessarily require the JIT compiler. if (compilerProviders.length === 0) { return Promise.resolve(moduleFactory); } const compiler = getCompilerFacade(); const compilerInjector = Injector.create({providers: compilerProviders}); const resourceLoader = compilerInjector.get(compiler.ResourceLoader); // The resource loader can also return a string while the "resolveComponentResources" // always expects a promise. Therefore we need to wrap the returned value in a promise. return resolveComponentResources(url => Promise.resolve(resourceLoader.get(url))) .then(() => moduleFactory); } let isBoundToModule: (cf: ComponentFactory) => boolean = isBoundToModule__PRE_R3__; export function isBoundToModule__PRE_R3__(cf: ComponentFactory): boolean { return cf instanceof ComponentFactoryBoundToModule; } export function isBoundToModule__POST_R3__(cf: ComponentFactory): boolean { return (cf as R3ComponentFactory).isBoundToModule; } export const ALLOW_MULTIPLE_PLATFORMS = new InjectionToken('AllowMultipleToken'); /** * A token for third-party components that can register themselves with NgProbe. * * @publicApi */ export class NgProbeToken { constructor(public name: string, public token: any) {} } /** * Creates a platform. * Platforms have to be eagerly created via this function. * * @publicApi */ export function createPlatform(injector: Injector): PlatformRef { if (_platform && !_platform.destroyed && !_platform.injector.get(ALLOW_MULTIPLE_PLATFORMS, false)) { throw new Error( 'There can be only one platform. Destroy the previous one to create a new one.'); } _platform = injector.get(PlatformRef); const inits = injector.get(PLATFORM_INITIALIZER, null); if (inits) inits.forEach((init: any) => init()); return _platform; } /** * Creates a factory for a platform * * @publicApi */ export function createPlatformFactory( parentPlatformFactory: ((extraProviders?: StaticProvider[]) => PlatformRef) | null, name: string, providers: StaticProvider[] = []): (extraProviders?: StaticProvider[]) => PlatformRef { const desc = `Platform: ${name}`; const marker = new InjectionToken(desc); return (extraProviders: StaticProvider[] = []) => { let platform = getPlatform(); if (!platform || platform.injector.get(ALLOW_MULTIPLE_PLATFORMS, false)) { if (parentPlatformFactory) { parentPlatformFactory( providers.concat(extraProviders).concat({provide: marker, useValue: true})); } else { const injectedProviders: StaticProvider[] = providers.concat(extraProviders).concat({provide: marker, useValue: true}); createPlatform(Injector.create({providers: injectedProviders, name: desc})); } } return assertPlatform(marker); }; } /** * Checks that there currently is a platform which contains the given token as a provider. * * @publicApi */ export function assertPlatform(requiredToken: any): PlatformRef { const platform = getPlatform(); if (!platform) { throw new Error('No platform exists!'); } if (!platform.injector.get(requiredToken, null)) { throw new Error( 'A platform with a different configuration has been created. Please destroy it first.'); } return platform; } /** * Destroy the existing platform. * * @publicApi */ export function destroyPlatform(): void { if (_platform && !_platform.destroyed) { _platform.destroy(); } } /** * Returns the current platform. * * @publicApi */ export function getPlatform(): PlatformRef|null { return _platform && !_platform.destroyed ? _platform : null; } /** * Provides additional options to the bootstraping process. * * */ export interface BootstrapOptions { /** * Optionally specify which `NgZone` should be used. * * - Provide your own `NgZone` instance. * - `zone.js` - Use default `NgZone` which requires `Zone.js`. * - `noop` - Use `NoopNgZone` which does nothing. */ ngZone?: NgZone|'zone.js'|'noop'; } /** * The Angular platform is the entry point for Angular on a web page. Each page * has exactly one platform, and services (such as reflection) which are common * to every Angular application running on the page are bound in its scope. * * A page's platform is initialized implicitly when a platform is created via a platform factory * (e.g. {@link platformBrowser}), or explicitly by calling the {@link createPlatform} function. * * @publicApi */ @Injectable() export class PlatformRef { private _modules: NgModuleRef[] = []; private _destroyListeners: Function[] = []; private _destroyed: boolean = false; /** @internal */ constructor(private _injector: Injector) {} /** * Creates an instance of an `@NgModule` for the given platform * for offline compilation. * * @usageNotes * ### Simple Example * * ```typescript * my_module.ts: * * @NgModule({ * imports: [BrowserModule] * }) * class MyModule {} * * main.ts: * import {MyModuleNgFactory} from './my_module.ngfactory'; * import {platformBrowser} from '@angular/platform-browser'; * * let moduleRef = platformBrowser().bootstrapModuleFactory(MyModuleNgFactory); * ``` */ bootstrapModuleFactory(moduleFactory: NgModuleFactory, options?: BootstrapOptions): Promise> { // Note: We need to create the NgZone _before_ we instantiate the module, // as instantiating the module creates some providers eagerly. // So we create a mini parent injector that just contains the new NgZone and // pass that as parent to the NgModuleFactory. const ngZoneOption = options ? options.ngZone : undefined; const ngZone = getNgZone(ngZoneOption); const providers: StaticProvider[] = [{provide: NgZone, useValue: ngZone}]; // Attention: Don't use ApplicationRef.run here, // as we want to be sure that all possible constructor calls are inside `ngZone.run`! return ngZone.run(() => { const ngZoneInjector = Injector.create( {providers: providers, parent: this.injector, name: moduleFactory.moduleType.name}); const moduleRef = >moduleFactory.create(ngZoneInjector); const exceptionHandler: ErrorHandler = moduleRef.injector.get(ErrorHandler, null); if (!exceptionHandler) { throw new Error('No ErrorHandler. Is platform module (BrowserModule) included?'); } // If the `LOCALE_ID` provider is defined at bootstrap we set the value for runtime i18n (ivy) if (ivyEnabled) { const localeId = moduleRef.injector.get(LOCALE_ID, DEFAULT_LOCALE_ID); setLocaleId(localeId || DEFAULT_LOCALE_ID); } moduleRef.onDestroy(() => remove(this._modules, moduleRef)); ngZone !.runOutsideAngular( () => ngZone !.onError.subscribe( {next: (error: any) => { exceptionHandler.handleError(error); }})); return _callAndReportToErrorHandler(exceptionHandler, ngZone !, () => { const initStatus: ApplicationInitStatus = moduleRef.injector.get(ApplicationInitStatus); initStatus.runInitializers(); return initStatus.donePromise.then(() => { this._moduleDoBootstrap(moduleRef); return moduleRef; }); }); }); } /** * Creates an instance of an `@NgModule` for a given platform using the given runtime compiler. * * @usageNotes * ### Simple Example * * ```typescript * @NgModule({ * imports: [BrowserModule] * }) * class MyModule {} * * let moduleRef = platformBrowser().bootstrapModule(MyModule); * ``` * */ bootstrapModule( moduleType: Type, compilerOptions: (CompilerOptions&BootstrapOptions)| Array = []): Promise> { const options = optionsReducer({}, compilerOptions); return compileNgModuleFactory(this.injector, options, moduleType) .then(moduleFactory => this.bootstrapModuleFactory(moduleFactory, options)); } private _moduleDoBootstrap(moduleRef: InternalNgModuleRef): void { const appRef = moduleRef.injector.get(ApplicationRef) as ApplicationRef; if (moduleRef._bootstrapComponents.length > 0) { moduleRef._bootstrapComponents.forEach(f => appRef.bootstrap(f)); } else if (moduleRef.instance.ngDoBootstrap) { moduleRef.instance.ngDoBootstrap(appRef); } else { throw new Error( `The module ${stringify(moduleRef.instance.constructor)} was bootstrapped, but it does not declare "@NgModule.bootstrap" components nor a "ngDoBootstrap" method. ` + `Please define one of these.`); } this._modules.push(moduleRef); } /** * Register a listener to be called when the platform is disposed. */ onDestroy(callback: () => void): void { this._destroyListeners.push(callback); } /** * Retrieve the platform {@link Injector}, which is the parent injector for * every Angular application on the page and provides singleton providers. */ get injector(): Injector { return this._injector; } /** * Destroy the Angular platform and all Angular applications on the page. */ destroy() { if (this._destroyed) { throw new Error('The platform has already been destroyed!'); } this._modules.slice().forEach(module => module.destroy()); this._destroyListeners.forEach(listener => listener()); this._destroyed = true; } get destroyed() { return this._destroyed; } } function getNgZone(ngZoneOption?: NgZone | 'zone.js' | 'noop'): NgZone { let ngZone: NgZone; if (ngZoneOption === 'noop') { ngZone = new NoopNgZone(); } else { ngZone = (ngZoneOption === 'zone.js' ? undefined : ngZoneOption) || new NgZone({enableLongStackTrace: isDevMode()}); } return ngZone; } function _callAndReportToErrorHandler( errorHandler: ErrorHandler, ngZone: NgZone, callback: () => any): any { try { const result = callback(); if (isPromise(result)) { return result.catch((e: any) => { ngZone.runOutsideAngular(() => errorHandler.handleError(e)); // rethrow as the exception handler might not do it throw e; }); } return result; } catch (e) { ngZone.runOutsideAngular(() => errorHandler.handleError(e)); // rethrow as the exception handler might not do it throw e; } } function optionsReducer(dst: any, objs: T | T[]): T { if (Array.isArray(objs)) { dst = objs.reduce(optionsReducer, dst); } else { dst = {...dst, ...(objs as any)}; } return dst; } /** * A reference to an Angular application running on a page. * * @usageNotes * * {@a is-stable-examples} * ### isStable examples and caveats * * Note two important points about `isStable`, demonstrated in the examples below: * - the application will never be stable if you start any kind * of recurrent asynchronous task when the application starts * (for example for a polling process, started with a `setInterval`, a `setTimeout` * or using RxJS operators like `interval`); * - the `isStable` Observable runs outside of the Angular zone. * * Let's imagine that you start a recurrent task * (here incrementing a counter, using RxJS `interval`), * and at the same time subscribe to `isStable`. * * ``` * constructor(appRef: ApplicationRef) { * appRef.isStable.pipe( * filter(stable => stable) * ).subscribe(() => console.log('App is stable now'); * interval(1000).subscribe(counter => console.log(counter)); * } * ``` * In this example, `isStable` will never emit `true`, * and the trace "App is stable now" will never get logged. * * If you want to execute something when the app is stable, * you have to wait for the application to be stable * before starting your polling process. * * ``` * constructor(appRef: ApplicationRef) { * appRef.isStable.pipe( * first(stable => stable), * tap(stable => console.log('App is stable now')), * switchMap(() => interval(1000)) * ).subscribe(counter => console.log(counter)); * } * ``` * In this example, the trace "App is stable now" will be logged * and then the counter starts incrementing every second. * * Note also that this Observable runs outside of the Angular zone, * which means that the code in the subscription * to this Observable will not trigger the change detection. * * Let's imagine that instead of logging the counter value, * you update a field of your component * and display it in its template. * * ``` * constructor(appRef: ApplicationRef) { * appRef.isStable.pipe( * first(stable => stable), * switchMap(() => interval(1000)) * ).subscribe(counter => this.value = counter); * } * ``` * As the `isStable` Observable runs outside the zone, * the `value` field will be updated properly, * but the template will not be refreshed! * * You'll have to manually trigger the change detection to update the template. * * ``` * constructor(appRef: ApplicationRef, cd: ChangeDetectorRef) { * appRef.isStable.pipe( * first(stable => stable), * switchMap(() => interval(1000)) * ).subscribe(counter => { * this.value = counter; * cd.detectChanges(); * }); * } * ``` * * Or make the subscription callback run inside the zone. * * ``` * constructor(appRef: ApplicationRef, zone: NgZone) { * appRef.isStable.pipe( * first(stable => stable), * switchMap(() => interval(1000)) * ).subscribe(counter => zone.run(() => this.value = counter)); * } * ``` * * @publicApi */ @Injectable() export class ApplicationRef { /** @internal */ static _tickScope: WtfScopeFn = wtfCreateScope('ApplicationRef#tick()'); private _bootstrapListeners: ((compRef: ComponentRef) => void)[] = []; private _views: InternalViewRef[] = []; private _runningTick: boolean = false; private _enforceNoNewChanges: boolean = false; private _stable = true; /** * Get a list of component types registered to this application. * This list is populated even before the component is created. */ public readonly componentTypes: Type[] = []; /** * Get a list of components registered to this application. */ public readonly components: ComponentRef[] = []; /** * Returns an Observable that indicates when the application is stable or unstable. * * @see [Usage notes](#is-stable-examples) for examples and caveats when using this API. */ // TODO(issue/24571): remove '!'. public readonly isStable !: Observable; /** @internal */ constructor( private _zone: NgZone, private _console: Console, private _injector: Injector, private _exceptionHandler: ErrorHandler, private _componentFactoryResolver: ComponentFactoryResolver, private _initStatus: ApplicationInitStatus) { this._enforceNoNewChanges = isDevMode(); this._zone.onMicrotaskEmpty.subscribe( {next: () => { this._zone.run(() => { this.tick(); }); }}); const isCurrentlyStable = new Observable((observer: Observer) => { this._stable = this._zone.isStable && !this._zone.hasPendingMacrotasks && !this._zone.hasPendingMicrotasks; this._zone.runOutsideAngular(() => { observer.next(this._stable); observer.complete(); }); }); const isStable = new Observable((observer: Observer) => { // Create the subscription to onStable outside the Angular Zone so that // the callback is run outside the Angular Zone. let stableSub: Subscription; this._zone.runOutsideAngular(() => { stableSub = this._zone.onStable.subscribe(() => { NgZone.assertNotInAngularZone(); // Check whether there are no pending macro/micro tasks in the next tick // to allow for NgZone to update the state. scheduleMicroTask(() => { if (!this._stable && !this._zone.hasPendingMacrotasks && !this._zone.hasPendingMicrotasks) { this._stable = true; observer.next(true); } }); }); }); const unstableSub: Subscription = this._zone.onUnstable.subscribe(() => { NgZone.assertInAngularZone(); if (this._stable) { this._stable = false; this._zone.runOutsideAngular(() => { observer.next(false); }); } }); return () => { stableSub.unsubscribe(); unstableSub.unsubscribe(); }; }); (this as{isStable: Observable}).isStable = merge(isCurrentlyStable, isStable.pipe(share())); } /** * Bootstrap a new component at the root level of the application. * * @usageNotes * ### Bootstrap process * * When bootstrapping a new root component into an application, Angular mounts the * specified application component onto DOM elements identified by the componentType's * selector and kicks off automatic change detection to finish initializing the component. * * Optionally, a component can be mounted onto a DOM element that does not match the * componentType's selector. * * ### Example * {@example core/ts/platform/platform.ts region='longform'} */ bootstrap(componentOrFactory: ComponentFactory|Type, rootSelectorOrNode?: string|any): ComponentRef { if (!this._initStatus.done) { throw new Error( 'Cannot bootstrap as there are still asynchronous initializers running. Bootstrap components in the `ngDoBootstrap` method of the root module.'); } let componentFactory: ComponentFactory; if (componentOrFactory instanceof ComponentFactory) { componentFactory = componentOrFactory; } else { componentFactory = this._componentFactoryResolver.resolveComponentFactory(componentOrFactory) !; } this.componentTypes.push(componentFactory.componentType); // Create a factory associated with the current module if it's not bound to some other const ngModule = isBoundToModule(componentFactory) ? null : this._injector.get(NgModuleRef); const selectorOrNode = rootSelectorOrNode || componentFactory.selector; const compRef = componentFactory.create(Injector.NULL, [], selectorOrNode, ngModule); compRef.onDestroy(() => { this._unloadComponent(compRef); }); const testability = compRef.injector.get(Testability, null); if (testability) { compRef.injector.get(TestabilityRegistry) .registerApplication(compRef.location.nativeElement, testability); } this._loadComponent(compRef); if (isDevMode()) { this._console.log( `Angular is running in the development mode. Call enableProdMode() to enable the production mode.`); } return compRef; } /** * Invoke this method to explicitly process change detection and its side-effects. * * In development mode, `tick()` also performs a second change detection cycle to ensure that no * further changes are detected. If additional changes are picked up during this second cycle, * bindings in the app have side-effects that cannot be resolved in a single change detection * pass. * In this case, Angular throws an error, since an Angular application can only have one change * detection pass during which all change detection must complete. */ tick(): void { if (this._runningTick) { throw new Error('ApplicationRef.tick is called recursively'); } const scope = ApplicationRef._tickScope(); try { this._runningTick = true; for (let view of this._views) { view.detectChanges(); } if (this._enforceNoNewChanges) { for (let view of this._views) { view.checkNoChanges(); } } } catch (e) { // Attention: Don't rethrow as it could cancel subscriptions to Observables! this._zone.runOutsideAngular(() => this._exceptionHandler.handleError(e)); } finally { this._runningTick = false; wtfLeave(scope); } } /** * Attaches a view so that it will be dirty checked. * The view will be automatically detached when it is destroyed. * This will throw if the view is already attached to a ViewContainer. */ attachView(viewRef: ViewRef): void { const view = (viewRef as InternalViewRef); this._views.push(view); view.attachToAppRef(this); } /** * Detaches a view from dirty checking again. */ detachView(viewRef: ViewRef): void { const view = (viewRef as InternalViewRef); remove(this._views, view); view.detachFromAppRef(); } private _loadComponent(componentRef: ComponentRef): void { this.attachView(componentRef.hostView); this.tick(); this.components.push(componentRef); // Get the listeners lazily to prevent DI cycles. const listeners = this._injector.get(APP_BOOTSTRAP_LISTENER, []).concat(this._bootstrapListeners); listeners.forEach((listener) => listener(componentRef)); } private _unloadComponent(componentRef: ComponentRef): void { this.detachView(componentRef.hostView); remove(this.components, componentRef); } /** @internal */ ngOnDestroy() { // TODO(alxhub): Dispose of the NgZone. this._views.slice().forEach((view) => view.destroy()); } /** * Returns the number of attached views. */ get viewCount() { return this._views.length; } } function remove(list: T[], el: T): void { const index = list.indexOf(el); if (index > -1) { list.splice(index, 1); } } function _mergeArrays(parts: any[][]): any[] { const result: any[] = []; parts.forEach((part) => part && result.push(...part)); return result; }