From f4c56f49310b2115c566441d72923b612d8abcc2 Mon Sep 17 00:00:00 2001 From: Marc Laval Date: Thu, 5 Apr 2018 10:29:47 +0200 Subject: [PATCH] feat(ivy): implement some of the ViewContainerRef API (#23189) PR Close #23189 --- packages/core/src/render3/assert.ts | 6 + packages/core/src/render3/di.ts | 51 +- .../core/src/render3/node_manipulation.ts | 1 + packages/core/test/render3/render_util.ts | 13 +- .../test/render3/view_container_ref_spec.ts | 705 ++++++++++-------- 5 files changed, 456 insertions(+), 320 deletions(-) diff --git a/packages/core/src/render3/assert.ts b/packages/core/src/render3/assert.ts index 71c693e757..8c80e47fb6 100644 --- a/packages/core/src/render3/assert.ts +++ b/packages/core/src/render3/assert.ts @@ -40,6 +40,12 @@ export function assertLessThan(actual: T, expected: T, msg: string) { } } +export function assertGreaterThan(actual: T, expected: T, msg: string) { + if (actual <= expected) { + throwError(msg); + } +} + export function assertNull(actual: T, msg: string) { if (actual != null) { throwError(msg); diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts index 333f81dcef..964eab955e 100644 --- a/packages/core/src/render3/di.ts +++ b/packages/core/src/render3/di.ts @@ -18,7 +18,7 @@ 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 {assertLessThan, assertNotNull} from './assert'; +import {assertGreaterThan, assertLessThan, assertNotNull} from './assert'; import {addToViewTree, assertPreviousIsParent, createLContainer, createLNodeObject, getDirectiveInstance, getPreviousOrParentNode, getRenderer, isComponent, renderEmbeddedTemplate, resolveDirective} from './instructions'; import {ComponentTemplate, DirectiveDef} from './interfaces/definition'; import {LInjector} from './interfaces/injector'; @@ -608,26 +608,31 @@ class ViewContainerRef implements viewEngine_ViewContainerRef { 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; } + createComponent( componentFactory: viewEngine_ComponentFactory, index?: number|undefined, injector?: Injector|undefined, projectableNodes?: any[][]|undefined, ngModule?: viewEngine_NgModuleRef|undefined): viewEngine_ComponentRef { throw notImplemented(); } + insert(viewRef: viewEngine_ViewRef, index?: number): viewEngine_ViewRef { const lViewNode = (viewRef as EmbeddedViewRef)._lViewNode; - const adjustedIdx = this._adjustAndAssertIndex(index); + const adjustedIdx = this._adjustIndex(index); insertView(this._lContainerNode, lViewNode, adjustedIdx); // invalidate cache of next sibling RNode (we do similar operation in the containerRefreshEnd @@ -653,23 +658,37 @@ class ViewContainerRef implements viewEngine_ViewContainerRef { } return viewRef; } - move(viewRef: viewEngine_ViewRef, currentIndex: number): viewEngine_ViewRef { - throw notImplemented(); - } - indexOf(viewRef: viewEngine_ViewRef): number { 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) { + move(viewRef: viewEngine_ViewRef, newIndex: number): viewEngine_ViewRef { + const index = this.indexOf(viewRef); + this.detach(index); + this.insert(viewRef, this._adjustIndex(newIndex)); + return viewRef; + } + + indexOf(viewRef: viewEngine_ViewRef): number { return this._viewRefs.indexOf(viewRef); } + + remove(index?: number): void { + this.detach(index); + // TODO(ml): proper destroy of the ViewRef, i.e. recursively destroy the LviewNode and its + // children, delete DOM nodes and QueryList, trigger hooks (onDestroy), destroy the renderer, + // detach projected nodes + } + + detach(index?: number): viewEngine_ViewRef|null { + const adjustedIdx = this._adjustIndex(index, -1); + removeView(this._lContainerNode, adjustedIdx); + return this._viewRefs.splice(adjustedIdx, 1)[0] || null; + } + + private _adjustIndex(index?: number, shift: number = 0) { if (index == null) { - index = this._lContainerNode.data.views.length; - } else { + return this._lContainerNode.data.views.length + shift; + } + if (ngDevMode) { + assertGreaterThan(index, -1, 'index must be positive'); // +1 because it's legal to insert at the end. - ngDevMode && assertLessThan(index, this._lContainerNode.data.views.length + 1, 'index'); + assertLessThan(index, this._lContainerNode.data.views.length + 1 + shift, 'index'); } return index; } diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index eda51bc2b2..f42816ce94 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -311,6 +311,7 @@ export function removeView(container: LContainerNode, removeIndex: number): LVie setViewNext(views[removeIndex - 1], viewNode.next); } views.splice(removeIndex, 1); + viewNode.next = null; destroyViewTree(viewNode.data); addRemoveViewFromContainer(container, viewNode, false); // Notify query that view has been removed diff --git a/packages/core/test/render3/render_util.ts b/packages/core/test/render3/render_util.ts index 0652d1ea2c..969fb30691 100644 --- a/packages/core/test/render3/render_util.ts +++ b/packages/core/test/render3/render_util.ts @@ -48,6 +48,8 @@ function noop() {} */ export class TemplateFixture extends BaseFixture { hostNode: LElementNode; + private _directiveDefs: DirectiveDefList|null; + private _pipeDefs: PipeDefList|null; /** * * @param createBlock Instructions which go into the creation block: @@ -55,15 +57,18 @@ export class TemplateFixture extends BaseFixture { * @param updateBlock Optional instructions which go after the creation block: * `if (creationMode) { ... } __here__`. */ - constructor(private createBlock: () => void, private updateBlock: () => void = noop) { + constructor( + private createBlock: () => void, private updateBlock: () => void = noop, + directives?: DirectiveTypesOrFactory|null, pipes?: PipeTypesOrFactory|null) { super(); - this.updateBlock = updateBlock || function() {}; + this._directiveDefs = toDefs(directives, extractDirectiveDef); + this._pipeDefs = toDefs(pipes, extractPipeDef); this.hostNode = renderTemplate(this.hostElement, (ctx: any, cm: boolean) => { if (cm) { this.createBlock(); } this.updateBlock(); - }, null !, domRendererFactory3, null); + }, null !, domRendererFactory3, null, this._directiveDefs, this._pipeDefs); } /** @@ -74,7 +79,7 @@ export class TemplateFixture extends BaseFixture { update(updateBlock?: () => void): void { renderTemplate( this.hostNode.native, updateBlock || this.updateBlock, null !, domRendererFactory3, - this.hostNode); + this.hostNode, this._directiveDefs, this._pipeDefs); } } diff --git a/packages/core/test/render3/view_container_ref_spec.ts b/packages/core/test/render3/view_container_ref_spec.ts index f13dd0772d..2e2693eed3 100644 --- a/packages/core/test/render3/view_container_ref_spec.ts +++ b/packages/core/test/render3/view_container_ref_spec.ts @@ -6,345 +6,450 @@ * found in the LICENSE file at https://angular.io/license */ -import {TemplateRef, ViewContainerRef} from '../../src/core'; +import {Directive, 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, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, interpolation1, load, loadDirective, text, textBinding} from '../../src/render3/instructions'; -import {ComponentFixture} from './render_util'; +import {ComponentFixture, TemplateFixture} from './render_util'; describe('ViewContainerRef', () => { - class TestDirective { - constructor(public viewContainer: ViewContainerRef, public template: TemplateRef, ) {} + describe('API', () => { + let directiveInstance: DirectiveWithVCRef|null; - static ngDirectiveDef = defineDirective({ - type: TestDirective, - selectors: [['', 'testdir', '']], - factory: () => new TestDirective(injectViewContainerRef(), injectTemplateRef(), ), - }); - } + beforeEach(() => { directiveInstance = null; }); - class TestComponent { - testDir: TestDirective; - - static ngComponentDef = defineComponent({ - type: TestComponent, - selectors: [['test-cmp']], - factory: () => new TestComponent(), - template: (cmp: TestComponent, cm: boolean) => { - if (cm) { - const subTemplate = (ctx: any, cm: boolean) => { - if (cm) { - text(0); - } - textBinding(0, bind(ctx.$implicit)); - }; - container(0, subTemplate, undefined, ['testdir', '']); - } - cmp.testDir = loadDirective(0); - }, - directives: [TestDirective] - }); - } - - - it('should add embedded view into container', () => { - const fixture = new ComponentFixture(TestComponent); - expect(fixture.html).toEqual(''); - - const dir = fixture.component.testDir; - const childCtx = {$implicit: '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'); - - // run change-detection cycle with no template insertion / removal - fixture.update(); - 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) { + function embeddedTemplate(ctx: any, cm: boolean) { if (cm) { text(0); } - textBinding(0, interpolation1('Hello, ', ctx.name, '')); + textBinding(0, ctx.name); + } + + class DirectiveWithVCRef { + static ngDirectiveDef = defineDirective({ + type: DirectiveWithVCRef, + selectors: [['', 'vcref', '']], + factory: () => directiveInstance = new DirectiveWithVCRef(injectViewContainerRef()), + inputs: {tplRef: 'tplRef'} + }); + + tplRef: TemplateRef<{}>; + + constructor(public vcref: ViewContainerRef) {} + } + + function createView(s: string, index?: number) { + directiveInstance !.vcref.createEmbeddedView(directiveInstance !.tplRef, {name: s}, index); } /** - * before|Hello, {{name}}|after + * + * {{name}} + * + *

+ *

*/ - 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] - }); + function createTemplate() { + container(0, embeddedTemplate); + elementStart(1, 'p', ['vcref', '']); + elementEnd(); } - const fixture = new ComponentFixture(TestComponent); - expect(fixture.html).toEqual('before||after'); + function updateTemplate() { + const tplRef = getOrCreateTemplateRef(getOrCreateNodeInjectorForNode(load(0))); + elementProperty(1, 'tplRef', bind(tplRef)); + } - directiveInstance !.insertTpl({name: 'World'}); - expect(fixture.html).toEqual('before|Hello, World|after'); + describe('createEmbeddedView (incl. insert)', () => { + it('should work on elements', () => { + function createTemplate() { + container(0, embeddedTemplate); + elementStart(1, 'header', ['vcref', '']); + elementEnd(); + elementStart(2, 'footer'); + elementEnd(); + } - // run change-detection cycle with no template insertion / removal - fixture.update(); - expect(fixture.html).toEqual('before|Hello, World|after'); + const fixture = new TemplateFixture(createTemplate, updateTemplate, [DirectiveWithVCRef]); + expect(fixture.html).toEqual('
'); - directiveInstance !.remove(0); - expect(fixture.html).toEqual('before||after'); - }); + createView('A'); + fixture.update(); + expect(fixture.html).toEqual('
A
'); - it('should add embedded views at the right position in the DOM tree (ng-template next to other ng-template)', - () => { - let directiveInstances: TestDirective[] = []; + createView('B'); + createView('C'); + fixture.update(); + expect(fixture.html).toEqual('
ABC
'); - class TestDirective { - static ngDirectiveDef = defineDirective({ - type: TestDirective, - selectors: [['', 'testdir', '']], - factory: () => { - const instance = new TestDirective(injectViewContainerRef(), injectTemplateRef()); + createView('Y', 0); + fixture.update(); + expect(fixture.html).toEqual('
YABC
'); - directiveInstances.push(instance); + expect(() => { createView('Z', -1); }).toThrow(); + expect(() => { createView('Z', 5); }).toThrow(); + }); - return instance; + it('should work on components', () => { + class HeaderComponent { + static ngComponentDef = defineComponent({ + type: HeaderComponent, + selectors: [['header-cmp']], + factory: () => new HeaderComponent(), + template: (cmp: HeaderComponent, cm: boolean) => {} + }); + } + + function createTemplate() { + container(0, embeddedTemplate); + elementStart(1, 'header-cmp', ['vcref', '']); + elementEnd(); + elementStart(2, 'footer'); + elementEnd(); + } + + const fixture = new TemplateFixture( + createTemplate, updateTemplate, [HeaderComponent, DirectiveWithVCRef]); + expect(fixture.html).toEqual('
'); + + createView('A'); + fixture.update(); + expect(fixture.html).toEqual('A
'); + + createView('B'); + createView('C'); + fixture.update(); + expect(fixture.html).toEqual('ABC
'); + + createView('Y', 0); + fixture.update(); + expect(fixture.html).toEqual('YABC
'); + + expect(() => { createView('Z', -1); }).toThrow(); + expect(() => { createView('Z', 5); }).toThrow(); + }); + + it('should work on containers', () => { + function createTemplate() { + container(0, embeddedTemplate, undefined, ['vcref', '']); + elementStart(1, 'footer'); + elementEnd(); + } + + function updateTemplate() { + const tplRef = getOrCreateTemplateRef(getOrCreateNodeInjectorForNode(load(0))); + elementProperty(0, 'tplRef', bind(tplRef)); + containerRefreshStart(0); + if (embeddedViewStart(1)) { + elementStart(0, 'header'); + elementEnd(); + } + embeddedViewEnd(); + containerRefreshEnd(); + } + + const fixture = new TemplateFixture(createTemplate, updateTemplate, [DirectiveWithVCRef]); + expect(fixture.html).toEqual('
'); + + createView('A'); + fixture.update(); + expect(fixture.html).toEqual('
A
'); + + createView('B'); + createView('C'); + fixture.update(); + expect(fixture.html).toEqual('
ABC
'); + + createView('Y', 0); + fixture.update(); + expect(fixture.html).toEqual('
YABC
'); + + expect(() => { createView('Z', -1); }).toThrow(); + expect(() => { createView('Z', 5); }).toThrow(); + }); + + it('should add embedded views at the right position in the DOM tree (ng-template next to other ng-template)', + () => { + let directiveInstances: TestDirective[] = []; + + class TestDirective { + static ngDirectiveDef = defineDirective({ + type: TestDirective, + selectors: [['', 'testdir', '']], + factory: () => { + const instance = new TestDirective(injectViewContainerRef(), injectTemplateRef()); + + directiveInstances.push(instance); + + return instance; + } + }); + + constructor(private _vcRef: ViewContainerRef, private _tplRef: TemplateRef<{}>) {} + + insertTpl(ctx: {}) { this._vcRef.createEmbeddedView(this._tplRef, ctx); } + + remove(index?: number) { this._vcRef.remove(index); } } - }); - constructor(private _vcRef: ViewContainerRef, private _tplRef: TemplateRef<{}>) {} - - insertTpl(ctx: {}) { this._vcRef.createEmbeddedView(this._tplRef, ctx); } - - remove(index?: number) { this._vcRef.remove(index); } - } - - function EmbeddedTemplateA(ctx: any, cm: boolean) { - if (cm) { - text(0, 'A'); - } - } - - function EmbeddedTemplateB(ctx: any, cm: boolean) { - if (cm) { - text(0, 'B'); - } - } - - /** - * before| - * A - * B - * |after - */ - class TestComponent { - testDir: TestDirective; - static ngComponentDef = defineComponent({ - type: TestComponent, - selectors: [['test-cmp']], - factory: () => new TestComponent(), - template: (cmp: TestComponent, cm: boolean) => { + function EmbeddedTemplateA(ctx: any, cm: boolean) { if (cm) { - text(0, 'before|'); - container(1, EmbeddedTemplateA, undefined, ['testdir', '']); - container(2, EmbeddedTemplateB, undefined, ['testdir', '']); - text(3, '|after'); + text(0, 'A'); } - }, - directives: [TestDirective] - }); - } + } - const fixture = new ComponentFixture(TestComponent); - expect(fixture.html).toEqual('before||after'); - - directiveInstances ![1].insertTpl({}); - expect(fixture.html).toEqual('before|B|after'); - - directiveInstances ![0].insertTpl({}); - expect(fixture.html).toEqual('before|AB|after'); - }); - - - it('should add embedded views at the right position in the DOM tree (ng-template next to a JS block)', - () => { - 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 EmbeddedTemplateA(ctx: any, cm: boolean) { - if (cm) { - text(0, 'A'); - } - } - - /** - * before| - * A - * % if (condition) { - * B - * } - * |after - */ - class TestComponent { - condition = false; - testDir: TestDirective; - static ngComponentDef = defineComponent({ - type: TestComponent, - selectors: [['test-cmp']], - factory: () => new TestComponent(), - template: (cmp: TestComponent, cm: boolean) => { + function EmbeddedTemplateB(ctx: any, cm: boolean) { if (cm) { - text(0, 'before|'); - container(1, EmbeddedTemplateA, undefined, ['testdir', '']); - container(2); - text(3, '|after'); + text(0, 'B'); } - containerRefreshStart(2); - { - if (cmp.condition) { - let cm1 = embeddedViewStart(0); + } + + /** + * before| + * A + * B + * |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, EmbeddedTemplateA, undefined, ['testdir', '']); + container(2, EmbeddedTemplateB, undefined, ['testdir', '']); + text(3, '|after'); + } + }, + directives: [TestDirective] + }); + } + + const fixture = new ComponentFixture(TestComponent); + expect(fixture.html).toEqual('before||after'); + + directiveInstances ![1].insertTpl({}); + expect(fixture.html).toEqual('before|B|after'); + + directiveInstances ![0].insertTpl({}); + expect(fixture.html).toEqual('before|AB|after'); + }); + + + it('should add embedded views at the right position in the DOM tree (ng-template next to a JS block)', + () => { + 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 EmbeddedTemplateA(ctx: any, cm: boolean) { + if (cm) { + text(0, 'A'); + } + } + + /** + * before| + * A + * % if (condition) { + * B + * } + * |after + */ + class TestComponent { + condition = false; + 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, EmbeddedTemplateA, undefined, ['testdir', '']); + container(2); + text(3, '|after'); + } + containerRefreshStart(2); { - if (cm1) { - text(0, 'B'); + if (cmp.condition) { + let cm1 = embeddedViewStart(0); + { + if (cm1) { + text(0, 'B'); + } + } + embeddedViewEnd(); } } - embeddedViewEnd(); - } - } - containerRefreshEnd(); - }, - directives: [TestDirective] + containerRefreshEnd(); + }, + directives: [TestDirective] + }); + } + + const fixture = new ComponentFixture(TestComponent); + expect(fixture.html).toEqual('before||after'); + + fixture.component.condition = true; + fixture.update(); + expect(fixture.html).toEqual('before|B|after'); + + directiveInstance !.insertTpl({}); + expect(fixture.html).toEqual('before|AB|after'); + + fixture.component.condition = false; + fixture.update(); + expect(fixture.html).toEqual('before|A|after'); + + directiveInstance !.insertTpl({}); + expect(fixture.html).toEqual('before|AA|after'); + + fixture.component.condition = true; + fixture.update(); + expect(fixture.html).toEqual('before|AAB|after'); }); - } + }); - const fixture = new ComponentFixture(TestComponent); - expect(fixture.html).toEqual('before||after'); + describe('detach', () => { + it('should detach the right embedded view when an index is specified', () => { + const fixture = new TemplateFixture(createTemplate, updateTemplate, [DirectiveWithVCRef]); + createView('A'); + createView('B'); + createView('C'); + createView('D'); + createView('E'); + fixture.update(); + expect(fixture.html).toEqual('

ABCDE'); - fixture.component.condition = true; - fixture.update(); - expect(fixture.html).toEqual('before|B|after'); + directiveInstance !.vcref.detach(3); + fixture.update(); + expect(fixture.html).toEqual('

ABCE'); - directiveInstance !.insertTpl({}); - expect(fixture.html).toEqual('before|AB|after'); + directiveInstance !.vcref.detach(0); + fixture.update(); + expect(fixture.html).toEqual('

BCE'); - fixture.component.condition = false; - fixture.update(); - expect(fixture.html).toEqual('before|A|after'); + expect(() => { directiveInstance !.vcref.detach(-1); }).toThrow(); + expect(() => { directiveInstance !.vcref.detach(42); }).toThrow(); + }); - directiveInstance !.insertTpl({}); - expect(fixture.html).toEqual('before|AA|after'); - fixture.component.condition = true; - fixture.update(); - expect(fixture.html).toEqual('before|AAB|after'); - }); + it('should detach the last embedded view when no index is specified', () => { + const fixture = new TemplateFixture(createTemplate, updateTemplate, [DirectiveWithVCRef]); + createView('A'); + createView('B'); + createView('C'); + createView('D'); + createView('E'); + fixture.update(); + expect(fixture.html).toEqual('

ABCDE'); + + directiveInstance !.vcref.detach(); + fixture.update(); + expect(fixture.html).toEqual('

ABCD'); + }); + }); + + describe('length', () => { + it('should return the number of embedded views', () => { + const fixture = new TemplateFixture(createTemplate, updateTemplate, [DirectiveWithVCRef]); + expect(directiveInstance !.vcref.length).toEqual(0); + + createView('A'); + createView('B'); + createView('C'); + fixture.update(); + expect(directiveInstance !.vcref.length).toEqual(3); + + directiveInstance !.vcref.detach(1); + fixture.update(); + expect(directiveInstance !.vcref.length).toEqual(2); + + directiveInstance !.vcref.clear(); + fixture.update(); + expect(directiveInstance !.vcref.length).toEqual(0); + }); + }); + + describe('get and indexOf', () => { + it('should retrieve a ViewRef from its index, and vice versa', () => { + const fixture = new TemplateFixture(createTemplate, updateTemplate, [DirectiveWithVCRef]); + createView('A'); + createView('B'); + createView('C'); + fixture.update(); + + let viewRef = directiveInstance !.vcref.get(0); + expect(directiveInstance !.vcref.indexOf(viewRef !)).toEqual(0); + + viewRef = directiveInstance !.vcref.get(1); + expect(directiveInstance !.vcref.indexOf(viewRef !)).toEqual(1); + + viewRef = directiveInstance !.vcref.get(2); + expect(directiveInstance !.vcref.indexOf(viewRef !)).toEqual(2); + }); + + it('should handle out of bounds cases', () => { + const fixture = new TemplateFixture(createTemplate, updateTemplate, [DirectiveWithVCRef]); + createView('A'); + fixture.update(); + + expect(directiveInstance !.vcref.get(-1)).toBeNull(); + expect(directiveInstance !.vcref.get(42)).toBeNull(); + + const viewRef = directiveInstance !.vcref.get(0); + directiveInstance !.vcref.remove(0); + expect(directiveInstance !.vcref.indexOf(viewRef !)).toEqual(-1); + }); + }); + + describe('move', () => { + it('should move embedded views and associated DOM nodes without recreating them', () => { + const fixture = new TemplateFixture(createTemplate, updateTemplate, [DirectiveWithVCRef]); + createView('A'); + createView('B'); + createView('C'); + fixture.update(); + expect(fixture.html).toEqual('

ABC'); + + // The DOM is manually modified here to ensure that the text node is actually moved + fixture.hostElement.childNodes[1].nodeValue = '**A**'; + expect(fixture.html).toEqual('

**A**BC'); + + let viewRef = directiveInstance !.vcref.get(0); + directiveInstance !.vcref.move(viewRef !, 2); + fixture.update(); + expect(fixture.html).toEqual('

BC**A**'); + + directiveInstance !.vcref.move(viewRef !, 0); + fixture.update(); + expect(fixture.html).toEqual('

**A**BC'); + + directiveInstance !.vcref.move(viewRef !, 1); + fixture.update(); + expect(fixture.html).toEqual('

B**A**C'); + + expect(() => { directiveInstance !.vcref.move(viewRef !, -1); }).toThrow(); + expect(() => { directiveInstance !.vcref.move(viewRef !, 42); }).toThrow(); + }); + }); + }); });