From e3759f7a7361991e2e3fd4de67ab139babd7c166 Mon Sep 17 00:00:00 2001 From: Jason Aden Date: Wed, 9 May 2018 16:49:39 -0700 Subject: [PATCH] feat(ivy): add support of ApplicationRef.bootstrapModuleFactory (#23811) PR Close #23811 --- packages/core/src/application_module.ts | 45 +++-- .../core/src/core_render3_private_export.ts | 2 + packages/core/src/di.ts | 2 +- packages/core/src/di/injector.ts | 9 +- packages/core/src/di/provider.ts | 8 +- packages/core/src/di/r3_injector.ts | 29 +-- packages/core/src/render3/assert.ts | 4 +- packages/core/src/render3/component.ts | 49 ++--- packages/core/src/render3/component_ref.ts | 176 ++++++++++++++++++ packages/core/src/render3/di.ts | 8 +- packages/core/src/render3/index.ts | 7 +- packages/core/src/render3/instructions.ts | 65 ++++--- packages/core/src/render3/interfaces/view.ts | 4 +- packages/core/src/render3/ng_module_ref.ts | 73 ++++++++ packages/core/src/render3/node_assert.ts | 6 +- .../core/src/render3/node_manipulation.ts | 2 +- .../core/src/render3/node_selector_matcher.ts | 4 +- packages/core/src/render3/query.ts | 8 +- packages/core/src/render3/view_ref.ts | 18 +- .../test/application_ref_integration_spec.ts | 95 ++++++++++ .../hello_world/bundle.golden_symbols.json | 3 + .../hello_world_r2/bundle.golden_symbols.json | 2 +- .../bundling/todo/bundle.golden_symbols.json | 5 +- packages/core/test/di/r3_injector_spec.ts | 20 ++ packages/core/test/render3/component_spec.ts | 27 ++- packages/core/test/render3/render_util.ts | 2 +- packages/platform-browser/src/browser.ts | 61 +++--- tools/public_api_guard/core/core.d.ts | 9 +- .../platform-browser/platform-browser.d.ts | 2 +- 29 files changed, 589 insertions(+), 156 deletions(-) create mode 100644 packages/core/src/render3/component_ref.ts create mode 100644 packages/core/src/render3/ng_module_ref.ts create mode 100644 packages/core/test/application_ref_integration_spec.ts diff --git a/packages/core/src/application_module.ts b/packages/core/src/application_module.ts index 55caa5d9b7..4cbd10b24b 100644 --- a/packages/core/src/application_module.ts +++ b/packages/core/src/application_module.ts @@ -10,11 +10,15 @@ import {APP_INITIALIZER, ApplicationInitStatus} from './application_init'; import {ApplicationRef} from './application_ref'; import {APP_ID_RANDOM_PROVIDER} from './application_tokens'; import {IterableDiffers, KeyValueDiffers, defaultIterableDiffers, defaultKeyValueDiffers} from './change_detection/change_detection'; -import {forwardRef} from './di/forward_ref'; +import {Console} from './console'; +import {InjectionToken, Injector, StaticProvider} from './di'; import {Inject, Optional, SkipSelf} from './di/metadata'; +import {ErrorHandler} from './error_handler'; import {LOCALE_ID} from './i18n/tokens'; +import {ComponentFactoryResolver} from './linker'; import {Compiler} from './linker/compiler'; import {NgModule} from './metadata'; +import {NgZone} from './zone'; export function _iterableDiffersFactory() { return defaultIterableDiffers; @@ -28,27 +32,36 @@ export function _localeFactory(locale?: string): string { return locale || 'en-US'; } +export const APPLICATION_MODULE_PROVIDERS: StaticProvider[] = [ + { + provide: ApplicationRef, + useClass: ApplicationRef, + deps: + [NgZone, Console, Injector, ErrorHandler, ComponentFactoryResolver, ApplicationInitStatus] + }, + { + provide: ApplicationInitStatus, + useClass: ApplicationInitStatus, + deps: [[new Optional(), APP_INITIALIZER]] + }, + {provide: Compiler, useClass: Compiler, deps: []}, + APP_ID_RANDOM_PROVIDER, + {provide: IterableDiffers, useFactory: _iterableDiffersFactory, deps: []}, + {provide: KeyValueDiffers, useFactory: _keyValueDiffersFactory, deps: []}, + { + provide: LOCALE_ID, + useFactory: _localeFactory, + deps: [[new Inject(LOCALE_ID), new Optional(), new SkipSelf()]] + }, +]; + /** * This module includes the providers of @angular/core that are needed * to bootstrap components via `ApplicationRef`. * * @experimental */ -@NgModule({ - providers: [ - ApplicationRef, - ApplicationInitStatus, - Compiler, - APP_ID_RANDOM_PROVIDER, - {provide: IterableDiffers, useFactory: _iterableDiffersFactory}, - {provide: KeyValueDiffers, useFactory: _keyValueDiffersFactory}, - { - provide: LOCALE_ID, - useFactory: _localeFactory, - deps: [[new Inject(LOCALE_ID), new Optional(), new SkipSelf()]] - }, - ], -}) +@NgModule({providers: APPLICATION_MODULE_PROVIDERS}) export class ApplicationModule { // Inject ApplicationRef to make it eager... constructor(appRef: ApplicationRef) {} diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index ff8c7449b4..3998173ec9 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -23,6 +23,8 @@ export { injectAttribute as ɵinjectAttribute, PublicFeature as ɵPublicFeature, NgOnChangesFeature as ɵNgOnChangesFeature, + NgModuleDef as ɵNgModuleDef, + NgModuleType as ɵNgModuleType, CssSelectorList as ɵCssSelectorList, markDirty as ɵmarkDirty, NC as ɵNC, diff --git a/packages/core/src/di.ts b/packages/core/src/di.ts index 62d3c4a38e..1e6b201b84 100644 --- a/packages/core/src/di.ts +++ b/packages/core/src/di.ts @@ -18,7 +18,7 @@ export {forwardRef, resolveForwardRef, ForwardRefFn} from './di/forward_ref'; export {Injectable, InjectableDecorator, InjectableProvider} from './di/injectable'; export {inject, InjectFlags, INJECTOR, Injector} from './di/injector'; export {ReflectiveInjector} from './di/reflective_injector'; -export {StaticProvider, ValueProvider, ExistingProvider, FactoryProvider, Provider, TypeProvider, ClassProvider} from './di/provider'; +export {StaticProvider, ValueProvider, ConstructorSansProvider, ExistingProvider, FactoryProvider, Provider, TypeProvider, ClassProvider} from './di/provider'; export {createInjector} from './di/r3_injector'; export {ResolvedReflectiveFactory, ResolvedReflectiveProvider} from './di/reflective_provider'; export {ReflectiveKey} from './di/reflective_key'; diff --git a/packages/core/src/di/injector.ts b/packages/core/src/di/injector.ts index 5cbd618846..e0286e80b4 100644 --- a/packages/core/src/di/injector.ts +++ b/packages/core/src/di/injector.ts @@ -32,6 +32,9 @@ export const INJECTOR = new InjectionToken('INJECTOR'); export class NullInjector implements Injector { get(token: any, notFoundValue: any = _THROW_IF_NOT_FOUND): any { if (notFoundValue === _THROW_IF_NOT_FOUND) { + // Intentionally left behind: With dev tools open the debugger will stop here. There is no + // reason why correctly written application should cause this exception. + debugger; throw new Error(`NullInjectorError: No provider for ${stringify(token)}!`); } return notFoundValue; @@ -487,11 +490,11 @@ export function injectArgs(types: (Type| InjectionToken| any[])[]): an for (let j = 0; j < arg.length; j++) { const meta = arg[j]; - if (meta instanceof Optional || meta.__proto__.ngMetadataName === 'Optional') { + if (meta instanceof Optional || meta.ngMetadataName === 'Optional') { flags |= InjectFlags.Optional; - } else if (meta instanceof SkipSelf || meta.__proto__.ngMetadataName === 'SkipSelf') { + } else if (meta instanceof SkipSelf || meta.ngMetadataName === 'SkipSelf') { flags |= InjectFlags.SkipSelf; - } else if (meta instanceof Self || meta.__proto__.ngMetadataName === 'Self') { + } else if (meta instanceof Self || meta.ngMetadataName === 'Self') { flags |= InjectFlags.Self; } else if (meta instanceof Inject) { type = meta.token; diff --git a/packages/core/src/di/provider.ts b/packages/core/src/di/provider.ts index 1b521e911a..447f7c2279 100644 --- a/packages/core/src/di/provider.ts +++ b/packages/core/src/di/provider.ts @@ -151,10 +151,6 @@ export interface StaticClassProvider extends StaticClassSansProvider { * * For more details, see the {@linkDocs guide/dependency-injection "Dependency Injection Guide"}. * - * ### Example - * - * {@example core/di/ts/provider_spec.ts region='ConstructorSansProvider'} - * * @experimental */ export interface ConstructorSansProvider { @@ -453,5 +449,5 @@ export interface ClassProvider extends ClassSansProvider { * * */ -export type Provider = - TypeProvider | ValueProvider | ClassProvider | ExistingProvider | FactoryProvider | any[]; +export type Provider = TypeProvider | ValueProvider | ClassProvider | ConstructorProvider | + ExistingProvider | FactoryProvider | any[]; diff --git a/packages/core/src/di/r3_injector.ts b/packages/core/src/di/r3_injector.ts index 9305d4b796..28742c19a2 100644 --- a/packages/core/src/di/r3_injector.ts +++ b/packages/core/src/di/r3_injector.ts @@ -14,7 +14,7 @@ import {InjectableDef, InjectableType, InjectorDef, InjectorType, InjectorTypeWi import {resolveForwardRef} from './forward_ref'; import {InjectableDefToken, InjectionToken} from './injection_token'; import {INJECTOR, InjectFlags, Injector, NullInjector, THROW_IF_NOT_FOUND, USE_VALUE, inject, injectArgs, setCurrentInjector} from './injector'; -import {ClassProvider, ConstructorProvider, ExistingProvider, FactoryProvider, Provider, StaticClassProvider, TypeProvider, ValueProvider} from './provider'; +import {ClassProvider, ConstructorProvider, ExistingProvider, FactoryProvider, Provider, StaticClassProvider, StaticProvider, TypeProvider, ValueProvider} from './provider'; import {APP_ROOT} from './scope'; @@ -64,14 +64,15 @@ interface Record { } /** - * Create a new `Injector` which is configured using `InjectorType`s. + * Create a new `Injector` which is configured using a `defType` of `InjectorType`s. * * @experimental */ export function createInjector( - defType: /* InjectorType */ any, parent: Injector | null = null): Injector { + defType: /* InjectorType */ any, parent: Injector | null = null, + additionalProviders: StaticProvider[] | null = null): Injector { parent = parent || getNullInjector(); - return new R3Injector(defType, parent); + return new R3Injector(defType, additionalProviders, parent); } export class R3Injector { @@ -101,12 +102,18 @@ export class R3Injector { */ private destroyed = false; - constructor(def: InjectorType, readonly parent: Injector) { + constructor( + def: InjectorType, additionalProviders: StaticProvider[]|null, + readonly parent: Injector) { // Start off by creating Records for every provider declared in every InjectorType // included transitively in `def`. deepForEach( [def], injectorDef => this.processInjectorType(injectorDef, new Set>())); + additionalProviders && + deepForEach(additionalProviders, provider => this.processProvider(provider)); + + // Make sure the INJECTOR token provides this injector. this.records.set(INJECTOR, makeRecord(undefined, this)); @@ -284,20 +291,18 @@ export class R3Injector { throw new Error(`Mixed multi-provider for ${token}.`); } } else { - token = provider; multiRecord = makeRecord(undefined, NOT_YET, true); multiRecord.factory = () => injectArgs(multiRecord !.multi !); this.records.set(token, multiRecord); } token = provider; multiRecord.multi !.push(provider); + } else { + const existing = this.records.get(token); + if (existing && existing.multi !== undefined) { + throw new Error(`Mixed multi-provider for ${stringify(token)}`); + } } - - const existing = this.records.get(token); - if (existing && existing.multi !== undefined) { - throw new Error(`Mixed multi-provider for ${token}`); - } - this.records.set(token, record); } diff --git a/packages/core/src/render3/assert.ts b/packages/core/src/render3/assert.ts index 8c80e47fb6..121217cb58 100644 --- a/packages/core/src/render3/assert.ts +++ b/packages/core/src/render3/assert.ts @@ -46,13 +46,13 @@ export function assertGreaterThan(actual: T, expected: T, msg: string) { } } -export function assertNull(actual: T, msg: string) { +export function assertNotDefined(actual: T, msg: string) { if (actual != null) { throwError(msg); } } -export function assertNotNull(actual: T, msg: string) { +export function assertDefined(actual: T, msg: string) { if (actual == null) { throwError(msg); } diff --git a/packages/core/src/render3/component.ts b/packages/core/src/render3/component.ts index 9bc5fa88ca..cfbc386e2d 100644 --- a/packages/core/src/render3/component.ts +++ b/packages/core/src/render3/component.ts @@ -13,7 +13,7 @@ import {Injector} from '../di/injector'; import {ComponentRef as viewEngine_ComponentRef} from '../linker/component_factory'; import {Sanitizer} from '../sanitization/security'; -import {assertComponentType, assertNotNull} from './assert'; +import {assertComponentType, assertDefined} from './assert'; import {queueInitHooks, queueLifecycleHooks} from './hooks'; import {CLEAN_PROMISE, ROOT_DIRECTIVE_INDICES, _getComponentHostLElementNode, baseDirectiveCreate, createLView, createTView, detectChangesInternal, enterView, executeInitAndContentHooks, getRootView, hostElement, initChangeDetectorIfExisting, leaveView, locateHostElement, setHostBindings} from './instructions'; import {ComponentDef, ComponentType} from './interfaces/definition'; @@ -72,31 +72,6 @@ export interface CreateComponentOptions { } -/** - * Bootstraps a component, then creates and returns a `ComponentRef` for that component. - * - * @param componentType Component to bootstrap - * @param options Optional parameters which control bootstrapping - */ -export function createComponentRef( - componentType: ComponentType, opts: CreateComponentOptions): viewEngine_ComponentRef { - const component = renderComponent(componentType, opts); - const hostView = _getComponentHostLElementNode(component).data as LView; - const hostViewRef = new ViewRef(hostView, component); - return { - location: {nativeElement: getHostElement(component)}, - injector: opts.injector || NULL_INJECTOR, - instance: component, - hostView: hostViewRef, - changeDetectorRef: hostViewRef, - componentType: componentType, - // TODO: implement destroy and onDestroy - destroy: () => {}, - onDestroy: (cb: Function) => {} - }; -} - - // TODO: A hack to not pull in the NullInjector from @angular/core. export const NULL_INJECTOR: Injector = { get: (token: any, notFoundValue?: any) => { @@ -131,12 +106,8 @@ export function renderComponent( // The first index of the first selector is the tag name. const componentTag = componentDef.selectors ![0] ![0] as string; const hostNode = locateHostElement(rendererFactory, opts.host || componentTag); - const rootContext: RootContext = { - // Incomplete initialization due to circular reference. - component: null !, - scheduler: opts.scheduler || requestAnimationFrame.bind(window), - clean: CLEAN_PROMISE, - }; + const rootContext = createRootContext(opts.scheduler || requestAnimationFrame.bind(window)); + const rootView: LView = createLView( rendererFactory.createRenderer(hostNode, componentDef.rendererType), createTView(-1, null, null, null), rootContext, @@ -152,8 +123,8 @@ export function renderComponent( elementNode = hostElement(componentTag, hostNode, componentDef, sanitizer); // Create directive instance with factory() and store at index 0 in directives array - component = rootContext.component = - baseDirectiveCreate(0, componentDef.factory(), componentDef) as T; + rootContext.components.push( + component = baseDirectiveCreate(0, componentDef.factory(), componentDef) as T); initChangeDetectorIfExisting(elementNode.nodeInjector, component, elementNode.data !); opts.hostFeatures && opts.hostFeatures.forEach((feature) => feature(component, componentDef)); @@ -169,6 +140,14 @@ export function renderComponent( return component; } +export function createRootContext(scheduler: (workFn: () => void) => void): RootContext { + return { + components: [], + scheduler: scheduler, + clean: CLEAN_PROMISE, + }; +} + /** * Used to enable lifecycle hooks on the root component. * @@ -198,7 +177,7 @@ export function LifecycleHooksFeature(component: any, def: ComponentDef): v */ function getRootContext(component: any): RootContext { const rootContext = getRootView(component).context as RootContext; - ngDevMode && assertNotNull(rootContext, 'rootContext'); + ngDevMode && assertDefined(rootContext, 'rootContext'); return rootContext; } diff --git a/packages/core/src/render3/component_ref.ts b/packages/core/src/render3/component_ref.ts new file mode 100644 index 0000000000..01222b5a3b --- /dev/null +++ b/packages/core/src/render3/component_ref.ts @@ -0,0 +1,176 @@ +/** + * @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 {ChangeDetectorRef} from '../change_detection/change_detector_ref'; +import {InjectionToken} from '../di/injection_token'; +import {Injector, inject} from '../di/injector'; +import {ComponentFactory as viewEngine_ComponentFactory, ComponentRef as viewEngine_ComponentRef} from '../linker/component_factory'; +import {ComponentFactoryResolver as viewEngine_ComponentFactoryResolver} from '../linker/component_factory_resolver'; +import {ElementRef} from '../linker/element_ref'; +import {NgModuleRef as viewEngine_NgModuleRef} from '../linker/ng_module_factory'; +import {RendererFactory2} from '../render/api'; +import {Type} from '../type'; + +import {assertComponentType, assertDefined} from './assert'; +import {createRootContext} from './component'; +import {baseDirectiveCreate, createLView, createTView, enterView, hostElement, initChangeDetectorIfExisting, leaveView, locateHostElement} from './instructions'; +import {ComponentDef, ComponentType} from './interfaces/definition'; +import {LElementNode} from './interfaces/node'; +import {RElement} from './interfaces/renderer'; +import {LView, LViewFlags, RootContext} from './interfaces/view'; +import {ViewRef} from './view_ref'; + +export class ComponentFactoryResolver extends viewEngine_ComponentFactoryResolver { + resolveComponentFactory(component: Type): viewEngine_ComponentFactory { + ngDevMode && assertComponentType(component); + const componentDef = (component as ComponentType).ngComponentDef; + return new ComponentFactory(componentDef); + } +} + +function toRefArray(map: {[key: string]: string}): {propName: string; templateName: string;}[] { + const array: {propName: string; templateName: string;}[] = []; + for (let nonMinified in map) { + if (map.hasOwnProperty(nonMinified)) { + const minified = map[nonMinified]; + array.push({propName: minified, templateName: nonMinified}); + } + } + return array; +} + +/** + * Default {@link RootContext} for all components rendered with {@link renderComponent}. + */ +export const ROOT_CONTEXT = new InjectionToken( + 'ROOT_CONTEXT_TOKEN', + {providedIn: 'root', factory: () => createRootContext(inject(SCHEDULER))}); + +/** + * A change detection scheduler token for {@link RootContext}. This token is the default value used + * for the default `RootContext` found in the {@link ROOT_CONTEXT} token. + */ +export const SCHEDULER = new InjectionToken<((fn: () => void) => void)>( + 'SCHEDULER_TOKEN', {providedIn: 'root', factory: () => requestAnimationFrame.bind(window)}); + +/** + * Render3 implementation of {@link viewEngine_ComponentFactory}. + */ +export class ComponentFactory extends viewEngine_ComponentFactory { + selector: string; + componentType: Type; + ngContentSelectors: string[]; + get inputs(): {propName: string; templateName: string;}[] { + return toRefArray(this.componentDef.inputs); + } + get outputs(): {propName: string; templateName: string;}[] { + return toRefArray(this.componentDef.outputs); + } + + constructor(private componentDef: ComponentDef) { + super(); + this.componentType = componentDef.type; + this.selector = componentDef.selectors[0][0] as string; + this.ngContentSelectors = []; + } + + create( + parentComponentInjector: Injector, projectableNodes?: any[][]|undefined, + rootSelectorOrNode?: any, + ngModule?: viewEngine_NgModuleRef|undefined): viewEngine_ComponentRef { + ngDevMode && assertDefined(ngModule, 'ngModule should always be defined'); + + const rendererFactory = ngModule ? ngModule.injector.get(RendererFactory2) : document; + const hostNode = locateHostElement(rendererFactory, rootSelectorOrNode); + + // The first index of the first selector is the tag name. + const componentTag = this.componentDef.selectors ![0] ![0] as string; + + const rootContext: RootContext = ngModule !.injector.get(ROOT_CONTEXT); + + // Create the root view. Uses empty TView and ContentTemplate. + const rootView: LView = createLView( + rendererFactory.createRenderer(hostNode, this.componentDef.rendererType), + createTView(-1, null, null, null), null, + this.componentDef.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways); + rootView.injector = ngModule && ngModule.injector || null; + + // rootView is the parent when bootstrapping + const oldView = enterView(rootView, null !); + + let component: T; + let elementNode: LElementNode; + try { + if (rendererFactory.begin) rendererFactory.begin(); + + // Create element node at index 0 in data array + elementNode = hostElement(componentTag, hostNode, this.componentDef); + + // Create directive instance with factory() and store at index 0 in directives array + rootContext.components.push( + component = baseDirectiveCreate(0, this.componentDef.factory(), this.componentDef) as T); + initChangeDetectorIfExisting(elementNode.nodeInjector, component, elementNode.data !); + + } finally { + enterView(oldView, null); + if (rendererFactory.end) rendererFactory.end(); + } + + // TODO(misko): this is the wrong injector here. + return new ComponentRef( + this.componentType, component, rootView, ngModule !.injector, hostNode !); + } +} + +/** + * Represents an instance of a Component created via a {@link ComponentFactory}. + * + * `ComponentRef` provides access to the Component Instance as well other objects related to this + * Component Instance and allows you to destroy the Component Instance via the {@link #destroy} + * method. + * + */ +export class ComponentRef extends viewEngine_ComponentRef { + destroyCbs: (() => void)[]|null = []; + location: ElementRef; + injector: Injector; + instance: T; + hostView: ViewRef; + changeDetectorRef: ChangeDetectorRef; + componentType: Type; + + constructor( + componentType: Type, instance: T, rootView: LView, injector: Injector, + hostNode: RElement) { + super(); + this.instance = instance; + /* TODO(jasonaden): This is incomplete, to be adjusted in follow-up PR. Notes from Kara:When + * ViewRef.detectChanges is called from ApplicationRef.tick, it will call detectChanges at the + * component instance level. I suspect this means that lifecycle hooks and host bindings on the + * given component won't work (as these are always called at the level above a component). + * + * In render2, ViewRef.detectChanges uses the root view instance for view checks, not the + * component instance. So passing in the root view (1 level above the component) is sufficient. + * We might want to think about creating a fake component for the top level? Or overwrite + * detectChanges with a function that calls tickRootContext? */ + this.hostView = this.changeDetectorRef = new ViewRef(rootView, instance); + this.injector = injector; + this.location = new ElementRef(hostNode); + this.componentType = componentType; + } + + destroy(): void { + ngDevMode && assertDefined(this.destroyCbs, 'NgModule already destroyed'); + this.destroyCbs !.forEach(fn => fn()); + this.destroyCbs = null; + } + onDestroy(callback: () => void): void { + ngDevMode && assertDefined(this.destroyCbs, 'NgModule already destroyed'); + this.destroyCbs !.push(callback); + } +} \ No newline at end of file diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts index 1c55e79fa6..4129107841 100644 --- a/packages/core/src/render3/di.ts +++ b/packages/core/src/render3/di.ts @@ -18,9 +18,9 @@ import {ViewContainerRef as viewEngine_ViewContainerRef} from '../linker/view_co import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, ViewRef as viewEngine_ViewRef} from '../linker/view_ref'; import {Type} from '../type'; -import {assertGreaterThan, assertLessThan, assertNotNull} from './assert'; +import {assertDefined, assertGreaterThan, assertLessThan} from './assert'; import {addToViewTree, assertPreviousIsParent, createLContainer, createLNodeObject, createTNode, createTView, getDirectiveInstance, getPreviousOrParentNode, getRenderer, isComponent, renderEmbeddedTemplate, resolveDirective} from './instructions'; -import {ComponentTemplate, DirectiveDef} from './interfaces/definition'; +import {ComponentTemplate, DirectiveDef, DirectiveDefList, PipeDefList} from './interfaces/definition'; import {LInjector} from './interfaces/injector'; import {AttributeMarker, LContainerNode, LElementNode, LNode, LViewNode, TNodeFlags, TNodeType} from './interfaces/node'; import {LQueries, QueryReadType} from './interfaces/query'; @@ -256,7 +256,7 @@ export function injectAttribute(attrNameToInject: string): string|undefined { const lElement = getPreviousOrParentNode() as LElementNode; ngDevMode && assertNodeType(lElement, TNodeType.Element); const tElement = lElement.tNode; - ngDevMode && assertNotNull(tElement, 'expecting tNode'); + ngDevMode && assertDefined(tElement, 'expecting tNode'); const attrs = tElement.attrs; if (attrs) { for (let i = 0; i < attrs.length; i = i + 2) { @@ -707,7 +707,7 @@ export function getOrCreateTemplateRef(di: LInjector): viewEngine_TemplateRef hostTNode.tViews = createTView( -1, hostNode.data.template !, hostTView.directiveRegistry, hostTView.pipeRegistry); } - ngDevMode && assertNotNull(hostTNode.tViews, 'TView must be allocated'); + ngDevMode && assertDefined(hostTNode.tViews, 'TView must be allocated'); di.templateRef = new TemplateRef( getOrCreateElementRef(di), hostTNode.tViews as TView, getRenderer(), hostNode.data.queries); } diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index 0e8e091bc3..9d3c28b344 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -6,16 +6,16 @@ * found in the LICENSE file at https://angular.io/license */ -import {LifecycleHooksFeature, createComponentRef, getHostElement, getRenderedText, renderComponent, whenRendered} from './component'; +import {LifecycleHooksFeature, getHostElement, getRenderedText, renderComponent, whenRendered} from './component'; import {NgOnChangesFeature, PublicFeature, defineComponent, defineDirective, definePipe} from './definition'; import {ComponentDef, ComponentTemplate, ComponentType, DirectiveDef, DirectiveDefFlags, DirectiveType, PipeDef} from './interfaces/definition'; +export {ComponentFactory, ComponentFactoryResolver, ComponentRef} from './component_ref'; export {QUERY_READ_CONTAINER_REF, QUERY_READ_ELEMENT_REF, QUERY_READ_FROM_NODE, QUERY_READ_TEMPLATE_REF, directiveInject, injectAttribute, injectChangeDetectorRef, injectElementRef, injectTemplateRef, injectViewContainerRef} from './di'; export {RenderFlags} from './interfaces/definition'; export {CssSelectorList} from './interfaces/projection'; - // Naming scheme: // - Capital letters are for creating things: T(Text), E(Element), D(Directive), V(View), // C(Container), L(Listener) @@ -73,6 +73,8 @@ export { tick, } from './instructions'; +export {NgModuleDef, NgModuleFactory, NgModuleRef, NgModuleType} from './ng_module_ref'; + export { AttributeMarker } from './interfaces/node'; @@ -122,7 +124,6 @@ export { defineComponent, defineDirective, definePipe, - createComponentRef, getHostElement, getRenderedText, renderComponent, diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index a004f10d81..3b2dcf65bf 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -8,7 +8,11 @@ import './ng_dev_mode'; -import {assertEqual, assertLessThan, assertNotEqual, assertNotNull, assertNull, assertSame} from './assert'; +import {Sanitizer} from '../sanitization/security'; + +import {assertDefined, assertEqual, assertLessThan, assertNotDefined, assertNotEqual, assertSame} from './assert'; +import {throwCyclicDependencyError, throwErrorIfNoChangesMode, throwMultipleComponentError} from './errors'; +import {executeHooks, executeInitHooks, queueInitHooks, queueLifecycleHooks} from './hooks'; import {LContainer} from './interfaces/container'; import {LInjector} from './interfaces/injector'; import {CssSelectorList, LProjection, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection'; @@ -22,10 +26,7 @@ import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefList, DirectiveDefListOrFactory, PipeDefList, PipeDefListOrFactory, RenderFlags} from './interfaces/definition'; import {RElement, RText, Renderer3, RendererFactory3, ProceduralRenderer3, RendererStyleFlags3, isProceduralRenderer} from './interfaces/renderer'; import {isDifferent, stringify} from './util'; -import {executeHooks, queueLifecycleHooks, queueInitHooks, executeInitHooks} from './hooks'; import {ViewRef} from './view_ref'; -import {throwCyclicDependencyError, throwErrorIfNoChangesMode, throwMultipleComponentError} from './errors'; -import {Sanitizer} from '../sanitization/security'; /** * Directive (D) sets a property on all component instances using this constant as a key and the @@ -406,7 +407,8 @@ export function createLNode( if ((type & TNodeType.ViewOrElement) === TNodeType.ViewOrElement && isState) { // Bit of a hack to bust through the readonly because there is a circular dep between // LView and LNode. - ngDevMode && assertNull((state as LView).node, 'LView.node should not have been initialized'); + ngDevMode && + assertNotDefined((state as LView).node, 'LView.node should not have been initialized'); (state as{node: LNode}).node = node; if (firstTemplatePass) (state as LView).tView.node = node.tNode; } @@ -455,7 +457,7 @@ export function renderTemplate( sanitizer)); } const hostView = host.data !; - ngDevMode && assertNotNull(hostView, 'Host node should have an LView defined in host.data.'); + ngDevMode && assertDefined(hostView, 'Host node should have an LView defined in host.data.'); renderComponentOrTemplate(host, hostView, context, template); return host; } @@ -1300,8 +1302,8 @@ export function textBinding(index: number, value: T | NO_CHANGE): void { if (value !== NO_CHANGE) { ngDevMode && assertDataInRange(index); const existingNode = data[index] as LTextNode; - ngDevMode && assertNotNull(existingNode, 'LNode should exist'); - ngDevMode && assertNotNull(existingNode.native, 'native element should exist'); + ngDevMode && assertDefined(existingNode, 'LNode should exist'); + ngDevMode && assertDefined(existingNode.native, 'native element should exist'); ngDevMode && ngDevMode.rendererSetText++; isProceduralRenderer(renderer) ? renderer.setValue(existingNode.native, stringify(value)) : existingNode.native.textContent = stringify(value); @@ -1325,7 +1327,7 @@ export function directiveCreate( index: number, directive: T, directiveDef: DirectiveDef| ComponentDef): T { const instance = baseDirectiveCreate(index, directive, directiveDef); - ngDevMode && assertNotNull(previousOrParentNode.tNode, 'previousOrParentNode.tNode'); + ngDevMode && assertDefined(previousOrParentNode.tNode, 'previousOrParentNode.tNode'); const tNode = previousOrParentNode.tNode; const isComponent = (directiveDef as ComponentDef).template; @@ -1494,7 +1496,7 @@ function generateInitialInputs( export function createLContainer( parentLNode: LNode, currentView: LView, template?: ComponentTemplate, isForViewContainerRef?: boolean): LContainer { - ngDevMode && assertNotNull(parentLNode, 'containers should have a parent'); + ngDevMode && assertDefined(parentLNode, 'containers should have a parent'); return { views: [], nextIndex: isForViewContainerRef ? null : 0, @@ -1611,7 +1613,7 @@ function refreshDynamicChildren() { const lViewNode = container.views[i]; // The directives and pipes are not needed here as an existing view is only being refreshed. const dynamicView = lViewNode.data; - ngDevMode && assertNotNull(dynamicView.tView, 'TView must be allocated'); + ngDevMode && assertDefined(dynamicView.tView, 'TView must be allocated'); renderEmbeddedTemplate(lViewNode, dynamicView.tView, dynamicView.context !, renderer); } } @@ -1699,7 +1701,7 @@ export function embeddedViewStart(viewBlockId: number): RenderFlags { function getOrCreateEmbeddedTView(viewIndex: number, parent: LContainerNode): TView { ngDevMode && assertNodeType(parent, TNodeType.Container); const containerTViews = (parent !.tNode as TContainerNode).tViews as TView[]; - ngDevMode && assertNotNull(containerTViews, 'TView expected'); + ngDevMode && assertDefined(containerTViews, 'TView expected'); ngDevMode && assertEqual(Array.isArray(containerTViews), true, 'TViews should be in an array'); if (viewIndex >= containerTViews.length || containerTViews[viewIndex] == null) { const tView = currentView.tView; @@ -1773,7 +1775,7 @@ export function componentRefresh(directiveIndex: number, elementIndex: number ngDevMode && assertDataInRange(elementIndex); const element = data ![elementIndex] as LElementNode; ngDevMode && assertNodeType(element, TNodeType.Element); - ngDevMode && assertNotNull(element.data, `Component's host node should have an LView attached.`); + ngDevMode && assertDefined(element.data, `Component's host node should have an LView attached.`); const hostView = element.data !; // Only attached CheckAlways components or attached, dirty OnPush components should be checked @@ -1928,13 +1930,13 @@ export function projection( function findComponentHost(lView: LView): LElementNode { let viewRootLNode = lView.node; while (viewRootLNode.tNode.type === TNodeType.View) { - ngDevMode && assertNotNull(lView.parent, 'lView.parent'); + ngDevMode && assertDefined(lView.parent, 'lView.parent'); lView = lView.parent !; viewRootLNode = lView.node; } ngDevMode && assertNodeType(viewRootLNode, TNodeType.Element); - ngDevMode && assertNotNull(viewRootLNode.data, 'node.data'); + ngDevMode && assertDefined(viewRootLNode.data, 'node.data'); return viewRootLNode as LElementNode; } @@ -2012,7 +2014,7 @@ export function markViewDirty(view: LView): void { } currentView.flags |= LViewFlags.Dirty; - ngDevMode && assertNotNull(currentView !.context, 'rootContext'); + ngDevMode && assertDefined(currentView !.context, 'rootContext'); scheduleTick(currentView !.context as RootContext); } @@ -2033,7 +2035,7 @@ export function scheduleTick(rootContext: RootContext) { let res: null|((val: null) => void); rootContext.clean = new Promise((r) => res = r); rootContext.scheduler(() => { - tick(rootContext.component); + tickRootContext(rootContext); res !(null); rootContext.clean = _CLEAN_PROMISE; }); @@ -2054,11 +2056,18 @@ export function scheduleTick(rootContext: RootContext) { */ export function tick(component: T): void { const rootView = getRootView(component); - const rootComponent = (rootView.context as RootContext).component; - const hostNode = _getComponentHostLElementNode(rootComponent); + const rootContext = rootView.context as RootContext; + tickRootContext(rootContext); +} - ngDevMode && assertNotNull(hostNode.data, 'Component host node should be attached to an LView'); - renderComponentOrTemplate(hostNode, rootView, rootComponent); +function tickRootContext(rootContext: RootContext) { + for (let i = 0; i < rootContext.components.length; i++) { + const rootComponent = rootContext.components[i]; + const hostNode = _getComponentHostLElementNode(rootComponent); + + ngDevMode && assertDefined(hostNode.data, 'Component host node should be attached to an LView'); + renderComponentOrTemplate(hostNode, getRootView(rootComponent), rootComponent); + } } /** @@ -2069,7 +2078,7 @@ export function tick(component: T): void { */ export function getRootView(component: any): LView { - ngDevMode && assertNotNull(component, 'component'); + ngDevMode && assertDefined(component, 'component'); const lElementNode = _getComponentHostLElementNode(component); let lView = lElementNode.view; while (lView.parent) { @@ -2093,7 +2102,7 @@ export function getRootView(component: any): LView { */ export function detectChanges(component: T): void { const hostNode = _getComponentHostLElementNode(component); - ngDevMode && assertNotNull(hostNode.data, 'Component host node should be attached to an LView'); + ngDevMode && assertDefined(hostNode.data, 'Component host node should be attached to an LView'); detectChangesInternal(hostNode.data as LView, hostNode, component); } @@ -2142,7 +2151,7 @@ export function detectChangesInternal(hostView: LView, hostNode: LElementNode * @param component Component to mark as dirty. */ export function markDirty(component: T) { - ngDevMode && assertNotNull(component, 'component'); + ngDevMode && assertDefined(component, 'component'); const lElementNode = _getComponentHostLElementNode(component); markViewDirty(lElementNode.view); } @@ -2388,7 +2397,7 @@ export function load(index: number): T { /** Retrieves a value from the `directives` array. */ export function loadDirective(index: number): T { - ngDevMode && assertNotNull(directives, 'Directives array should be defined if reading a dir.'); + ngDevMode && assertDefined(directives, 'Directives array should be defined if reading a dir.'); ngDevMode && assertDataInRange(index, directives !); return directives ![index]; } @@ -2453,7 +2462,7 @@ export function assertPreviousIsParent() { } function assertHasParent() { - assertNotNull(getParentLNode(previousOrParentNode), 'previousOrParentNode should have a parent'); + assertDefined(getParentLNode(previousOrParentNode), 'previousOrParentNode should have a parent'); } function assertDataInRange(index: number, arr?: any[]) { @@ -2484,9 +2493,9 @@ export function assertReservedSlotInitialized(slotOffset: number, numSlots: numb } export function _getComponentHostLElementNode(component: T): LElementNode { - ngDevMode && assertNotNull(component, 'expecting component got null'); + ngDevMode && assertDefined(component, 'expecting component got null'); const lElementNode = (component as any)[NG_HOST_SYMBOL] as LElementNode; - ngDevMode && assertNotNull(component, 'object is not a component'); + ngDevMode && assertDefined(component, 'object is not a component'); return lElementNode; } diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts index c069e6f0ed..204c1e0960 100644 --- a/packages/core/src/render3/interfaces/view.ts +++ b/packages/core/src/render3/interfaces/view.ts @@ -420,10 +420,10 @@ export interface RootContext { clean: Promise; /** - * RootComponent - The component which was instantiated by the call to + * RootComponents - The components that were instantiated by the call to * {@link renderComponent}. */ - component: {}; + components: {}[]; } /** diff --git a/packages/core/src/render3/ng_module_ref.ts b/packages/core/src/render3/ng_module_ref.ts new file mode 100644 index 0000000000..1844b3b57c --- /dev/null +++ b/packages/core/src/render3/ng_module_ref.ts @@ -0,0 +1,73 @@ +/** + * @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 {Injector} from '../di/injector'; +import {StaticProvider} from '../di/provider'; +import {createInjector} from '../di/r3_injector'; +import {ComponentFactoryResolver as viewEngine_ComponentFactoryResolver} from '../linker/component_factory_resolver'; +import {InternalNgModuleRef, NgModuleFactory as viewEngine_NgModuleFactory, NgModuleRef as viewEngine_NgModuleRef} from '../linker/ng_module_factory'; +import {Type} from '../type'; +import {stringify} from '../util'; +import {assertDefined} from './assert'; +import {ComponentFactoryResolver} from './component_ref'; + +export interface NgModuleType { ngModuleDef: NgModuleDef; } + +export interface NgModuleDef { bootstrap: Type[]; } + +export const COMPONENT_FACTORY_RESOLVER: StaticProvider = { + provide: viewEngine_ComponentFactoryResolver, + useFactory: () => new ComponentFactoryResolver(), + deps: [], +}; + +export class NgModuleRef extends viewEngine_NgModuleRef implements InternalNgModuleRef { + // tslint:disable-next-line:require-internal-with-underscore + _bootstrapComponents: Type[] = []; + injector: Injector; + componentFactoryResolver: viewEngine_ComponentFactoryResolver; + instance: T; + destroyCbs: (() => void)[]|null = []; + + constructor(ngModuleType: Type, parentInjector: Injector|null) { + super(); + const ngModuleDef = (ngModuleType as any as NgModuleType).ngModuleDef; + ngDevMode && assertDefined( + ngModuleDef, + `NgModule '${stringify(ngModuleType)}' is not a subtype of 'NgModuleType'.`); + + this._bootstrapComponents = ngModuleDef.bootstrap; + const additionalProviders: StaticProvider[] = [ + COMPONENT_FACTORY_RESOLVER, { + provide: viewEngine_NgModuleRef, + useValue: this, + } + ]; + this.injector = createInjector(ngModuleType, parentInjector, additionalProviders); + this.instance = this.injector.get(ngModuleType); + this.componentFactoryResolver = new ComponentFactoryResolver(); + } + + destroy(): void { + ngDevMode && assertDefined(this.destroyCbs, 'NgModule already destroyed'); + this.destroyCbs !.forEach(fn => fn()); + this.destroyCbs = null; + } + onDestroy(callback: () => void): void { + ngDevMode && assertDefined(this.destroyCbs, 'NgModule already destroyed'); + this.destroyCbs !.push(callback); + } +} + +export class NgModuleFactory extends viewEngine_NgModuleFactory { + constructor(public moduleType: Type) { super(); } + + create(parentInjector: Injector|null): viewEngine_NgModuleRef { + return new NgModuleRef(this.moduleType, parentInjector); + } +} diff --git a/packages/core/src/render3/node_assert.ts b/packages/core/src/render3/node_assert.ts index 73bf9b6997..c30a0f2455 100644 --- a/packages/core/src/render3/node_assert.ts +++ b/packages/core/src/render3/node_assert.ts @@ -6,16 +6,16 @@ * found in the LICENSE file at https://angular.io/license */ -import {assertEqual, assertNotNull} from './assert'; +import {assertDefined, assertEqual} from './assert'; import {LNode, TNodeType} from './interfaces/node'; export function assertNodeType(node: LNode, type: TNodeType) { - assertNotNull(node, 'should be called with a node'); + assertDefined(node, 'should be called with a node'); assertEqual(node.tNode.type, type, `should be a ${typeName(type)}`); } export function assertNodeOfPossibleTypes(node: LNode, ...types: TNodeType[]) { - assertNotNull(node, 'should be called with a node'); + assertDefined(node, 'should be called with a node'); const found = types.some(type => node.tNode.type === type); assertEqual(found, true, `Should be one of ${types.map(typeName).join(', ')}`); } diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index db4eb95d50..716565fc71 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {assertNotNull} from './assert'; +import {assertDefined} from './assert'; import {callHooks} from './hooks'; import {LContainer, unusedValueExportToPlacateAjd as unused1} from './interfaces/container'; import {LContainerNode, LElementNode, LNode, LProjectionNode, LTextNode, LViewNode, TNodeFlags, TNodeType, unusedValueExportToPlacateAjd as unused2} from './interfaces/node'; diff --git a/packages/core/src/render3/node_selector_matcher.ts b/packages/core/src/render3/node_selector_matcher.ts index 73f2ab55ec..db94094e24 100644 --- a/packages/core/src/render3/node_selector_matcher.ts +++ b/packages/core/src/render3/node_selector_matcher.ts @@ -8,7 +8,7 @@ import './ng_dev_mode'; -import {assertNotNull} from './assert'; +import {assertDefined} from './assert'; import {AttributeMarker, TAttributes, TNode, unusedValueExportToPlacateAjd as unused1} from './interfaces/node'; import {CssSelector, CssSelectorList, NG_PROJECT_AS_ATTR_NAME, SelectorFlags, unusedValueExportToPlacateAjd as unused2} from './interfaces/projection'; @@ -36,7 +36,7 @@ function isCssClassMatching(nodeClassAttrVal: string, cssClassToMatch: string): * @returns true if node matches the selector. */ export function isNodeMatchingSelector(tNode: TNode, selector: CssSelector): boolean { - ngDevMode && assertNotNull(selector[0], 'Selector should have a tag name'); + ngDevMode && assertDefined(selector[0], 'Selector should have a tag name'); let mode: SelectorFlags = SelectorFlags.ELEMENT; const nodeAttrs = tNode.attrs !; diff --git a/packages/core/src/render3/query.ts b/packages/core/src/render3/query.ts index fd6a667a01..c04f3f48c8 100644 --- a/packages/core/src/render3/query.ts +++ b/packages/core/src/render3/query.ts @@ -15,7 +15,7 @@ import {QueryList as viewEngine_QueryList} from '../linker/query_list'; import {Type} from '../type'; import {getSymbolIterator} from '../util'; -import {assertEqual, assertNotNull} from './assert'; +import {assertDefined, assertEqual} from './assert'; import {ReadFromInjectorFn, getOrCreateNodeInjectorForNode} from './di'; import {assertPreviousIsParent, getCurrentQueries, store, storeCleanupWithContext} from './instructions'; import {DirectiveDef, unusedValueExportToPlacateAjd as unused1} from './interfaces/definition'; @@ -163,7 +163,7 @@ export class LQueries_ implements LQueries { let query = this.deep; while (query) { ngDevMode && - assertNotNull( + assertDefined( query.containerValues, 'View queries need to have a pointer to container values.'); query.containerValues !.splice(index, 0, query.values); query = query.next; @@ -179,7 +179,7 @@ export class LQueries_ implements LQueries { let query = this.deep; while (query) { ngDevMode && - assertNotNull( + assertDefined( query.containerValues, 'View queries need to have a pointer to container values.'); const removed = query.containerValues !.splice(index, 1); @@ -273,7 +273,7 @@ function add(query: LQuery| null, node: LNode) { if (directiveIdx !== null) { // a node is matching a predicate - determine what to read // note that queries using name selector must specify read strategy - ngDevMode && assertNotNull(predicate.read, 'the node should have a predicate'); + ngDevMode && assertDefined(predicate.read, 'the node should have a predicate'); const result = readFromNodeInjector(nodeInjector, node, predicate.read !, directiveIdx); if (result !== null) { addMatch(query, result); diff --git a/packages/core/src/render3/view_ref.ts b/packages/core/src/render3/view_ref.ts index 060f2c997b..17a7beb5a8 100644 --- a/packages/core/src/render3/view_ref.ts +++ b/packages/core/src/render3/view_ref.ts @@ -6,8 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ +import {ApplicationRef} from '../application_ref'; +import {ChangeDetectorRef as viewEngine_ChangeDetectorRef} from '../change_detection/change_detector_ref'; import {ViewContainerRef as viewEngine_ViewContainerRef} from '../linker/view_container_ref'; -import {EmbeddedViewRef as viewEngine_EmbeddedViewRef} from '../linker/view_ref'; +import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, InternalViewRef as viewEngine_InternalViewRef} from '../linker/view_ref'; import {checkNoChanges, detectChanges, markViewDirty, storeCleanupFn} from './instructions'; import {ComponentTemplate} from './interfaces/definition'; @@ -15,7 +17,15 @@ import {LViewNode} from './interfaces/node'; import {LView, LViewFlags} from './interfaces/view'; import {destroyLView} from './node_manipulation'; -export class ViewRef implements viewEngine_EmbeddedViewRef { +// Needed due to tsickle downleveling where multiple `implements` with classes creates +// multiple @extends in Closure annotations, which is illegal. This workaround fixes +// the multiple @extends by making the annotation @implements instead +export interface viewEngine_ChangeDetectorRef_interface extends viewEngine_ChangeDetectorRef {} + +export class ViewRef implements viewEngine_EmbeddedViewRef, viewEngine_InternalViewRef, + viewEngine_ChangeDetectorRef_interface { + private _appRef: ApplicationRef|null; + context: T; rootNodes: any[]; @@ -210,6 +220,10 @@ export class ViewRef implements viewEngine_EmbeddedViewRef { * introduce other changes. */ checkNoChanges(): void { checkNoChanges(this.context); } + + detachFromAppRef() { this._appRef = null; } + + attachToAppRef(appRef: ApplicationRef) { this._appRef = appRef; } } diff --git a/packages/core/test/application_ref_integration_spec.ts b/packages/core/test/application_ref_integration_spec.ts new file mode 100644 index 0000000000..9dee7acc17 --- /dev/null +++ b/packages/core/test/application_ref_integration_spec.ts @@ -0,0 +1,95 @@ +/** + * @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 {ApplicationModule, ApplicationRef, DoCheck, InjectFlags, InjectorType, Input, OnInit, PlatformRef, TestabilityRegistry, Type, defineInjector, inject, ɵE as elementStart, ɵNgModuleDef as NgModuleDef, ɵRenderFlags as RenderFlags, ɵT as text, ɵdefineComponent as defineComponent, ɵe as elementEnd, ɵi1 as interpolation1, ɵt as textBinding} from '@angular/core'; +import {getTestBed, withBody} from '@angular/core/testing'; +import {BrowserModule, EVENT_MANAGER_PLUGINS, platformBrowser} from '@angular/platform-browser'; + +import {BROWSER_MODULE_PROVIDERS} from '../../platform-browser/src/browser'; +import {APPLICATION_MODULE_PROVIDERS} from '../src/application_module'; +import {NgModuleFactory} from '../src/render3/ng_module_ref'; + +describe('ApplicationRef bootstrap', () => { + class HelloWorldComponent implements OnInit, DoCheck { + log: string[] = []; + name = 'World'; + static ngComponentDef = defineComponent({ + type: HelloWorldComponent, + selectors: [['hello-world']], + factory: () => new HelloWorldComponent(), + template: function(rf: RenderFlags, ctx: HelloWorldComponent): void { + if (rf & RenderFlags.Create) { + elementStart(0, 'div'); + text(1); + elementEnd(); + } + if (rf & RenderFlags.Update) { + textBinding(1, interpolation1('Hello ', ctx.name, '')); + } + } + }); + + ngOnInit(): void { this.log.push('OnInit'); } + + ngDoCheck(): void { this.log.push('DoCheck'); } + } + + class MyAppModule { + static ngInjectorDef = + defineInjector({factory: () => new MyAppModule(), imports: [BrowserModule]}); + static ngModuleDef = defineNgModule({bootstrap: [HelloWorldComponent]}); + } + + it('should bootstrap hello world', withBody('', async() => { + const MyAppModuleFactory = new NgModuleFactory(MyAppModule); + const moduleRef = + await getTestBed().platform.bootstrapModuleFactory(MyAppModuleFactory, {ngZone: 'noop'}); + const appRef = moduleRef.injector.get(ApplicationRef); + const helloWorldComponent = appRef.components[0].instance as HelloWorldComponent; + expect(document.body.innerHTML).toEqual('
Hello World
'); + // TODO(jasonaden): Get with Kara on lifecycle hooks + // expect(helloWorldComponent.log).toEqual(['OnInit', 'DoCheck']); + helloWorldComponent.name = 'Mundo'; + appRef.tick(); + expect(document.body.innerHTML).toEqual('
Hello Mundo
'); + // TODO(jasonaden): Get with Kara on lifecycle hooks + // expect(helloWorldComponent.log).toEqual(['OnInit', 'DoCheck', 'DoCheck']); + + // Cleanup TestabilityRegistry + const registry: TestabilityRegistry = getTestBed().get(TestabilityRegistry); + registry.unregisterAllApplications(); + })); + +}); + +///////////////////////////////////////////////////////// + +// These go away when Compiler is ready + +(BrowserModule as any as InjectorType).ngInjectorDef = defineInjector({ + factory: function BrowserModule_Factory() { + return new BrowserModule(inject(BrowserModule, InjectFlags.Optional | InjectFlags.SkipSelf)); + }, + imports: [ApplicationModule], + providers: BROWSER_MODULE_PROVIDERS +}); + +(ApplicationModule as any as InjectorType).ngInjectorDef = defineInjector({ + factory: function ApplicationModule_Factory() { + return new ApplicationModule(inject(ApplicationRef)); + }, + providers: APPLICATION_MODULE_PROVIDERS +}); + +export function defineNgModule({bootstrap}: {bootstrap?: Type[]}): NgModuleDef { + return { + bootstrap: bootstrap || [], + }; +} + +///////////////////////////////////////////////////////// diff --git a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json index df06f6a31f..c1c0de6f38 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -56,6 +56,9 @@ { "name": "createLView" }, + { + "name": "createRootContext" + }, { "name": "createTNode" }, diff --git a/packages/core/test/bundling/hello_world_r2/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world_r2/bundle.golden_symbols.json index 56d1450518..c359140b00 100644 --- a/packages/core/test/bundling/hello_world_r2/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world_r2/bundle.golden_symbols.json @@ -2418,7 +2418,7 @@ "name": "assertInterpolationSymbols" }, { - "name": "assertNotNull$1" + "name": "assertNotNull" }, { "name": "assertPlatform" diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 64f8a6e020..865432ac91 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -266,6 +266,9 @@ { "name": "createOutput" }, + { + "name": "createRootContext" + }, { "name": "createTNode" }, @@ -657,7 +660,7 @@ "name": "throwMultipleComponentError" }, { - "name": "tick" + "name": "tickRootContext" }, { "name": "trackByIdentity" diff --git a/packages/core/test/di/r3_injector_spec.ts b/packages/core/test/di/r3_injector_spec.ts index 7d08f65de5..5e690b2f86 100644 --- a/packages/core/test/di/r3_injector_spec.ts +++ b/packages/core/test/di/r3_injector_spec.ts @@ -41,6 +41,8 @@ describe('InjectorDef-based createInjector()', () => { const STATIC_TOKEN = new InjectionToken('STATIC_TOKEN'); + const LOCALE = new InjectionToken('LOCALE'); + class ServiceWithDep { constructor(readonly service: Service) {} @@ -50,6 +52,15 @@ describe('InjectorDef-based createInjector()', () => { }); } + class ServiceWithMultiDep { + constructor(readonly locale: string[]) {} + + static ngInjectableDef = defineInjectable({ + providedIn: null, + factory: () => new ServiceWithMultiDep(inject(LOCALE)), + }); + } + class ServiceTwo { static ngInjectableDef = defineInjectable({ providedIn: null, @@ -112,6 +123,9 @@ describe('InjectorDef-based createInjector()', () => { imports: [IntermediateModule], providers: [ ServiceWithDep, + ServiceWithMultiDep, + {provide: LOCALE, multi: true, useValue: 'en'}, + {provide: LOCALE, multi: true, useValue: 'es'}, Service, {provide: SERVICE_TOKEN, useExisting: Service}, CircularA, @@ -168,6 +182,12 @@ describe('InjectorDef-based createInjector()', () => { expect(instance.service).toBe(injector.get(Service)); }); + it('injects a service with dependencies on multi-providers', () => { + const instance = injector.get(ServiceWithMultiDep); + expect(instance instanceof ServiceWithMultiDep); + expect(instance.locale).toEqual(['en', 'es']); + }); + it('injects a token with useExisting', () => { const instance = injector.get(SERVICE_TOKEN); expect(instance).toBe(injector.get(Service)); diff --git a/packages/core/test/render3/component_spec.ts b/packages/core/test/render3/component_spec.ts index 7b7026e9a2..148f447ded 100644 --- a/packages/core/test/render3/component_spec.ts +++ b/packages/core/test/render3/component_spec.ts @@ -7,9 +7,9 @@ */ -import {ComponentFactory, DoCheck, ViewEncapsulation, createInjector, defineInjectable, defineInjector} from '../../src/core'; +import {DoCheck, ViewEncapsulation, createInjector, defineInjectable, defineInjector} from '../../src/core'; import {getRenderedText} from '../../src/render3/component'; -import {LifecycleHooksFeature, defineComponent, directiveInject, markDirty} from '../../src/render3/index'; +import {ComponentFactory, LifecycleHooksFeature, defineComponent, directiveInject, markDirty} from '../../src/render3/index'; import {bind, container, containerRefreshEnd, containerRefreshStart, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, text, textBinding, tick} from '../../src/render3/instructions'; import {ComponentDef, DirectiveDef, RenderFlags} from '../../src/render3/interfaces/definition'; import {createRendererType2} from '../../src/view/index'; @@ -364,4 +364,27 @@ describe('recursive components', () => { tick(comp); expect(events).toEqual(['check6', 'check2', 'check0', 'check1', 'check5', 'check3', 'check4']); }); + + it('should map inputs minified & unminified names', async() => { + class TestInputsComponent { + minifiedName: string; + static ngComponentDef = defineComponent({ + type: TestInputsComponent, + selectors: [['test-inputs']], + inputs: {minifiedName: 'unminifiedName'}, + factory: () => new TestInputsComponent(), + template: function(rf: RenderFlags, ctx: TestInputsComponent): void { + // Template not needed for this test + } + }); + } + + const testInputsComponentFactory = new ComponentFactory(TestInputsComponent.ngComponentDef); + + expect([ + {propName: 'minifiedName', templateName: 'unminifiedName'} + ]).toEqual(testInputsComponentFactory.inputs); + + }); + }); diff --git a/packages/core/test/render3/render_util.ts b/packages/core/test/render3/render_util.ts index cb4bf297ad..0a68b1ead8 100644 --- a/packages/core/test/render3/render_util.ts +++ b/packages/core/test/render3/render_util.ts @@ -135,7 +135,7 @@ export class ComponentFixture extends BaseFixture { // Fixtures above are preferred way of testing Components and Templates /////////////////////////////////////////////////////////////////////////////////// -export const document = ((global || window) as any).document; +export const document = ((typeof global == 'object' && global || window) as any).document; export let containerEl: HTMLElement = null !; let host: LElementNode|null; const isRenderer2 = diff --git a/packages/platform-browser/src/browser.ts b/packages/platform-browser/src/browser.ts index dc13d2bdbb..9550029af9 100644 --- a/packages/platform-browser/src/browser.ts +++ b/packages/platform-browser/src/browser.ts @@ -7,7 +7,7 @@ */ import {CommonModule, PlatformLocation, ɵPLATFORM_BROWSER_ID as PLATFORM_BROWSER_ID} from '@angular/common'; -import {APP_ID, ApplicationModule, ErrorHandler, ModuleWithProviders, NgModule, Optional, PLATFORM_ID, PLATFORM_INITIALIZER, PlatformRef, RendererFactory2, RootRenderer, Sanitizer, SkipSelf, StaticProvider, Testability, createPlatformFactory, platformCore, ɵAPP_ROOT as APP_ROOT} from '@angular/core'; +import {APP_ID, ApplicationModule, ClassProvider, ConstructorSansProvider, ErrorHandler, ExistingProvider, FactoryProvider, Inject, InjectionToken, ModuleWithProviders, NgModule, NgZone, Optional, PLATFORM_ID, PLATFORM_INITIALIZER, PlatformRef, RendererFactory2, RootRenderer, Sanitizer, SkipSelf, StaticProvider, Testability, TypeProvider, ValueProvider, createPlatformFactory, platformCore, ɵAPP_ROOT as APP_ROOT} from '@angular/core'; import {BrowserDomAdapter} from './browser/browser_adapter'; import {BrowserPlatformLocation} from './browser/location/browser_platform_location'; @@ -20,7 +20,7 @@ import {getDOM} from './dom/dom_adapter'; import {DomRendererFactory2} from './dom/dom_renderer'; import {DOCUMENT} from './dom/dom_tokens'; import {DomEventsPlugin} from './dom/events/dom_events'; -import {EVENT_MANAGER_PLUGINS, EventManager} from './dom/events/event_manager'; +import {EVENT_MANAGER_PLUGINS, EventManager, EventManagerPlugin} from './dom/events/event_manager'; import {HAMMER_GESTURE_CONFIG, HammerGestureConfig, HammerGesturesPlugin} from './dom/events/hammer_gestures'; import {KeyEventsPlugin} from './dom/events/key_events'; import {DomSharedStylesHost, SharedStylesHost} from './dom/shared_styles_host'; @@ -60,34 +60,47 @@ export function _document(): any { return document; } +export const BROWSER_MODULE_PROVIDERS: StaticProvider[] = [ + BROWSER_SANITIZATION_PROVIDERS, + {provide: APP_ROOT, useValue: true}, + {provide: ErrorHandler, useFactory: errorHandler, deps: []}, + { + provide: EVENT_MANAGER_PLUGINS, + useClass: DomEventsPlugin, + multi: true, + deps: [DOCUMENT, NgZone, PLATFORM_ID] + }, + {provide: EVENT_MANAGER_PLUGINS, useClass: KeyEventsPlugin, multi: true, deps: [DOCUMENT]}, + { + provide: EVENT_MANAGER_PLUGINS, + useClass: HammerGesturesPlugin, + multi: true, + deps: [DOCUMENT, HAMMER_GESTURE_CONFIG] + }, + {provide: HAMMER_GESTURE_CONFIG, useClass: HammerGestureConfig, deps: []}, + { + provide: DomRendererFactory2, + useClass: DomRendererFactory2, + deps: [EventManager, DomSharedStylesHost] + }, + {provide: RendererFactory2, useExisting: DomRendererFactory2}, + {provide: SharedStylesHost, useExisting: DomSharedStylesHost}, + {provide: DomSharedStylesHost, useClass: DomSharedStylesHost, deps: [DOCUMENT]}, + {provide: Testability, useClass: Testability, deps: [NgZone]}, + {provide: EventManager, useClass: EventManager, deps: [EVENT_MANAGER_PLUGINS, NgZone]}, + ELEMENT_PROBE_PROVIDERS, + {provide: Meta, useClass: Meta, deps: [DOCUMENT]}, + {provide: Title, useClass: Title, deps: [DOCUMENT]}, +]; + /** * The ng module for the browser. * * */ -@NgModule({ - providers: [ - BROWSER_SANITIZATION_PROVIDERS, - {provide: APP_ROOT, useValue: true}, - {provide: ErrorHandler, useFactory: errorHandler, deps: []}, - {provide: EVENT_MANAGER_PLUGINS, useClass: DomEventsPlugin, multi: true}, - {provide: EVENT_MANAGER_PLUGINS, useClass: KeyEventsPlugin, multi: true}, - {provide: EVENT_MANAGER_PLUGINS, useClass: HammerGesturesPlugin, multi: true}, - {provide: HAMMER_GESTURE_CONFIG, useClass: HammerGestureConfig}, - DomRendererFactory2, - {provide: RendererFactory2, useExisting: DomRendererFactory2}, - {provide: SharedStylesHost, useExisting: DomSharedStylesHost}, - DomSharedStylesHost, - Testability, - EventManager, - ELEMENT_PROBE_PROVIDERS, - Meta, - Title, - ], - exports: [CommonModule, ApplicationModule] -}) +@NgModule({providers: BROWSER_MODULE_PROVIDERS, exports: [CommonModule, ApplicationModule]}) export class BrowserModule { - constructor(@Optional() @SkipSelf() parentModule: BrowserModule) { + constructor(@Optional() @SkipSelf() @Inject(BrowserModule) parentModule: BrowserModule|null) { if (parentModule) { throw new Error( `BrowserModule has already been loaded. If you need access to common directives such as NgIf and NgFor from a lazy loaded module, import CommonModule instead.`); diff --git a/tools/public_api_guard/core/core.d.ts b/tools/public_api_guard/core/core.d.ts index d781017237..d65ed33a3c 100644 --- a/tools/public_api_guard/core/core.d.ts +++ b/tools/public_api_guard/core/core.d.ts @@ -143,6 +143,11 @@ export declare abstract class ComponentRef { abstract onDestroy(callback: Function): void; } +/** @experimental */ +export interface ConstructorSansProvider { + deps?: any[]; +} + export declare const ContentChild: ContentChildDecorator; export interface ContentChildDecorator { @@ -168,7 +173,7 @@ export interface ContentChildrenDecorator { } /** @experimental */ -export declare function createInjector(defType: any, parent?: Injector | null): Injector; +export declare function createInjector(defType: any, parent?: Injector | null, additionalProviders?: StaticProvider[] | null): Injector; /** @experimental */ export declare function createPlatform(injector: Injector): PlatformRef; @@ -609,7 +614,7 @@ export interface Predicate { (value: T): boolean; } -export declare type Provider = TypeProvider | ValueProvider | ClassProvider | ExistingProvider | FactoryProvider | any[]; +export declare type Provider = TypeProvider | ValueProvider | ClassProvider | ConstructorProvider | ExistingProvider | FactoryProvider | any[]; export declare abstract class Query { } diff --git a/tools/public_api_guard/platform-browser/platform-browser.d.ts b/tools/public_api_guard/platform-browser/platform-browser.d.ts index e1c07cbe9b..52231287f2 100644 --- a/tools/public_api_guard/platform-browser/platform-browser.d.ts +++ b/tools/public_api_guard/platform-browser/platform-browser.d.ts @@ -1,5 +1,5 @@ export declare class BrowserModule { - constructor(parentModule: BrowserModule); + constructor(parentModule: BrowserModule | null); /** @experimental */ static withServerTransition(params: { appId: string; }): ModuleWithProviders;