From 445b9a5627fb5584bff6be0b133a49bdd065f517 Mon Sep 17 00:00:00 2001 From: Marc Laval Date: Fri, 20 Jul 2018 14:32:23 +0200 Subject: [PATCH] feat(ivy): support ViewContainerRef.createComponent() (#24997) PR Close #24997 --- packages/core/src/render3/STATUS.md | 2 +- packages/core/src/render3/component_ref.ts | 73 +++- packages/core/src/render3/di.ts | 44 ++- packages/core/src/render3/index.ts | 2 +- packages/core/src/render3/instructions.ts | 75 ++-- packages/core/src/render3/view_ref.ts | 44 +-- .../hello_world/bundle.golden_symbols.json | 15 + .../bundling/todo/bundle.golden_symbols.json | 23 +- .../test/render3/view_container_ref_spec.ts | 339 ++++++++++++++++-- 9 files changed, 484 insertions(+), 133 deletions(-) diff --git a/packages/core/src/render3/STATUS.md b/packages/core/src/render3/STATUS.md index 233fdbb7ff..f3d1550331 100644 --- a/packages/core/src/render3/STATUS.md +++ b/packages/core/src/render3/STATUS.md @@ -244,7 +244,7 @@ The goal is for the `@Component` (and friends) to be the compiler of template. S | `clear()` | ✅ | n/a | n/a | n/a | n/a | n/a | | `get()` | ✅ | n/a | n/a | n/a | n/a | n/a | | `createEmbededView()` | ✅ | ✅ | n/a | n/a | n/a | n/a | -| `createComponent()` | ❌ | n/a | n/a | n/a | n/a | n/a | +| `createComponent()` | ✅ | n/a | n/a | n/a | n/a | n/a | | `insert()` | ✅ | n/a | n/a | n/a | n/a | n/a | | `move()` | ✅ | n/a | n/a | n/a | n/a | n/a | | `indexOf()` | ✅ | n/a | n/a | n/a | n/a | n/a | diff --git a/packages/core/src/render3/component_ref.ts b/packages/core/src/render3/component_ref.ts index ea8453b4ed..747adbcc7b 100644 --- a/packages/core/src/render3/component_ref.ts +++ b/packages/core/src/render3/component_ref.ts @@ -17,12 +17,12 @@ import {RendererFactory2} from '../render/api'; import {Type} from '../type'; import {assertComponentType, assertDefined} from './assert'; -import {createRootContext} from './component'; -import {baseDirectiveCreate, createLViewData, createTView, enterView, hostElement, initChangeDetectorIfExisting, locateHostElement} from './instructions'; -import {ComponentDefInternal, ComponentType} from './interfaces/definition'; -import {LElementNode} from './interfaces/node'; -import {RElement} from './interfaces/renderer'; -import {INJECTOR, LViewData, LViewFlags, RootContext} from './interfaces/view'; +import {LifecycleHooksFeature, createRootContext} from './component'; +import {baseDirectiveCreate, createLNode, createLViewData, createTView, elementCreate, enterView, hostElement, initChangeDetectorIfExisting, locateHostElement, renderEmbeddedTemplate} from './instructions'; +import {ComponentDefInternal, ComponentType, RenderFlags} from './interfaces/definition'; +import {LElementNode, TNode, TNodeType} from './interfaces/node'; +import {RElement, domRendererFactory3} from './interfaces/renderer'; +import {FLAGS, INJECTOR, LViewData, LViewFlags, RootContext, TVIEW} from './interfaces/view'; import {ViewRef} from './view_ref'; export class ComponentFactoryResolver extends viewEngine_ComponentFactoryResolver { @@ -80,23 +80,28 @@ export class ComponentFactory extends viewEngine_ComponentFactory { } create( - parentComponentInjector: Injector, projectableNodes?: any[][]|undefined, - rootSelectorOrNode?: any, + injector: Injector, projectableNodes?: any[][]|undefined, rootSelectorOrNode?: any, ngModule?: viewEngine_NgModuleRef|undefined): viewEngine_ComponentRef { - ngDevMode && assertDefined(ngModule, 'ngModule should always be defined'); + const isInternalRootView = rootSelectorOrNode === undefined; - const rendererFactory = ngModule ? ngModule.injector.get(RendererFactory2) : document; - const hostNode = locateHostElement(rendererFactory, rootSelectorOrNode); + const rendererFactory = + ngModule ? ngModule.injector.get(RendererFactory2) : domRendererFactory3; + const hostNode = isInternalRootView ? + elementCreate( + this.selector, rendererFactory.createRenderer(null, this.componentDef.rendererType)) : + 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); + const rootContext: RootContext = ngModule && !isInternalRootView ? + ngModule.injector.get(ROOT_CONTEXT) : + createRootContext(requestAnimationFrame.bind(window)); // Create the root view. Uses empty TView and ContentTemplate. const rootView: LViewData = createLViewData( rendererFactory.createRenderer(hostNode, this.componentDef.rendererType), - createTView(-1, null, null, null, null), null, + createTView(-1, null, null, null, null), rootContext, this.componentDef.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways); rootView[INJECTOR] = ngModule && ngModule.injector || null; @@ -116,14 +121,49 @@ export class ComponentFactory extends viewEngine_ComponentFactory { component = baseDirectiveCreate(0, this.componentDef.factory(), this.componentDef) as T); initChangeDetectorIfExisting(elementNode.nodeInjector, component, elementNode.data !); + // TODO: should LifecycleHooksFeature and other host features be generated by the compiler and + // executed here? + // Angular 5 reference: https://stackblitz.com/edit/lifecycle-hooks-vcref + LifecycleHooksFeature(component, this.componentDef); + + // Transform the arrays of native nodes into a LNode structure that can be consumed by the + // projection instruction. This is needed to support the reprojection of these nodes. + if (projectableNodes) { + let index = 0; + const projection: TNode[] = elementNode.tNode.projection = []; + for (let i = 0; i < projectableNodes.length; i++) { + const nodeList = projectableNodes[i]; + let firstTNode: TNode|null = null; + let previousTNode: TNode|null = null; + for (let j = 0; j < nodeList.length; j++) { + const lNode = + createLNode(++index, TNodeType.Element, nodeList[j] as RElement, null, null); + if (previousTNode) { + previousTNode.next = lNode.tNode; + } else { + firstTNode = lNode.tNode; + } + previousTNode = lNode.tNode; + } + projection.push(firstTNode !); + } + } + + // Execute the template in creation mode only, and then turn off the CreationMode flag + renderEmbeddedTemplate(elementNode, elementNode.data ![TVIEW], component, RenderFlags.Create); + elementNode.data ![FLAGS] &= ~LViewFlags.CreationMode; } 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 !); + const componentRef = + new ComponentRef(this.componentType, component, rootView, injector, hostNode !); + if (isInternalRootView) { + // The host element of the internal root view is attached to the component's host view node + componentRef.hostView._lViewNode !.tNode.child = elementNode.tNode; + } + return componentRef; } } @@ -159,6 +199,7 @@ export class ComponentRef extends viewEngine_ComponentRef { * 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.hostView._lViewNode = createLNode(-1, TNodeType.View, null, null, null, rootView); this.injector = injector; this.location = new ElementRef(hostNode); this.componentType = componentType; diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts index e6eb323f8b..b353fb95db 100644 --- a/packages/core/src/render3/di.ts +++ b/packages/core/src/render3/di.ts @@ -11,6 +11,7 @@ import {ChangeDetectorRef as viewEngine_ChangeDetectorRef} from '../change_detection/change_detector_ref'; import {InjectFlags, Injector, inject, setCurrentInjector} 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 as viewEngine_ElementRef} from '../linker/element_ref'; import {NgModuleRef as viewEngine_NgModuleRef} from '../linker/ng_module_factory'; import {TemplateRef as viewEngine_TemplateRef} from '../linker/template_ref'; @@ -19,9 +20,10 @@ import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, ViewRef as viewEngine_Vie import {Type} from '../type'; import {assertDefined, assertGreaterThan, assertLessThan} from './assert'; +import {ComponentFactoryResolver} from './component_ref'; import {addToViewTree, assertPreviousIsParent, createEmbeddedViewNode, createLContainer, createLNodeObject, createTNode, getPreviousOrParentNode, getRenderer, isComponent, renderEmbeddedTemplate, resolveDirective} from './instructions'; import {VIEWS} from './interfaces/container'; -import {ComponentTemplate, DirectiveDefInternal, RenderFlags} from './interfaces/definition'; +import {DirectiveDefInternal, RenderFlags} from './interfaces/definition'; import {LInjector} from './interfaces/injector'; import {AttributeMarker, LContainerNode, LElementNode, LNode, LViewNode, TContainerNode, TElementNode, TNodeFlags, TNodeType} from './interfaces/node'; import {LQueries, QueryReadType} from './interfaces/query'; @@ -29,8 +31,8 @@ import {Renderer3} from './interfaces/renderer'; import {DIRECTIVES, HOST_NODE, INJECTOR, LViewData, QUERIES, RENDERER, TVIEW, TView} from './interfaces/view'; import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; import {addRemoveViewFromContainer, appendChild, detachView, getChildLNode, getParentLNode, insertView, removeView} from './node_manipulation'; -import {notImplemented, stringify} from './util'; -import {EmbeddedViewRef, ViewRef} from './view_ref'; +import {stringify} from './util'; +import {ViewRef} from './view_ref'; @@ -130,7 +132,7 @@ export function getOrCreateNodeInjectorForNode(node: LElementNode | LContainerNo templateRef: null, viewContainerRef: null, elementRef: null, - changeDetectorRef: null + changeDetectorRef: null, }; } @@ -221,6 +223,18 @@ export function injectChangeDetectorRef(): viewEngine_ChangeDetectorRef { return getOrCreateChangeDetectorRef(getOrCreateNodeInjector(), null); } +/** + * Creates a ComponentFactoryResolver and stores it on the injector. Or, if the + * ComponentFactoryResolver + * already exists, retrieves the existing ComponentFactoryResolver. + * + * @returns The ComponentFactoryResolver instance to use + */ +export function injectComponentFactoryResolver(): viewEngine_ComponentFactoryResolver { + return componentFactoryResolver; +} +const componentFactoryResolver: ComponentFactoryResolver = new ComponentFactoryResolver(); + /** * Inject static attribute value into directive constructor. * @@ -634,7 +648,7 @@ class ViewContainerRef implements viewEngine_ViewContainerRef { const adjustedIdx = this._adjustIndex(index); const viewRef = (templateRef as TemplateRef) .createEmbeddedView(context || {}, this._lContainerNode, adjustedIdx); - (viewRef as EmbeddedViewRef).attachToViewContainerRef(this); + (viewRef as ViewRef).attachToViewContainerRef(this); this._viewRefs.splice(adjustedIdx, 0, viewRef); return viewRef; } @@ -642,15 +656,23 @@ class ViewContainerRef implements viewEngine_ViewContainerRef { createComponent( componentFactory: viewEngine_ComponentFactory, index?: number|undefined, injector?: Injector|undefined, projectableNodes?: any[][]|undefined, - ngModule?: viewEngine_NgModuleRef|undefined): viewEngine_ComponentRef { - throw notImplemented(); + ngModuleRef?: viewEngine_NgModuleRef|undefined): viewEngine_ComponentRef { + const contextInjector = injector || this.parentInjector; + if (!ngModuleRef && contextInjector) { + ngModuleRef = contextInjector.get(viewEngine_NgModuleRef); + } + + const componentRef = + componentFactory.create(contextInjector, projectableNodes, undefined, ngModuleRef); + this.insert(componentRef.hostView, index); + return componentRef; } insert(viewRef: viewEngine_ViewRef, index?: number): viewEngine_ViewRef { if (viewRef.destroyed) { throw new Error('Cannot insert a destroyed View in a ViewContainer!'); } - const lViewNode = (viewRef as EmbeddedViewRef)._lViewNode; + const lViewNode = (viewRef as ViewRef)._lViewNode !; const adjustedIdx = this._adjustIndex(index); insertView(this._lContainerNode, lViewNode, adjustedIdx); @@ -660,7 +682,7 @@ class ViewContainerRef implements viewEngine_ViewContainerRef { this._lContainerNode.native; addRemoveViewFromContainer(this._lContainerNode, lViewNode, true, beforeNode); - (viewRef as EmbeddedViewRef).attachToViewContainerRef(this); + (viewRef as ViewRef).attachToViewContainerRef(this); this._viewRefs.splice(adjustedIdx, 0, viewRef); return viewRef; @@ -736,6 +758,8 @@ class TemplateRef implements viewEngine_TemplateRef { insertView(containerNode, viewNode, index !); } renderEmbeddedTemplate(viewNode, this._tView, context, RenderFlags.Create); - return new EmbeddedViewRef(viewNode, this._tView.template !as ComponentTemplate, context); + const viewRef = new ViewRef(viewNode.data, context); + viewRef._lViewNode = viewNode; + return viewRef; } } diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index 2838b658af..014a092e66 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -15,7 +15,7 @@ import {I18nExpInstruction, I18nInstruction, i18nExpMapping, i18nInterpolation, import {ComponentDef, ComponentDefInternal, ComponentTemplate, ComponentType, DirectiveDef, DirectiveDefFlags, DirectiveDefInternal, 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 {QUERY_READ_CONTAINER_REF, QUERY_READ_ELEMENT_REF, QUERY_READ_FROM_NODE, QUERY_READ_TEMPLATE_REF, directiveInject, injectAttribute, injectChangeDetectorRef, injectComponentFactoryResolver, injectElementRef, injectTemplateRef, injectViewContainerRef} from './di'; export {RenderFlags} from './interfaces/definition'; export {CssSelectorList} from './interfaces/projection'; diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index eba6b90974..3b27b0d5d8 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -529,29 +529,35 @@ export function createEmbeddedViewNode( * TView for dynamically created views on their host TNode, which only has one instance. */ export function renderEmbeddedTemplate( - viewNode: LViewNode, tView: TView, context: T, rf: RenderFlags): LViewNode { + viewNode: LViewNode | LElementNode, tView: TView, context: T, rf: RenderFlags): LViewNode| + LElementNode { const _isParent = isParent; const _previousOrParentNode = previousOrParentNode; let oldView: LViewData; - try { - isParent = true; - previousOrParentNode = null !; + if (viewNode.data ![PARENT] == null && viewNode.data ![CONTEXT] && !tView.template) { + // This is a root view inside the view tree + tickRootContext(viewNode.data ![CONTEXT] as RootContext); + } else { + try { + isParent = true; + previousOrParentNode = null !; - oldView = enterView(viewNode.data, viewNode); - namespaceHTML(); - tView.template !(rf, context); - if (rf & RenderFlags.Update) { - refreshView(); - } else { - viewNode.data[TVIEW].firstTemplatePass = firstTemplatePass = false; + oldView = enterView(viewNode.data !, viewNode); + namespaceHTML(); + tView.template !(rf, context); + if (rf & RenderFlags.Update) { + refreshView(); + } else { + viewNode.data ![TVIEW].firstTemplatePass = firstTemplatePass = false; + } + } finally { + // renderEmbeddedTemplate() is called twice in fact, once for creation only and then once for + // update. When for creation only, leaveView() must not trigger view hooks, nor clean flags. + const isCreationOnly = (rf & RenderFlags.Create) === RenderFlags.Create; + leaveView(oldView !, isCreationOnly); + isParent = _isParent; + previousOrParentNode = _previousOrParentNode; } - } finally { - // renderEmbeddedTemplate() is called twice in fact, once for creation only and then once for - // update. When for creation only, leaveView() must not trigger view hooks, nor clean flags. - const isCreationOnly = (rf & RenderFlags.Create) === RenderFlags.Create; - leaveView(oldView !, isCreationOnly); - isParent = _isParent; - previousOrParentNode = _previousOrParentNode; } return viewNode; } @@ -654,17 +660,7 @@ export function elementStart( ngDevMode && ngDevMode.rendererCreateElement++; - let native: RElement; - - if (isProceduralRenderer(renderer)) { - native = renderer.createElement(name, _currentNamespace); - } else { - if (_currentNamespace === null) { - native = renderer.createElement(name); - } else { - native = renderer.createElementNS(_currentNamespace, name); - } - } + const native = elementCreate(name); ngDevMode && assertDataInRange(index - 1); @@ -679,6 +675,27 @@ export function elementStart( createDirectivesAndLocals(localRefs); return native; } +/** + * Creates a native element from a tag name, using a renderer. + * @param name the tag name + * @param overriddenRenderer Optional A renderer to override the default one + * @returns the element created + */ +export function elementCreate(name: string, overriddenRenderer?: Renderer3): RElement { + let native: RElement; + const rendererToUse = overriddenRenderer || renderer; + + if (isProceduralRenderer(rendererToUse)) { + native = rendererToUse.createElement(name, _currentNamespace); + } else { + if (_currentNamespace === null) { + native = rendererToUse.createElement(name); + } else { + native = rendererToUse.createElementNS(_currentNamespace, name); + } + } + return native; +} /** * Creates directive instances and populates local refs. diff --git a/packages/core/src/render3/view_ref.ts b/packages/core/src/render3/view_ref.ts index 6bf5d4e6b6..1b23cf216c 100644 --- a/packages/core/src/render3/view_ref.ts +++ b/packages/core/src/render3/view_ref.ts @@ -12,7 +12,6 @@ import {ViewContainerRef as viewEngine_ViewContainerRef} from '../linker/view_co import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, InternalViewRef as viewEngine_InternalViewRef} from '../linker/view_ref'; import {checkNoChanges, detectChanges, markViewDirty, storeCleanupFn, viewAttached} from './instructions'; -import {ComponentTemplate} from './interfaces/definition'; import {LViewNode} from './interfaces/node'; import {FLAGS, LViewData, LViewFlags} from './interfaces/view'; import {destroyLView} from './node_manipulation'; @@ -24,8 +23,13 @@ export interface viewEngine_ChangeDetectorRef_interface extends viewEngine_Chang export class ViewRef implements viewEngine_EmbeddedViewRef, viewEngine_InternalViewRef, viewEngine_ChangeDetectorRef_interface { - // TODO(issue/24571): remove '!'. - private _appRef !: ApplicationRef | null; + private _appRef: ApplicationRef|null = null; + private _viewContainerRef: viewEngine_ViewContainerRef|null = null; + + /** + * @internal + */ + _lViewNode: LViewNode|null = null; context: T; // TODO(issue/24571): remove '!'. @@ -43,7 +47,13 @@ export class ViewRef implements viewEngine_EmbeddedViewRef, viewEngine_Int return (this._view[FLAGS] & LViewFlags.Destroyed) === LViewFlags.Destroyed; } - destroy(): void { destroyLView(this._view); } + destroy(): void { + if (this._viewContainerRef && viewAttached(this._view)) { + this._viewContainerRef.detach(this._viewContainerRef.indexOf(this)); + this._viewContainerRef = null; + } + destroyLView(this._view); + } onDestroy(callback: Function) { storeCleanupFn(this._view, callback); } @@ -227,31 +237,9 @@ export class ViewRef implements viewEngine_EmbeddedViewRef, viewEngine_Int */ checkNoChanges(): void { checkNoChanges(this.context); } + attachToViewContainerRef(vcRef: viewEngine_ViewContainerRef) { this._viewContainerRef = vcRef; } + detachFromAppRef() { this._appRef = null; } attachToAppRef(appRef: ApplicationRef) { this._appRef = appRef; } } - - -export class EmbeddedViewRef extends ViewRef { - /** - * @internal - */ - _lViewNode: LViewNode; - private _viewContainerRef: viewEngine_ViewContainerRef|null = null; - - constructor(viewNode: LViewNode, template: ComponentTemplate, context: T) { - super(viewNode.data, context); - this._lViewNode = viewNode; - } - - destroy(): void { - if (this._viewContainerRef && viewAttached(this._view)) { - this._viewContainerRef.detach(this._viewContainerRef.indexOf(this)); - this._viewContainerRef = null; - } - super.destroy(); - } - - attachToViewContainerRef(vcRef: viewEngine_ViewContainerRef) { this._viewContainerRef = vcRef; } -} 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 5914081fe2..68ee9b8b51 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -50,6 +50,9 @@ { "name": "NG_PROJECT_AS_ATTR_NAME" }, + { + "name": "PARENT" + }, { "name": "QUERIES" }, @@ -80,6 +83,9 @@ { "name": "_ROOT_DIRECTIVE_INDICES" }, + { + "name": "_getComponentHostLElementNode" + }, { "name": "_renderCompCount" }, @@ -170,6 +176,9 @@ { "name": "getRenderFlags" }, + { + "name": "getRootView" + }, { "name": "hostElement" }, @@ -209,6 +218,9 @@ { "name": "renderComponent" }, + { + "name": "renderComponentOrTemplate" + }, { "name": "renderEmbeddedTemplate" }, @@ -230,6 +242,9 @@ { "name": "text" }, + { + "name": "tickRootContext" + }, { "name": "updateViewQuery" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 2957b13643..86230a5fc4 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -42,10 +42,7 @@ "name": "EMPTY_RENDERER_TYPE_ID" }, { - "name": "ElementRef" - }, - { - "name": "EmbeddedViewRef" + "name": "ElementRef$1" }, { "name": "FLAGS" @@ -95,6 +92,9 @@ { "name": "NgIfContext" }, + { + "name": "NgModuleRef" + }, { "name": "Optional" }, @@ -155,6 +155,9 @@ { "name": "ViewEncapsulation$1" }, + { + "name": "ViewRef" + }, { "name": "_CLEAN_PROMISE" }, @@ -167,9 +170,6 @@ { "name": "_ROOT_DIRECTIVE_INDICES" }, - { - "name": "__extends" - }, { "name": "__read" }, @@ -401,6 +401,9 @@ { "name": "elementClassProp" }, + { + "name": "elementCreate" + }, { "name": "elementEnd" }, @@ -437,9 +440,6 @@ { "name": "executePipeOnDestroys" }, - { - "name": "extendStatics" - }, { "name": "extractDirectiveDef" }, @@ -668,9 +668,6 @@ { "name": "namespaceHTML" }, - { - "name": "notImplemented" - }, { "name": "pointers" }, diff --git a/packages/core/test/render3/view_container_ref_spec.ts b/packages/core/test/render3/view_container_ref_spec.ts index 894b1bc552..0be79218fd 100644 --- a/packages/core/test/render3/view_container_ref_spec.ts +++ b/packages/core/test/render3/view_container_ref_spec.ts @@ -6,11 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, Directive, EmbeddedViewRef, Pipe, PipeTransform, TemplateRef, ViewContainerRef} from '../../src/core'; +import {Component, ComponentFactoryResolver, Directive, EmbeddedViewRef, NgModuleRef, Pipe, PipeTransform, RendererFactory2, TemplateRef, ViewContainerRef, createInjector, defineInjector, ɵAPP_ROOT as APP_ROOT, ɵNgModuleDef as NgModuleDef} from '../../src/core'; import {getOrCreateNodeInjectorForNode, getOrCreateTemplateRef} from '../../src/render3/di'; -import {NgOnChangesFeature, defineComponent, defineDirective, definePipe, injectTemplateRef, injectViewContainerRef} from '../../src/render3/index'; -import {bind, container, containerRefreshEnd, containerRefreshStart, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, interpolation1, load, loadDirective, projection, projectionDef, reserveSlots, text, textBinding} from '../../src/render3/instructions'; +import {NgOnChangesFeature, defineComponent, defineDirective, definePipe, injectComponentFactoryResolver, injectTemplateRef, injectViewContainerRef} from '../../src/render3/index'; +import {bind, container, containerRefreshEnd, containerRefreshStart, element, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, interpolation1, load, loadDirective, projection, projectionDef, reserveSlots, text, textBinding} from '../../src/render3/instructions'; import {RenderFlags} from '../../src/render3/interfaces/definition'; +import {NgModuleFactory} from '../../src/render3/ng_module_ref'; import {pipe, pipeBind1} from '../../src/render3/pipe'; import {getRendererFactory2} from './imported_renderer2'; @@ -25,7 +26,8 @@ describe('ViewContainerRef', () => { static ngDirectiveDef = defineDirective({ type: DirectiveWithVCRef, selectors: [['', 'vcref', '']], - factory: () => directiveInstance = new DirectiveWithVCRef(injectViewContainerRef()), + factory: () => directiveInstance = new DirectiveWithVCRef( + injectViewContainerRef(), injectComponentFactoryResolver()), inputs: {tplRef: 'tplRef'} }); @@ -34,7 +36,7 @@ describe('ViewContainerRef', () => { // injecting a ViewContainerRef to create a dynamic container in which embedded views will be // created - constructor(public vcref: ViewContainerRef) {} + constructor(public vcref: ViewContainerRef, public cfr: ComponentFactoryResolver) {} } describe('API', () => { @@ -650,6 +652,177 @@ describe('ViewContainerRef', () => { expect(() => { directiveInstance !.vcref.move(viewRef !, 42); }).toThrow(); }); }); + + describe('createComponent', () => { + let templateExecutionCounter = 0; + + class EmbeddedComponent { + static ngComponentDef = defineComponent({ + type: EmbeddedComponent, + selectors: [['embedded-cmp']], + factory: () => new EmbeddedComponent(), + template: (rf: RenderFlags, cmp: EmbeddedComponent) => { + templateExecutionCounter++; + if (rf & RenderFlags.Create) { + text(0, 'foo'); + } + } + }); + } + + it('should work without Injector and NgModuleRef', () => { + templateExecutionCounter = 0; + const fixture = new TemplateFixture(createTemplate, updateTemplate, [DirectiveWithVCRef]); + expect(fixture.html).toEqual('

'); + expect(templateExecutionCounter).toEqual(0); + + const componentRef = directiveInstance !.vcref.createComponent( + directiveInstance !.cfr.resolveComponentFactory(EmbeddedComponent)); + fixture.update(); + expect(fixture.html).toEqual('

foo'); + expect(templateExecutionCounter).toEqual(2); + + directiveInstance !.vcref.detach(0); + fixture.update(); + expect(fixture.html).toEqual('

'); + expect(templateExecutionCounter).toEqual(2); + + directiveInstance !.vcref.insert(componentRef.hostView); + fixture.update(); + expect(fixture.html).toEqual('

foo'); + expect(templateExecutionCounter).toEqual(3); + }); + + it('should work with NgModuleRef and Injector', () => { + class MyAppModule { + static ngInjectorDef = defineInjector({ + factory: () => new MyAppModule(), + imports: [], + providers: [ + {provide: APP_ROOT, useValue: true}, + {provide: RendererFactory2, useValue: getRendererFactory2(document)} + ] + }); + static ngModuleDef: NgModuleDef = { bootstrap: [] } as any; + } + const myAppModuleFactory = new NgModuleFactory(MyAppModule); + const ngModuleRef = myAppModuleFactory.create(null); + + class SomeModule { + static ngInjectorDef = defineInjector({ + factory: () => new SomeModule(), + providers: [{provide: NgModuleRef, useValue: ngModuleRef}] + }); + } + const injector = createInjector(SomeModule); + + templateExecutionCounter = 0; + const fixture = new TemplateFixture(createTemplate, updateTemplate, [DirectiveWithVCRef]); + expect(fixture.html).toEqual('

'); + expect(templateExecutionCounter).toEqual(0); + + directiveInstance !.vcref.createComponent( + directiveInstance !.cfr.resolveComponentFactory(EmbeddedComponent), 0, injector); + fixture.update(); + expect(fixture.html).toEqual('

foo'); + expect(templateExecutionCounter).toEqual(2); + + directiveInstance !.vcref.createComponent( + directiveInstance !.cfr.resolveComponentFactory(EmbeddedComponent), 0, undefined, + undefined, ngModuleRef); + fixture.update(); + expect(fixture.html) + .toEqual( + '

foofoo'); + expect(templateExecutionCounter).toEqual(5); + }); + + class EmbeddedComponentWithNgContent { + static ngComponentDef = defineComponent({ + type: EmbeddedComponentWithNgContent, + selectors: [['embedded-cmp-with-ngcontent']], + factory: () => new EmbeddedComponentWithNgContent(), + template: (rf: RenderFlags, cmp: EmbeddedComponentWithNgContent) => { + if (rf & RenderFlags.Create) { + projectionDef(); + projection(0, 0); + element(1, 'hr'); + projection(2, 1); + } + } + }); + } + + it('should support projectable nodes', () => { + const fixture = new TemplateFixture(createTemplate, updateTemplate, [DirectiveWithVCRef]); + expect(fixture.html).toEqual('

'); + + const myNode = document.createElement('div'); + const myText = document.createTextNode('bar'); + const myText2 = document.createTextNode('baz'); + myNode.appendChild(myText); + myNode.appendChild(myText2); + + directiveInstance !.vcref.createComponent( + directiveInstance !.cfr.resolveComponentFactory(EmbeddedComponentWithNgContent), 0, + undefined, [[myNode]]); + fixture.update(); + expect(fixture.html) + .toEqual( + '

barbaz

'); + }); + + it('should support reprojection of projectable nodes', () => { + class Reprojector { + static ngComponentDef = defineComponent({ + type: Reprojector, + selectors: [['reprojector']], + factory: () => new Reprojector(), + template: (rf: RenderFlags, cmp: Reprojector) => { + if (rf & RenderFlags.Create) { + projectionDef(); + elementStart(0, 'embedded-cmp-with-ngcontent'); + { projection(1, 0); } + elementEnd(); + } + }, + directives: [EmbeddedComponentWithNgContent] + }); + } + + const fixture = new TemplateFixture(createTemplate, updateTemplate, [DirectiveWithVCRef]); + expect(fixture.html).toEqual('

'); + + const myNode = document.createElement('div'); + const myText = document.createTextNode('bar'); + const myText2 = document.createTextNode('baz'); + myNode.appendChild(myText); + myNode.appendChild(myText2); + + directiveInstance !.vcref.createComponent( + directiveInstance !.cfr.resolveComponentFactory(Reprojector), 0, undefined, [[myNode]]); + fixture.update(); + expect(fixture.html) + .toEqual( + '

barbaz

'); + }); + + it('should support many projectable nodes with many slots', () => { + const fixture = new TemplateFixture(createTemplate, updateTemplate, [DirectiveWithVCRef]); + expect(fixture.html).toEqual('

'); + + directiveInstance !.vcref.createComponent( + directiveInstance !.cfr.resolveComponentFactory(EmbeddedComponentWithNgContent), 0, + undefined, [ + [document.createTextNode('1'), document.createTextNode('2')], + [document.createTextNode('3'), document.createTextNode('4')] + ]); + fixture.update(); + expect(fixture.html) + .toEqual( + '

12
34
'); + }); + }); }); describe('projection', () => { @@ -944,43 +1117,44 @@ describe('ViewContainerRef', () => { // Angular 5 reference: https://stackblitz.com/edit/lifecycle-hooks-vcref const log: string[] = []; - it('should call all hooks in correct order', () => { - @Component({selector: 'hooks', template: `{{name}}`}) - class ComponentWithHooks { - // TODO(issue/24571): remove '!'. - name !: string; - private log(msg: string) { log.push(msg); } + @Component({selector: 'hooks', template: `{{name}}`}) + class ComponentWithHooks { + // TODO(issue/24571): remove '!'. + name !: string; - ngOnChanges() { this.log('onChanges-' + this.name); } - ngOnInit() { this.log('onInit-' + this.name); } - ngDoCheck() { this.log('doCheck-' + this.name); } + private log(msg: string) { log.push(msg); } - ngAfterContentInit() { this.log('afterContentInit-' + this.name); } - ngAfterContentChecked() { this.log('afterContentChecked-' + this.name); } + ngOnChanges() { this.log('onChanges-' + this.name); } + ngOnInit() { this.log('onInit-' + this.name); } + ngDoCheck() { this.log('doCheck-' + this.name); } - ngAfterViewInit() { this.log('afterViewInit-' + this.name); } - ngAfterViewChecked() { this.log('afterViewChecked-' + this.name); } + ngAfterContentInit() { this.log('afterContentInit-' + this.name); } + ngAfterContentChecked() { this.log('afterContentChecked-' + this.name); } - ngOnDestroy() { this.log('onDestroy-' + this.name); } + ngAfterViewInit() { this.log('afterViewInit-' + this.name); } + ngAfterViewChecked() { this.log('afterViewChecked-' + this.name); } - static ngComponentDef = defineComponent({ - type: ComponentWithHooks, - selectors: [['hooks']], - factory: () => new ComponentWithHooks(), - template: (rf: RenderFlags, cmp: ComponentWithHooks) => { - if (rf & RenderFlags.Create) { - text(0); - } - if (rf & RenderFlags.Update) { - textBinding(0, interpolation1('', cmp.name, '')); - } - }, - features: [NgOnChangesFeature], - inputs: {name: 'name'} - }); - } + ngOnDestroy() { this.log('onDestroy-' + this.name); } + static ngComponentDef = defineComponent({ + type: ComponentWithHooks, + selectors: [['hooks']], + factory: () => new ComponentWithHooks(), + template: (rf: RenderFlags, cmp: ComponentWithHooks) => { + if (rf & RenderFlags.Create) { + text(0); + } + if (rf & RenderFlags.Update) { + textBinding(0, interpolation1('', cmp.name, '')); + } + }, + features: [NgOnChangesFeature], + inputs: {name: 'name'} + }); + } + + it('should call all hooks in correct order when creating with createEmbeddedView', () => { @Component({ template: ` @@ -1022,6 +1196,8 @@ describe('ViewContainerRef', () => { }); } + log.length = 0; + const fixture = new ComponentFixture(SomeComponent); expect(log).toEqual([ 'onChanges-A', 'onInit-A', 'doCheck-A', 'onChanges-B', 'onInit-B', 'doCheck-B', @@ -1082,5 +1258,98 @@ describe('ViewContainerRef', () => { 'afterViewChecked-A', 'afterViewChecked-B' ]); }); + + it('should call all hooks in correct order when creating with createComponent', () => { + @Component({ + template: ` + + + ` + }) + class SomeComponent { + static ngComponentDef = defineComponent({ + type: SomeComponent, + selectors: [['some-comp']], + factory: () => new SomeComponent(), + template: (rf: RenderFlags, cmp: SomeComponent) => { + if (rf & RenderFlags.Create) { + elementStart(0, 'hooks', ['vcref', '']); + elementEnd(); + elementStart(1, 'hooks'); + elementEnd(); + } + if (rf & RenderFlags.Update) { + elementProperty(0, 'name', bind('A')); + elementProperty(1, 'name', bind('B')); + } + }, + directives: [ComponentWithHooks, DirectiveWithVCRef] + }); + } + + log.length = 0; + + const fixture = new ComponentFixture(SomeComponent); + expect(log).toEqual([ + 'onChanges-A', 'onInit-A', 'doCheck-A', 'onChanges-B', 'onInit-B', 'doCheck-B', + 'afterContentInit-A', 'afterContentChecked-A', 'afterContentInit-B', + 'afterContentChecked-B', 'afterViewInit-A', 'afterViewChecked-A', 'afterViewInit-B', + 'afterViewChecked-B' + ]); + + log.length = 0; + fixture.update(); + expect(log).toEqual([ + 'doCheck-A', 'doCheck-B', 'afterContentChecked-A', 'afterContentChecked-B', + 'afterViewChecked-A', 'afterViewChecked-B' + ]); + + log.length = 0; + const componentRef = directiveInstance !.vcref.createComponent( + directiveInstance !.cfr.resolveComponentFactory(ComponentWithHooks)); + expect(fixture.html).toEqual('AB'); + expect(log).toEqual([]); + + componentRef.instance.name = 'D'; + log.length = 0; + fixture.update(); + expect(fixture.html).toEqual('ADB'); + expect(log).toEqual([ + 'doCheck-A', 'doCheck-B', 'onChanges-D', 'onInit-D', 'doCheck-D', 'afterContentInit-D', + 'afterContentChecked-D', 'afterViewInit-D', 'afterViewChecked-D', 'afterContentChecked-A', + 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' + ]); + + log.length = 0; + fixture.update(); + expect(log).toEqual([ + 'doCheck-A', 'doCheck-B', 'doCheck-D', 'afterContentChecked-D', 'afterViewChecked-D', + 'afterContentChecked-A', 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' + ]); + + log.length = 0; + const viewRef = directiveInstance !.vcref.detach(0); + fixture.update(); + expect(log).toEqual([ + 'doCheck-A', 'doCheck-B', 'afterContentChecked-A', 'afterContentChecked-B', + 'afterViewChecked-A', 'afterViewChecked-B' + ]); + + log.length = 0; + directiveInstance !.vcref.insert(viewRef !); + fixture.update(); + expect(log).toEqual([ + 'doCheck-A', 'doCheck-B', 'doCheck-D', 'afterContentChecked-D', 'afterViewChecked-D', + 'afterContentChecked-A', 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' + ]); + + log.length = 0; + directiveInstance !.vcref.remove(0); + fixture.update(); + expect(log).toEqual([ + 'onDestroy-D', 'doCheck-A', 'doCheck-B', 'afterContentChecked-A', 'afterContentChecked-B', + 'afterViewChecked-A', 'afterViewChecked-B' + ]); + }); }); });