feat(ivy): implement some of the ViewContainerRef API (#23189)
PR Close #23189
This commit is contained in:
@ -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) {
|
export function assertNull<T>(actual: T, msg: string) {
|
||||||
if (actual != null) {
|
if (actual != null) {
|
||||||
throwError(msg);
|
throwError(msg);
|
||||||
|
@ -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 {EmbeddedViewRef as viewEngine_EmbeddedViewRef, ViewRef as viewEngine_ViewRef} from '../linker/view_ref';
|
||||||
import {Type} from '../type';
|
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 {addToViewTree, assertPreviousIsParent, createLContainer, createLNodeObject, getDirectiveInstance, getPreviousOrParentNode, getRenderer, isComponent, renderEmbeddedTemplate, resolveDirective} from './instructions';
|
||||||
import {ComponentTemplate, DirectiveDef} from './interfaces/definition';
|
import {ComponentTemplate, DirectiveDef} from './interfaces/definition';
|
||||||
import {LInjector} from './interfaces/injector';
|
import {LInjector} from './interfaces/injector';
|
||||||
@ -608,26 +608,31 @@ class ViewContainerRef implements viewEngine_ViewContainerRef {
|
|||||||
this.remove(0);
|
this.remove(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get(index: number): viewEngine_ViewRef|null { return this._viewRefs[index] || null; }
|
get(index: number): viewEngine_ViewRef|null { return this._viewRefs[index] || null; }
|
||||||
|
|
||||||
get length(): number {
|
get length(): number {
|
||||||
const lContainer = this._lContainerNode.data;
|
const lContainer = this._lContainerNode.data;
|
||||||
return lContainer.views.length;
|
return lContainer.views.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
createEmbeddedView<C>(templateRef: viewEngine_TemplateRef<C>, context?: C, index?: number):
|
createEmbeddedView<C>(templateRef: viewEngine_TemplateRef<C>, context?: C, index?: number):
|
||||||
viewEngine_EmbeddedViewRef<C> {
|
viewEngine_EmbeddedViewRef<C> {
|
||||||
const viewRef = templateRef.createEmbeddedView(context || <any>{});
|
const viewRef = templateRef.createEmbeddedView(context || <any>{});
|
||||||
this.insert(viewRef, index);
|
this.insert(viewRef, index);
|
||||||
return viewRef;
|
return viewRef;
|
||||||
}
|
}
|
||||||
|
|
||||||
createComponent<C>(
|
createComponent<C>(
|
||||||
componentFactory: viewEngine_ComponentFactory<C>, index?: number|undefined,
|
componentFactory: viewEngine_ComponentFactory<C>, index?: number|undefined,
|
||||||
injector?: Injector|undefined, projectableNodes?: any[][]|undefined,
|
injector?: Injector|undefined, projectableNodes?: any[][]|undefined,
|
||||||
ngModule?: viewEngine_NgModuleRef<any>|undefined): viewEngine_ComponentRef<C> {
|
ngModule?: viewEngine_NgModuleRef<any>|undefined): viewEngine_ComponentRef<C> {
|
||||||
throw notImplemented();
|
throw notImplemented();
|
||||||
}
|
}
|
||||||
|
|
||||||
insert(viewRef: viewEngine_ViewRef, index?: number): viewEngine_ViewRef {
|
insert(viewRef: viewEngine_ViewRef, index?: number): viewEngine_ViewRef {
|
||||||
const lViewNode = (viewRef as EmbeddedViewRef<any>)._lViewNode;
|
const lViewNode = (viewRef as EmbeddedViewRef<any>)._lViewNode;
|
||||||
const adjustedIdx = this._adjustAndAssertIndex(index);
|
const adjustedIdx = this._adjustIndex(index);
|
||||||
|
|
||||||
insertView(this._lContainerNode, lViewNode, adjustedIdx);
|
insertView(this._lContainerNode, lViewNode, adjustedIdx);
|
||||||
// invalidate cache of next sibling RNode (we do similar operation in the containerRefreshEnd
|
// invalidate cache of next sibling RNode (we do similar operation in the containerRefreshEnd
|
||||||
@ -653,23 +658,37 @@ class ViewContainerRef implements viewEngine_ViewContainerRef {
|
|||||||
}
|
}
|
||||||
return viewRef;
|
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) {
|
if (index == null) {
|
||||||
index = this._lContainerNode.data.views.length;
|
return this._lContainerNode.data.views.length + shift;
|
||||||
} else {
|
}
|
||||||
|
if (ngDevMode) {
|
||||||
|
assertGreaterThan(index, -1, 'index must be positive');
|
||||||
// +1 because it's legal to insert at the end.
|
// +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;
|
return index;
|
||||||
}
|
}
|
||||||
|
@ -311,6 +311,7 @@ export function removeView(container: LContainerNode, removeIndex: number): LVie
|
|||||||
setViewNext(views[removeIndex - 1], viewNode.next);
|
setViewNext(views[removeIndex - 1], viewNode.next);
|
||||||
}
|
}
|
||||||
views.splice(removeIndex, 1);
|
views.splice(removeIndex, 1);
|
||||||
|
viewNode.next = null;
|
||||||
destroyViewTree(viewNode.data);
|
destroyViewTree(viewNode.data);
|
||||||
addRemoveViewFromContainer(container, viewNode, false);
|
addRemoveViewFromContainer(container, viewNode, false);
|
||||||
// Notify query that view has been removed
|
// Notify query that view has been removed
|
||||||
|
@ -48,6 +48,8 @@ function noop() {}
|
|||||||
*/
|
*/
|
||||||
export class TemplateFixture extends BaseFixture {
|
export class TemplateFixture extends BaseFixture {
|
||||||
hostNode: LElementNode;
|
hostNode: LElementNode;
|
||||||
|
private _directiveDefs: DirectiveDefList|null;
|
||||||
|
private _pipeDefs: PipeDefList|null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param createBlock Instructions which go into the creation block:
|
* @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:
|
* @param updateBlock Optional instructions which go after the creation block:
|
||||||
* `if (creationMode) { ... } __here__`.
|
* `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();
|
super();
|
||||||
this.updateBlock = updateBlock || function() {};
|
this._directiveDefs = toDefs(directives, extractDirectiveDef);
|
||||||
|
this._pipeDefs = toDefs(pipes, extractPipeDef);
|
||||||
this.hostNode = renderTemplate(this.hostElement, (ctx: any, cm: boolean) => {
|
this.hostNode = renderTemplate(this.hostElement, (ctx: any, cm: boolean) => {
|
||||||
if (cm) {
|
if (cm) {
|
||||||
this.createBlock();
|
this.createBlock();
|
||||||
}
|
}
|
||||||
this.updateBlock();
|
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 {
|
update(updateBlock?: () => void): void {
|
||||||
renderTemplate(
|
renderTemplate(
|
||||||
this.hostNode.native, updateBlock || this.updateBlock, null !, domRendererFactory3,
|
this.hostNode.native, updateBlock || this.updateBlock, null !, domRendererFactory3,
|
||||||
this.hostNode);
|
this.hostNode, this._directiveDefs, this._pipeDefs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,345 +6,450 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* 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 {getOrCreateNodeInjectorForNode, getOrCreateTemplateRef} from '../../src/render3/di';
|
||||||
import {defineComponent, defineDirective, injectTemplateRef, injectViewContainerRef} from '../../src/render3/index';
|
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 {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', () => {
|
describe('ViewContainerRef', () => {
|
||||||
class TestDirective {
|
describe('API', () => {
|
||||||
constructor(public viewContainer: ViewContainerRef, public template: TemplateRef<any>, ) {}
|
let directiveInstance: DirectiveWithVCRef|null;
|
||||||
|
|
||||||
static ngDirectiveDef = defineDirective({
|
beforeEach(() => { directiveInstance = null; });
|
||||||
type: TestDirective,
|
|
||||||
selectors: [['', 'testdir', '']],
|
|
||||||
factory: () => new TestDirective(injectViewContainerRef(), injectTemplateRef(), ),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class TestComponent {
|
function embeddedTemplate(ctx: any, cm: boolean) {
|
||||||
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) {
|
|
||||||
if (cm) {
|
if (cm) {
|
||||||
text(0);
|
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 {
|
function createTemplate() {
|
||||||
testDir: TestDirective;
|
container(0, embeddedTemplate);
|
||||||
static ngComponentDef = defineComponent({
|
elementStart(1, 'p', ['vcref', '']);
|
||||||
type: TestComponent,
|
elementEnd();
|
||||||
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);
|
function updateTemplate() {
|
||||||
expect(fixture.html).toEqual('before||after');
|
const tplRef = getOrCreateTemplateRef(getOrCreateNodeInjectorForNode(load(0)));
|
||||||
|
elementProperty(1, 'tplRef', bind(tplRef));
|
||||||
|
}
|
||||||
|
|
||||||
directiveInstance !.insertTpl({name: 'World'});
|
describe('createEmbeddedView (incl. insert)', () => {
|
||||||
expect(fixture.html).toEqual('before|Hello, World|after');
|
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
|
const fixture = new TemplateFixture(createTemplate, updateTemplate, [DirectiveWithVCRef]);
|
||||||
fixture.update();
|
expect(fixture.html).toEqual('<header vcref=""></header><footer></footer>');
|
||||||
expect(fixture.html).toEqual('before|Hello, World|after');
|
|
||||||
|
|
||||||
directiveInstance !.remove(0);
|
createView('A');
|
||||||
expect(fixture.html).toEqual('before||after');
|
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)',
|
createView('B');
|
||||||
() => {
|
createView('C');
|
||||||
let directiveInstances: TestDirective[] = [];
|
fixture.update();
|
||||||
|
expect(fixture.html).toEqual('<header vcref=""></header>ABC<footer></footer>');
|
||||||
|
|
||||||
class TestDirective {
|
createView('Y', 0);
|
||||||
static ngDirectiveDef = defineDirective({
|
fixture.update();
|
||||||
type: TestDirective,
|
expect(fixture.html).toEqual('<header vcref=""></header>YABC<footer></footer>');
|
||||||
selectors: [['', 'testdir', '']],
|
|
||||||
factory: () => {
|
|
||||||
const instance = new TestDirective(injectViewContainerRef(), injectTemplateRef());
|
|
||||||
|
|
||||||
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<{}>) {}
|
function EmbeddedTemplateA(ctx: any, cm: boolean) {
|
||||||
|
|
||||||
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) => {
|
|
||||||
if (cm) {
|
if (cm) {
|
||||||
text(0, 'before|');
|
text(0, 'A');
|
||||||
container(1, EmbeddedTemplateA, undefined, ['testdir', '']);
|
|
||||||
container(2, EmbeddedTemplateB, undefined, ['testdir', '']);
|
|
||||||
text(3, '|after');
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
directives: [TestDirective]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const fixture = new ComponentFixture(TestComponent);
|
function EmbeddedTemplateB(ctx: any, cm: boolean) {
|
||||||
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) {
|
if (cm) {
|
||||||
text(0, 'before|');
|
text(0, 'B');
|
||||||
container(1, EmbeddedTemplateA, undefined, ['testdir', '']);
|
|
||||||
container(2);
|
|
||||||
text(3, '|after');
|
|
||||||
}
|
}
|
||||||
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) {
|
if (cmp.condition) {
|
||||||
text(0, 'B');
|
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);
|
describe('detach', () => {
|
||||||
expect(fixture.html).toEqual('before||after');
|
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;
|
directiveInstance !.vcref.detach(3);
|
||||||
fixture.update();
|
fixture.update();
|
||||||
expect(fixture.html).toEqual('before|B|after');
|
expect(fixture.html).toEqual('<p vcref=""></p>ABCE');
|
||||||
|
|
||||||
directiveInstance !.insertTpl({});
|
directiveInstance !.vcref.detach(0);
|
||||||
expect(fixture.html).toEqual('before|AB|after');
|
fixture.update();
|
||||||
|
expect(fixture.html).toEqual('<p vcref=""></p>BCE');
|
||||||
|
|
||||||
fixture.component.condition = false;
|
expect(() => { directiveInstance !.vcref.detach(-1); }).toThrow();
|
||||||
fixture.update();
|
expect(() => { directiveInstance !.vcref.detach(42); }).toThrow();
|
||||||
expect(fixture.html).toEqual('before|A|after');
|
});
|
||||||
|
|
||||||
directiveInstance !.insertTpl({});
|
|
||||||
expect(fixture.html).toEqual('before|AA|after');
|
|
||||||
|
|
||||||
fixture.component.condition = true;
|
it('should detach the last embedded view when no index is specified', () => {
|
||||||
fixture.update();
|
const fixture = new TemplateFixture(createTemplate, updateTemplate, [DirectiveWithVCRef]);
|
||||||
expect(fixture.html).toEqual('before|AAB|after');
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user