feat(ivy): implement some of the ViewContainerRef API (#23189)

PR Close #23189
This commit is contained in:
Marc Laval 2018-04-05 10:29:47 +02:00 committed by Igor Minar
parent 30a6861fd0
commit f4c56f4931
5 changed files with 456 additions and 320 deletions

View File

@ -40,6 +40,12 @@ export function assertLessThan<T>(actual: T, expected: T, msg: string) {
}
}
export function assertGreaterThan<T>(actual: T, expected: T, msg: string) {
if (actual <= expected) {
throwError(msg);
}
}
export function assertNull<T>(actual: T, msg: string) {
if (actual != null) {
throwError(msg);

View File

@ -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<C>(templateRef: viewEngine_TemplateRef<C>, context?: C, index?: number):
viewEngine_EmbeddedViewRef<C> {
const viewRef = templateRef.createEmbeddedView(context || <any>{});
this.insert(viewRef, index);
return viewRef;
}
createComponent<C>(
componentFactory: viewEngine_ComponentFactory<C>, index?: number|undefined,
injector?: Injector|undefined, projectableNodes?: any[][]|undefined,
ngModule?: viewEngine_NgModuleRef<any>|undefined): viewEngine_ComponentRef<C> {
throw notImplemented();
}
insert(viewRef: viewEngine_ViewRef, index?: number): viewEngine_ViewRef {
const lViewNode = (viewRef as EmbeddedViewRef<any>)._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;
}

View File

@ -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

View File

@ -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);
}
}

View File

@ -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<any>, ) {}
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<TestDirective>(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.');
}
}
/**
* <ng-template #tpl>From a template<ng-template>
* before
* <div directive [tpl]="tpl"></div>
* 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<div testdir=""></div>after');
directiveInstance !.insertTpl();
expect(fixture.html).toEqual('before<div testdir=""></div>From a template.after');
// run change-detection cycle with no template insertion / removal
fixture.update();
expect(fixture.html).toEqual('before<div testdir=""></div>From a template.after');
directiveInstance !.insertTpl();
expect(fixture.html)
.toEqual('before<div testdir=""></div>From a template.From a template.after');
directiveInstance !.clear();
expect(fixture.html).toEqual('before<div testdir=""></div>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|<ng-template directive>Hello, {{name}}<ng-template>|after
* <ng-template #foo>
* {{name}}
* </ng-template>
* <p vcref="" [tplRef]="foo">
* </p>
*/
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('<header vcref=""></header><footer></footer>');
directiveInstance !.remove(0);
expect(fixture.html).toEqual('before||after');
});
createView('A');
fixture.update();
expect(fixture.html).toEqual('<header vcref=""></header>A<footer></footer>');
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('<header vcref=""></header>ABC<footer></footer>');
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('<header vcref=""></header>YABC<footer></footer>');
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('<header-cmp vcref=""></header-cmp><footer></footer>');
createView('A');
fixture.update();
expect(fixture.html).toEqual('<header-cmp vcref=""></header-cmp>A<footer></footer>');
createView('B');
createView('C');
fixture.update();
expect(fixture.html).toEqual('<header-cmp vcref=""></header-cmp>ABC<footer></footer>');
createView('Y', 0);
fixture.update();
expect(fixture.html).toEqual('<header-cmp vcref=""></header-cmp>YABC<footer></footer>');
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('<header></header><footer></footer>');
createView('A');
fixture.update();
expect(fixture.html).toEqual('<header></header>A<footer></footer>');
createView('B');
createView('C');
fixture.update();
expect(fixture.html).toEqual('<header></header>ABC<footer></footer>');
createView('Y', 0);
fixture.update();
expect(fixture.html).toEqual('<header></header>YABC<footer></footer>');
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|
* <ng-template directive>A<ng-template>
* <ng-template directive>B<ng-template>
* |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|
* <ng-template directive>A<ng-template>
* % 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|
* <ng-template directive>A<ng-template>
* <ng-template directive>B<ng-template>
* |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|
* <ng-template directive>A<ng-template>
* % 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('<p vcref=""></p>ABCDE');
fixture.component.condition = true;
fixture.update();
expect(fixture.html).toEqual('before|B|after');
directiveInstance !.vcref.detach(3);
fixture.update();
expect(fixture.html).toEqual('<p vcref=""></p>ABCE');
directiveInstance !.insertTpl({});
expect(fixture.html).toEqual('before|AB|after');
directiveInstance !.vcref.detach(0);
fixture.update();
expect(fixture.html).toEqual('<p vcref=""></p>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('<p vcref=""></p>ABCDE');
directiveInstance !.vcref.detach();
fixture.update();
expect(fixture.html).toEqual('<p vcref=""></p>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('<p vcref=""></p>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('<p vcref=""></p>**A**BC');
let viewRef = directiveInstance !.vcref.get(0);
directiveInstance !.vcref.move(viewRef !, 2);
fixture.update();
expect(fixture.html).toEqual('<p vcref=""></p>BC**A**');
directiveInstance !.vcref.move(viewRef !, 0);
fixture.update();
expect(fixture.html).toEqual('<p vcref=""></p>**A**BC');
directiveInstance !.vcref.move(viewRef !, 1);
fixture.update();
expect(fixture.html).toEqual('<p vcref=""></p>B**A**C');
expect(() => { directiveInstance !.vcref.move(viewRef !, -1); }).toThrow();
expect(() => { directiveInstance !.vcref.move(viewRef !, 42); }).toThrow();
});
});
});
});