diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts index d1852f59ac..d86a3d0678 100644 --- a/packages/core/src/render3/di.ts +++ b/packages/core/src/render3/di.ts @@ -19,7 +19,7 @@ import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, ViewRef as viewEngine_Vie import {Type} from '../type'; import {assertLessThan, assertNotNull} from './assert'; -import {assertPreviousIsParent, enterView, getDirectiveInstance, getPreviousOrParentNode, getRenderer, isComponent, renderEmbeddedTemplate} from './instructions'; +import {addToViewTree, assertPreviousIsParent, createLContainer, createLNodeObject, getDirectiveInstance, getPreviousOrParentNode, getRenderer, isComponent, renderEmbeddedTemplate} from './instructions'; import {ComponentTemplate, DirectiveDef} from './interfaces/definition'; import {LInjector} from './interfaces/injector'; import {LContainerNode, LElementNode, LNode, LNodeType, LViewNode, TNodeFlags} from './interfaces/node'; @@ -27,7 +27,7 @@ import {QueryReadType} from './interfaces/query'; import {Renderer3} from './interfaces/renderer'; import {LView} from './interfaces/view'; import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; -import {insertView} from './node_manipulation'; +import {insertView, removeView} from './node_manipulation'; import {notImplemented, stringify} from './util'; import {EmbeddedViewRef, ViewRef, addDestroyable, createViewRef} from './view_ref'; @@ -551,8 +551,21 @@ class ElementRef implements viewEngine_ElementRef { * @returns The ViewContainerRef instance to use */ export function getOrCreateContainerRef(di: LInjector): viewEngine_ViewContainerRef { - return di.viewContainerRef || - (di.viewContainerRef = new ViewContainerRef(di.node as LContainerNode)); + if (!di.viewContainerRef) { + const vcRefHost = di.node; + + ngDevMode && assertNodeOfPossibleTypes(vcRefHost, LNodeType.Container, LNodeType.Element); + + const lContainer = createLContainer(vcRefHost.parent !, vcRefHost.view, undefined, vcRefHost); + const lContainerNode: LContainerNode = createLNodeObject( + LNodeType.Container, vcRefHost.view, vcRefHost.parent !, undefined, lContainer, null); + + addToViewTree(vcRefHost.view, lContainer); + + di.viewContainerRef = new ViewContainerRef(lContainerNode); + } + + return di.viewContainerRef; } /** @@ -560,19 +573,27 @@ export function getOrCreateContainerRef(di: LInjector): viewEngine_ViewContainer * imperatively. */ class ViewContainerRef implements viewEngine_ViewContainerRef { + private _viewRefs: viewEngine_ViewRef[] = []; element: viewEngine_ElementRef; injector: Injector; parentInjector: Injector; - constructor(private _node: LContainerNode) {} + constructor(private _lContainerNode: LContainerNode) {} - clear(): void { throw notImplemented(); } - get(index: number): viewEngine_ViewRef|null { throw notImplemented(); } - length: number; - createEmbeddedView( - templateRef: viewEngine_TemplateRef, context?: C|undefined, - index?: number|undefined): viewEngine_EmbeddedViewRef { - const viewRef = templateRef.createEmbeddedView(context !); + clear(): void { + const lContainer = this._lContainerNode.data; + while (lContainer.views.length) { + this.remove(0); + } + } + get(index: number): viewEngine_ViewRef|null { return this._viewRefs[index] || null; } + get length(): number { + const lContainer = this._lContainerNode.data; + return lContainer.views.length; + } + createEmbeddedView(templateRef: viewEngine_TemplateRef, context?: C, index?: number): + viewEngine_EmbeddedViewRef { + const viewRef = templateRef.createEmbeddedView(context || {}); this.insert(viewRef, index); return viewRef; } @@ -582,36 +603,26 @@ class ViewContainerRef implements viewEngine_ViewContainerRef { ngModule?: viewEngine_NgModuleRef|undefined): viewEngine_ComponentRef { throw notImplemented(); } - insert(viewRef: viewEngine_ViewRef, index?: number|undefined): viewEngine_ViewRef { - if (index == null) { - index = this._node.data.views.length; - } else { - // +1 because it's legal to insert at the end. - ngDevMode && assertLessThan(index, this._node.data.views.length + 1, 'index'); - } - const lView = (viewRef as EmbeddedViewRef)._lViewNode; - insertView(this._node, lView, index); + insert(viewRef: viewEngine_ViewRef, index?: number): viewEngine_ViewRef { + const lViewNode = (viewRef as EmbeddedViewRef)._lViewNode; + const adjustedIdx = this._adjustAndAssertIndex(index); - // TODO(pk): this is a temporary index adjustment so imperativelly inserted (through - // ViewContainerRef) views - // are not removed in the containerRefreshEnd instruction. - // The final fix will consist of creating a dedicated container node for views inserted through - // ViewContainerRef. - // Such container should not be trimmed as it is the case in the containerRefreshEnd - // instruction. - this._node.data.nextIndex = this._node.data.views.length; + insertView(this._lContainerNode, lViewNode, adjustedIdx); + this._viewRefs.splice(adjustedIdx, 0, viewRef); + + (lViewNode as{parent: LNode}).parent = this._lContainerNode; // If the view is dynamic (has a template), it needs to be counted both at the container // level and at the node above the container. - if (lView.data.template !== null) { + if (lViewNode.data.template !== null) { // Increment the container view count. - this._node.data.dynamicViewCount++; + this._lContainerNode.data.dynamicViewCount++; // Look for the parent node and increment its dynamic view count. - if (this._node.parent !== null && this._node.parent.data !== null) { - ngDevMode && - assertNodeOfPossibleTypes(this._node.parent, LNodeType.View, LNodeType.Element); - this._node.parent.data.dynamicViewCount++; + if (this._lContainerNode.parent !== null && this._lContainerNode.parent.data !== null) { + ngDevMode && assertNodeOfPossibleTypes( + this._lContainerNode.parent, LNodeType.View, LNodeType.Element); + this._lContainerNode.parent.data.dynamicViewCount++; } } return viewRef; @@ -620,8 +631,22 @@ class ViewContainerRef implements viewEngine_ViewContainerRef { throw notImplemented(); } indexOf(viewRef: viewEngine_ViewRef): number { throw notImplemented(); } - remove(index?: number|undefined): void { throw notImplemented(); } + remove(index?: number): void { + const adjustedIdx = this._adjustAndAssertIndex(index); + removeView(this._lContainerNode, adjustedIdx); + this._viewRefs.splice(adjustedIdx, 1); + } detach(index?: number|undefined): viewEngine_ViewRef|null { throw notImplemented(); } + + private _adjustAndAssertIndex(index?: number|undefined) { + if (index == null) { + index = this._lContainerNode.data.views.length; + } else { + // +1 because it's legal to insert at the end. + ngDevMode && assertLessThan(index, this._lContainerNode.data.views.length + 1, 'index'); + } + return index; + } } /** @@ -650,7 +675,7 @@ class TemplateRef implements viewEngine_TemplateRef { } createEmbeddedView(context: T): viewEngine_EmbeddedViewRef { - let viewNode: LViewNode = renderEmbeddedTemplate(null, this._template, context, this._renderer); + const viewNode = renderEmbeddedTemplate(null, this._template, context, this._renderer); return addDestroyable(new EmbeddedViewRef(viewNode, this._template, context)); } } diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 51e993183a..e5a1ffdcab 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -298,6 +298,30 @@ export function createLView( return newView; } +/** + * Creation of LNode object is extracted to a separate function so we always create LNode object + * with the same shape + * (same properties assigned in the same order). + */ +export function createLNodeObject( + type: LNodeType, currentView: LView, parent: LNode, native: RText | RElement | null | undefined, + state: any, + queries: LQueries | null): LElementNode<extNode&LViewNode&LContainerNode&LProjectionNode { + return { + type: type, + native: native as any, + view: currentView, + parent: parent as any, + child: null, + next: null, + nodeInjector: parent ? parent.nodeInjector : null, + data: state, + queries: queries, + tNode: null, + pNextOrParent: null + }; +} + /** * A common way of creating the LNode to make sure that all of them have same shape to * keep the execution code monomorphic and fast. @@ -323,19 +347,8 @@ export function createLNode( (isParent ? currentQueries : previousOrParentNode && previousOrParentNode.queries) || parent && parent.queries && parent.queries.child(); const isState = state != null; - const node: LElementNode<extNode&LViewNode&LContainerNode&LProjectionNode = { - type: type, - native: native as any, - view: currentView, - parent: parent as any, - child: null, - next: null, - nodeInjector: parent ? parent.nodeInjector : null, - data: isState ? state as any : null, - queries: queries, - tNode: null, - pNextOrParent: null - }; + const node = + createLNodeObject(type, currentView, parent, native, isState ? state as any : null, queries); if ((type & LNodeType.ViewOrElement) === LNodeType.ViewOrElement && isState) { // Bit of a hack to bust through the readonly because there is a circular dep between @@ -371,7 +384,7 @@ export function createLNode( } else if (previousOrParentNode) { ngDevMode && assertNull( previousOrParentNode.next, - `previousOrParentNode's next property should not have been set.`); + `previousOrParentNode's next property should not have been set ${index}.`); previousOrParentNode.next = node; } } @@ -446,8 +459,9 @@ export function renderEmbeddedTemplate( enterView(viewNode.data, viewNode); template(context, cm); - refreshDynamicChildren(); refreshDirectives(); + refreshDynamicChildren(); + } finally { leaveView(currentView && currentView !.parent !); isParent = _isParent; @@ -1185,9 +1199,11 @@ function addComponentLogic( // Only component views should be added to the view tree directly. Embedded views are // accessed through their containers because they may be removed / re-added later. - const hostView = addToViewTree(createLView( - -1, rendererFactory.createRenderer(previousOrParentNode.native as RElement, def.rendererType), - tView, null, null, def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways)); + const hostView = addToViewTree( + currentView, createLView( + -1, rendererFactory.createRenderer( + previousOrParentNode.native as RElement, def.rendererType), + tView, null, null, def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways)); (previousOrParentNode.data as any) = hostView; (hostView.node as any) = previousOrParentNode; @@ -1291,6 +1307,26 @@ function generateInitialInputs( //// ViewContainer & View ////////////////////////// + +export function createLContainer( + parentLNode: LNode, currentView: LView, template?: ComponentTemplate, + host?: LContainerNode | LElementNode): LContainer { + ngDevMode && assertNotNull(parentLNode, 'containers should have a parent'); + return { + views: [], + nextIndex: 0, + // If the direct parent of the container is a view, its views will need to be added + // through insertView() when its parent view is being inserted: + renderParent: canInsertNativeNode(parentLNode, currentView) ? parentLNode : null, + template: template == null ? null : template, + next: null, + parent: currentView, + dynamicViewCount: 0, + queries: null, + host: host == null ? null : host + }; +} + /** * Creates an LContainerNode. * @@ -1310,20 +1346,7 @@ export function container( currentView.bindingStartIndex, 'container nodes should be created before any bindings'); const currentParent = isParent ? previousOrParentNode : previousOrParentNode.parent !; - ngDevMode && assertNotNull(currentParent, 'containers should have a parent'); - - const lContainer = { - views: [], - nextIndex: 0, - // If the direct parent of the container is a view, its views will need to be added - // through insertView() when its parent view is being inserted: - renderParent: canInsertNativeNode(currentParent, currentView) ? currentParent : null, - template: template == null ? null : template, - next: null, - parent: currentView, - dynamicViewCount: 0, - queries: null - }; + const lContainer = createLContainer(currentParent, currentView, template); const node = createLNode(index, LNodeType.Container, undefined, lContainer); @@ -1333,7 +1356,7 @@ export function container( // Containers are added to the current view tree instead of their embedded views // because views can be removed and re-inserted. - addToViewTree(node.data); + addToViewTree(currentView, node.data); if (firstTemplatePass) cacheMatchingDirectivesForNode(node.tNode); @@ -1701,10 +1724,11 @@ function findComponentHost(lView: LView): LElementNode { * This structure will be used to traverse through nested views to remove listeners * and call onDestroy callbacks. * + * @param currentView The view where LView or LContainer should be added * @param state The LView or LContainer to add to the view tree * @returns The state passed in */ -export function addToViewTree(state: T): T { +export function addToViewTree(currentView: LView, state: T): T { currentView.tail ? (currentView.tail.next = state) : (currentView.child = state); currentView.tail = state; return state; @@ -1887,8 +1911,8 @@ export function detectChangesInternal( try { template(component, creationMode); - refreshDynamicChildren(); refreshDirectives(); + refreshDynamicChildren(); } finally { leaveView(oldView); } diff --git a/packages/core/src/render3/interfaces/container.ts b/packages/core/src/render3/interfaces/container.ts index 1095ebf076..3790d3444e 100644 --- a/packages/core/src/render3/interfaces/container.ts +++ b/packages/core/src/render3/interfaces/container.ts @@ -7,7 +7,7 @@ */ import {ComponentTemplate} from './definition'; -import {LElementNode, LViewNode} from './node'; +import {LContainerNode, LElementNode, LViewNode} from './node'; import {LQueries} from './query'; import {LView, TView} from './view'; @@ -80,6 +80,13 @@ export interface LContainer { * this container are reported to queries referenced here. */ queries: LQueries|null; + + /** + * If a LContainer is created dynamically (by a directive requesting ViewContainerRef) this fields + * keeps a reference to a node on which a ViewContainerRef was requested. We need to store this + * information to find a next render sibling node. + */ + host: LContainerNode|LElementNode|null; } /** diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index fffd59260f..906185f28d 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -281,7 +281,10 @@ export function insertView( if (!beforeNode) { let containerNextNativeNode = container.native; if (containerNextNativeNode === undefined) { - containerNextNativeNode = container.native = findNextRNodeSibling(container, null); + // TODO(pk): this is probably too simplistic, add more tests for various host placements + // (dynamic view, projection, ...) + containerNextNativeNode = container.native = + findNextRNodeSibling(container.data.host ? container.data.host : container, null); } beforeNode = containerNextNativeNode; } 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 ff48fd607c..5fab70e5bb 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -170,6 +170,9 @@ { "name": "createLNode" }, + { + "name": "createLNodeObject" + }, { "name": "createLView" }, @@ -413,4 +416,4 @@ { "name": "viewAttached" } -] \ No newline at end of file +] diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 98b88a420f..6fe3656f00 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -344,6 +344,9 @@ { "name": "checkNoChangesMode" }, + { + "name": "cleanUpView" + }, { "name": "componentRefresh" }, @@ -359,9 +362,15 @@ { "name": "createInjector" }, + { + "name": "createLContainer" + }, { "name": "createLNode" }, + { + "name": "createLNodeObject" + }, { "name": "createLView" }, @@ -395,6 +404,9 @@ { "name": "defineInjector" }, + { + "name": "destroyViewTree" + }, { "name": "detectChanges" }, @@ -473,6 +485,12 @@ { "name": "executeInitHooks" }, + { + "name": "executeOnDestroys" + }, + { + "name": "executePipeOnDestroys" + }, { "name": "extendStatics" }, @@ -542,6 +560,9 @@ { "name": "getOrCreateTemplateRef" }, + { + "name": "getParentState" + }, { "name": "getPreviousIndex" }, @@ -767,6 +788,12 @@ { "name": "refreshDynamicChildren" }, + { + "name": "removeListeners" + }, + { + "name": "removeView" + }, { "name": "renderComponent" }, @@ -884,4 +911,4 @@ { "name": "ɵ0" } -] \ No newline at end of file +] diff --git a/packages/core/test/render3/common_integration_spec.ts b/packages/core/test/render3/common_integration_spec.ts index 96ac82e0f7..12e30404b4 100644 --- a/packages/core/test/render3/common_integration_spec.ts +++ b/packages/core/test/render3/common_integration_spec.ts @@ -9,12 +9,13 @@ import {NgForOfContext} from '@angular/common'; import {defineComponent} from '../../src/render3/index'; -import {bind, container, containerRefreshEnd, containerRefreshStart, elementEnd, elementProperty, elementStart, text, textBinding} from '../../src/render3/instructions'; +import {bind, container, elementEnd, elementProperty, elementStart, interpolation3, text, textBinding, tick} from '../../src/render3/instructions'; import {NgForOf} from './common_with_def'; -import {renderComponent, toHtml} from './render_util'; +import {ComponentFixture} from './render_util'; describe('@angular/common integration', () => { + describe('NgForOf', () => { it('should update a loop', () => { class MyApp { @@ -34,8 +35,6 @@ describe('@angular/common integration', () => { elementEnd(); } elementProperty(1, 'ngForOf', bind(myApp.items)); - containerRefreshStart(1); - containerRefreshEnd(); function liTemplate(row: NgForOfContext, cm: boolean) { if (cm) { @@ -50,9 +49,71 @@ describe('@angular/common integration', () => { }); } - const myApp = renderComponent(MyApp); - expect(toHtml(myApp)).toEqual('
  • first
  • second
'); + const fixture = new ComponentFixture(MyApp); + expect(fixture.html).toEqual('
  • first
  • second
'); + + // change detection cycle, no model changes + fixture.update(); + expect(fixture.html).toEqual('
  • first
  • second
'); + + // remove the last item + fixture.component.items.length = 1; + fixture.update(); + expect(fixture.html).toEqual('
  • first
'); + + // change an item + fixture.component.items[0] = 'one'; + fixture.update(); + expect(fixture.html).toEqual('
  • one
'); + + // add an item + fixture.component.items.push('two'); + fixture.update(); + expect(fixture.html).toEqual('
  • one
  • two
'); }); - // TODO: Test inheritance + + it('should support ngForOf context variables', () => { + + class MyApp { + items: string[] = ['first', 'second']; + + static ngComponentDef = defineComponent({ + type: MyApp, + factory: () => new MyApp(), + selectors: [['my-app']], + //
    + //
  • {{index}} of {{count}}: {{item}}
  • + //
+ template: (myApp: MyApp, cm: boolean) => { + if (cm) { + elementStart(0, 'ul'); + { container(1, liTemplate, undefined, ['ngForOf', '']); } + elementEnd(); + } + elementProperty(1, 'ngForOf', bind(myApp.items)); + + function liTemplate(row: NgForOfContext, cm: boolean) { + if (cm) { + elementStart(0, 'li'); + { text(1); } + elementEnd(); + } + textBinding( + 1, interpolation3('', row.index, ' of ', row.count, ': ', row.$implicit, '')); + } + }, + directives: () => [NgForOf] + }); + } + + const fixture = new ComponentFixture(MyApp); + expect(fixture.html).toEqual('
  • 0 of 2: first
  • 1 of 2: second
'); + + fixture.component.items.splice(1, 0, 'middle'); + fixture.update(); + expect(fixture.html) + .toEqual('
  • 0 of 3: first
  • 1 of 3: middle
  • 2 of 3: second
'); + }); + }); }); diff --git a/packages/core/test/render3/view_container_ref_spec.ts b/packages/core/test/render3/view_container_ref_spec.ts index 473d873f61..0dc09cc7b4 100644 --- a/packages/core/test/render3/view_container_ref_spec.ts +++ b/packages/core/test/render3/view_container_ref_spec.ts @@ -7,10 +7,11 @@ */ import {TemplateRef, ViewContainerRef} from '../../src/core'; +import {getOrCreateNodeInjectorForNode, getOrCreateTemplateRef} from '../../src/render3/di'; import {defineComponent, defineDirective, injectTemplateRef, injectViewContainerRef} from '../../src/render3/index'; -import {bind, container, containerRefreshEnd, containerRefreshStart, loadDirective, text, textBinding} from '../../src/render3/instructions'; +import {bind, container, elementEnd, elementProperty, elementStart, interpolation1, load, loadDirective, text, textBinding} from '../../src/render3/instructions'; -import {renderComponent, toHtml} from './render_util'; +import {ComponentFixture} from './render_util'; describe('ViewContainerRef', () => { class TestDirective { @@ -18,7 +19,7 @@ describe('ViewContainerRef', () => { static ngDirectiveDef = defineDirective({ type: TestDirective, - selectors: [['', 'testDir', '']], + selectors: [['', 'testdir', '']], factory: () => new TestDirective(injectViewContainerRef(), injectTemplateRef(), ), }); } @@ -38,11 +39,9 @@ describe('ViewContainerRef', () => { } textBinding(0, bind(ctx.$implicit)); }; - container(0, subTemplate, undefined, ['testDir', '']); + container(0, subTemplate, undefined, ['testdir', '']); } - containerRefreshStart(0); cmp.testDir = loadDirective(0); - containerRefreshEnd(); }, directives: [TestDirective] }); @@ -50,11 +49,137 @@ describe('ViewContainerRef', () => { it('should add embedded view into container', () => { - const testCmp = renderComponent(TestComponent); - expect(toHtml(testCmp)).toEqual(''); - const dir = testCmp.testDir; + const fixture = new ComponentFixture(TestComponent); + expect(fixture.html).toEqual(''); + + const dir = fixture.component.testDir; const childCtx = {$implicit: 'works'}; - const viewRef = dir.viewContainer.createEmbeddedView(dir.template, childCtx); - expect(toHtml(testCmp)).toEqual('works'); + dir.viewContainer.createEmbeddedView(dir.template, childCtx); + expect(fixture.html).toEqual('works'); }); + + it('should add embedded view into a view container on elements', () => { + let directiveInstance: TestDirective|undefined; + + class TestDirective { + static ngDirectiveDef = defineDirective({ + type: TestDirective, + selectors: [['', 'testdir', '']], + factory: () => directiveInstance = new TestDirective(injectViewContainerRef()), + inputs: {tpl: 'tpl'} + }); + + tpl: TemplateRef<{}>; + + constructor(private _vcRef: ViewContainerRef) {} + + insertTpl(ctx?: {}) { this._vcRef.createEmbeddedView(this.tpl, ctx); } + + clear() { this._vcRef.clear(); } + } + + function EmbeddedTemplate(ctx: any, cm: boolean) { + if (cm) { + text(0, 'From a template.'); + } + } + + /** + * From a template + * before + *
+ * after + */ + class TestComponent { + testDir: TestDirective; + static ngComponentDef = defineComponent({ + type: TestComponent, + selectors: [['test-cmp']], + factory: () => new TestComponent(), + template: (cmp: TestComponent, cm: boolean) => { + if (cm) { + container(0, EmbeddedTemplate); + text(1, 'before'); + elementStart(2, 'div', ['testdir', '']); + elementEnd(); + text(3, 'after'); + } + const tpl = getOrCreateTemplateRef(getOrCreateNodeInjectorForNode( + load(0))); // TODO(pk): we need proper design / spec for this + elementProperty(2, 'tpl', bind(tpl)); + }, + directives: [TestDirective] + }); + } + + + const fixture = new ComponentFixture(TestComponent); + expect(fixture.html).toEqual('before
after'); + + directiveInstance !.insertTpl(); + expect(fixture.html).toEqual('before
From a template.after'); + + directiveInstance !.insertTpl(); + expect(fixture.html) + .toEqual('before
From a template.From a template.after'); + + directiveInstance !.clear(); + expect(fixture.html).toEqual('before
after'); + }); + + it('should add embedded view into a view container on ng-template', () => { + let directiveInstance: TestDirective; + + class TestDirective { + static ngDirectiveDef = defineDirective({ + type: TestDirective, + selectors: [['', 'testdir', '']], + factory: () => directiveInstance = + new TestDirective(injectViewContainerRef(), injectTemplateRef()) + }); + + constructor(private _vcRef: ViewContainerRef, private _tplRef: TemplateRef<{}>) {} + + insertTpl(ctx: {}) { this._vcRef.createEmbeddedView(this._tplRef, ctx); } + + remove(index?: number) { this._vcRef.remove(index); } + } + + function EmbeddedTemplate(ctx: any, cm: boolean) { + if (cm) { + text(0); + } + textBinding(0, interpolation1('Hello, ', ctx.name, '')); + } + + /** + * before|Hello, {{name}}|after + */ + class TestComponent { + testDir: TestDirective; + static ngComponentDef = defineComponent({ + type: TestComponent, + selectors: [['test-cmp']], + factory: () => new TestComponent(), + template: (cmp: TestComponent, cm: boolean) => { + if (cm) { + text(0, 'before|'); + container(1, EmbeddedTemplate, undefined, ['testdir', '']); + text(2, '|after'); + } + }, + directives: [TestDirective] + }); + } + + const fixture = new ComponentFixture(TestComponent); + expect(fixture.html).toEqual('before||after'); + + directiveInstance !.insertTpl({name: 'World'}); + expect(fixture.html).toEqual('before|Hello, World|after'); + + directiveInstance !.remove(0); + expect(fixture.html).toEqual('before||after'); + }); + });