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) { export function assertNull<T>(actual: T, msg: string) {
if (actual != null) { if (actual != null) {
throwError(msg); 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 {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;
} }

View File

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

View File

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

View File

@ -6,188 +6,167 @@
* 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) { if (cm) {
text(0); text(0);
} }
textBinding(0, bind(ctx.$implicit)); textBinding(0, ctx.name);
};
container(0, subTemplate, undefined, ['testdir', '']);
}
cmp.testDir = loadDirective<TestDirective>(0);
},
directives: [TestDirective]
});
} }
class DirectiveWithVCRef {
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({ static ngDirectiveDef = defineDirective({
type: TestDirective, type: DirectiveWithVCRef,
selectors: [['', 'testdir', '']], selectors: [['', 'vcref', '']],
factory: () => directiveInstance = new TestDirective(injectViewContainerRef()), factory: () => directiveInstance = new DirectiveWithVCRef(injectViewContainerRef()),
inputs: {tpl: 'tpl'} inputs: {tplRef: 'tplRef'}
}); });
tpl: TemplateRef<{}>; tplRef: TemplateRef<{}>;
constructor(private _vcRef: ViewContainerRef) {} constructor(public vcref: ViewContainerRef) {}
insertTpl(ctx?: {}) { this._vcRef.createEmbeddedView(this.tpl, ctx); }
clear() { this._vcRef.clear(); }
} }
function EmbeddedTemplate(ctx: any, cm: boolean) { function createView(s: string, index?: number) {
if (cm) { directiveInstance !.vcref.createEmbeddedView(directiveInstance !.tplRef, {name: s}, index);
text(0, 'From a template.');
}
} }
/** /**
* <ng-template #tpl>From a template<ng-template> * <ng-template #foo>
* before * {{name}}
* <div directive [tpl]="tpl"></div> * </ng-template>
* after * <p vcref="" [tplRef]="foo">
* </p>
*/ */
class TestComponent { function createTemplate() {
testDir: TestDirective; container(0, embeddedTemplate);
static ngComponentDef = defineComponent({ elementStart(1, 'p', ['vcref', '']);
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(); 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]
});
} }
function updateTemplate() {
const tplRef = getOrCreateTemplateRef(getOrCreateNodeInjectorForNode(load(0)));
elementProperty(1, 'tplRef', bind(tplRef));
}
const fixture = new ComponentFixture(TestComponent); describe('createEmbeddedView (incl. insert)', () => {
expect(fixture.html).toEqual('before<div testdir=""></div>after'); it('should work on elements', () => {
function createTemplate() {
container(0, embeddedTemplate);
elementStart(1, 'header', ['vcref', '']);
elementEnd();
elementStart(2, 'footer');
elementEnd();
}
directiveInstance !.insertTpl(); const fixture = new TemplateFixture(createTemplate, updateTemplate, [DirectiveWithVCRef]);
expect(fixture.html).toEqual('before<div testdir=""></div>From a template.after'); expect(fixture.html).toEqual('<header vcref=""></header><footer></footer>');
// run change-detection cycle with no template insertion / removal createView('A');
fixture.update(); fixture.update();
expect(fixture.html).toEqual('before<div testdir=""></div>From a template.after'); expect(fixture.html).toEqual('<header vcref=""></header>A<footer></footer>');
directiveInstance !.insertTpl(); createView('B');
expect(fixture.html) createView('C');
.toEqual('before<div testdir=""></div>From a template.From a template.after'); fixture.update();
expect(fixture.html).toEqual('<header vcref=""></header>ABC<footer></footer>');
directiveInstance !.clear(); createView('Y', 0);
expect(fixture.html).toEqual('before<div testdir=""></div>after'); fixture.update();
expect(fixture.html).toEqual('<header vcref=""></header>YABC<footer></footer>');
expect(() => { createView('Z', -1); }).toThrow();
expect(() => { createView('Z', 5); }).toThrow();
}); });
it('should add embedded view into a view container on ng-template', () => { it('should work on components', () => {
let directiveInstance: TestDirective; class HeaderComponent {
class TestDirective {
static ngDirectiveDef = defineDirective({
type: TestDirective,
selectors: [['', 'testdir', '']],
factory: () => directiveInstance =
new TestDirective(injectViewContainerRef(), injectTemplateRef())
});
constructor(private _vcRef: ViewContainerRef, private _tplRef: TemplateRef<{}>) {}
insertTpl(ctx: {}) { this._vcRef.createEmbeddedView(this._tplRef, ctx); }
remove(index?: number) { this._vcRef.remove(index); }
}
function EmbeddedTemplate(ctx: any, cm: boolean) {
if (cm) {
text(0);
}
textBinding(0, interpolation1('Hello, ', ctx.name, ''));
}
/**
* before|<ng-template directive>Hello, {{name}}<ng-template>|after
*/
class TestComponent {
testDir: TestDirective;
static ngComponentDef = defineComponent({ static ngComponentDef = defineComponent({
type: TestComponent, type: HeaderComponent,
selectors: [['test-cmp']], selectors: [['header-cmp']],
factory: () => new TestComponent(), factory: () => new HeaderComponent(),
template: (cmp: TestComponent, cm: boolean) => { template: (cmp: HeaderComponent, cm: boolean) => {}
if (cm) {
text(0, 'before|');
container(1, EmbeddedTemplate, undefined, ['testdir', '']);
text(2, '|after');
}
},
directives: [TestDirective]
}); });
} }
const fixture = new ComponentFixture(TestComponent); function createTemplate() {
expect(fixture.html).toEqual('before||after'); container(0, embeddedTemplate);
elementStart(1, 'header-cmp', ['vcref', '']);
elementEnd();
elementStart(2, 'footer');
elementEnd();
}
directiveInstance !.insertTpl({name: 'World'}); const fixture = new TemplateFixture(
expect(fixture.html).toEqual('before|Hello, World|after'); createTemplate, updateTemplate, [HeaderComponent, DirectiveWithVCRef]);
expect(fixture.html).toEqual('<header-cmp vcref=""></header-cmp><footer></footer>');
// run change-detection cycle with no template insertion / removal createView('A');
fixture.update(); fixture.update();
expect(fixture.html).toEqual('before|Hello, World|after'); expect(fixture.html).toEqual('<header-cmp vcref=""></header-cmp>A<footer></footer>');
directiveInstance !.remove(0); createView('B');
expect(fixture.html).toEqual('before||after'); 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)', it('should add embedded views at the right position in the DOM tree (ng-template next to other ng-template)',
@ -347,4 +326,130 @@ describe('ViewContainerRef', () => {
fixture.update(); fixture.update();
expect(fixture.html).toEqual('before|AAB|after'); expect(fixture.html).toEqual('before|AAB|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');
directiveInstance !.vcref.detach(3);
fixture.update();
expect(fixture.html).toEqual('<p vcref=""></p>ABCE');
directiveInstance !.vcref.detach(0);
fixture.update();
expect(fixture.html).toEqual('<p vcref=""></p>BCE');
expect(() => { directiveInstance !.vcref.detach(-1); }).toThrow();
expect(() => { directiveInstance !.vcref.detach(42); }).toThrow();
});
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();
});
});
});
}); });