/** * @license * Copyright Google Inc. All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import {defineComponent} from '../../src/render3/index'; import {NO_CHANGE, bind, bind1, bind2, bind3, bind4, bind5, bind6, bind7, bind8, bindV, componentRefresh, container, containerRefreshEnd, containerRefreshStart, elementAttribute, elementClass, elementEnd, elementProperty, elementStart, elementStyle, memory, projection, projectionDef, text, textBinding, viewEnd, viewStart} from '../../src/render3/instructions'; import {containerEl, renderToHtml} from './render_util'; describe('render3 integration test', () => { describe('render', () => { it('should render basic template', () => { expect(renderToHtml(Template, {})).toEqual('Greetings'); function Template(ctx: any, cm: boolean) { if (cm) { elementStart(0, 'span', ['title', 'Hello']); { text(1, 'Greetings'); } elementEnd(); } } }); it('should render and update basic "Hello, World" template', () => { expect(renderToHtml(Template, 'World')).toEqual('

Hello, World!

'); expect(renderToHtml(Template, 'New World')).toEqual('

Hello, New World!

'); function Template(name: string, cm: boolean) { if (cm) { elementStart(0, 'h1'); { text(1); } elementEnd(); } textBinding(1, bind1('Hello, ', name, '!')); } }); }); describe('text bindings', () => { it('should render "undefined" as "" when used with `bind()`', () => { function Template(name: string, cm: boolean) { if (cm) { text(0); } textBinding(0, bind(name)); } expect(renderToHtml(Template, 'benoit')).toEqual('benoit'); expect(renderToHtml(Template, undefined)).toEqual(''); }); it('should render "null" as "" when used with `bind()`', () => { function Template(name: string, cm: boolean) { if (cm) { text(0); } textBinding(0, bind(name)); } expect(renderToHtml(Template, 'benoit')).toEqual('benoit'); expect(renderToHtml(Template, null)).toEqual(''); }); it('should support creation-time values in text nodes', () => { function Template(value: string, cm: boolean) { if (cm) { text(0); } textBinding(0, cm ? value : NO_CHANGE); } expect(renderToHtml(Template, 'once')).toEqual('once'); expect(renderToHtml(Template, 'twice')).toEqual('once'); }); it('should support creation-time bindings in interpolations', () => { function Template(v: string, cm: boolean) { if (cm) { text(0); text(1); text(2); text(3); text(4); text(5); text(6); text(7); text(8); } textBinding(0, bind1('', cm ? v : NO_CHANGE, '|')); textBinding(1, bind2('', v, '_', cm ? v : NO_CHANGE, '|')); textBinding(2, bind3('', v, '_', v, '_', cm ? v : NO_CHANGE, '|')); textBinding(3, bind4('', v, '_', v, '_', v, '_', cm ? v : NO_CHANGE, '|')); textBinding(4, bind5('', v, '_', v, '_', v, '_', v, '_', cm ? v : NO_CHANGE, '|')); textBinding(5, bind6('', v, '_', v, '_', v, '_', v, '_', v, '_', cm ? v : NO_CHANGE, '|')); textBinding( 6, bind7('', v, '_', v, '_', v, '_', v, '_', v, '_', v, '_', cm ? v : NO_CHANGE, '|')); textBinding( 7, bind8( '', v, '_', v, '_', v, '_', v, '_', v, '_', v, '_', v, '_', cm ? v : NO_CHANGE, '|')); textBinding(8, bindV([ '', v, '_', v, '_', v, '_', v, '_', v, '_', v, '_', v, '_', v, '_', cm ? v : NO_CHANGE, '' ])); } expect(renderToHtml(Template, 'a')) .toEqual( 'a|a_a|a_a_a|a_a_a_a|a_a_a_a_a|a_a_a_a_a_a|a_a_a_a_a_a_a|a_a_a_a_a_a_a_a|a_a_a_a_a_a_a_a_a'); expect(renderToHtml(Template, 'A')) .toEqual( 'a|A_a|A_A_a|A_A_A_a|A_A_A_A_a|A_A_A_A_A_a|A_A_A_A_A_A_a|A_A_A_A_A_A_A_a|A_A_A_A_A_A_A_A_a'); }); }); describe('Siblings update', () => { it('should handle a flat list of static/bound text nodes', () => { function Template(name: string, cm: boolean) { if (cm) { text(0, 'Hello '); text(1); text(2, '!'); } textBinding(1, bind(name)); } expect(renderToHtml(Template, 'world')).toEqual('Hello world!'); expect(renderToHtml(Template, 'monde')).toEqual('Hello monde!'); }); it('should handle a list of static/bound text nodes as element children', () => { function Template(name: string, cm: boolean) { if (cm) { elementStart(0, 'b'); { text(1, 'Hello '); text(2); text(3, '!'); } elementEnd(); } textBinding(2, bind(name)); } expect(renderToHtml(Template, 'world')).toEqual('Hello world!'); expect(renderToHtml(Template, 'mundo')).toEqual('Hello mundo!'); }); it('should render/update text node as a child of a deep list of elements', () => { function Template(name: string, cm: boolean) { if (cm) { elementStart(0, 'b'); { elementStart(1, 'b'); { elementStart(2, 'b'); { elementStart(3, 'b'); { text(4); } elementEnd(); } elementEnd(); } elementEnd(); } elementEnd(); } textBinding(4, bind1('Hello ', name, '!')); } expect(renderToHtml(Template, 'world')).toEqual('Hello world!'); expect(renderToHtml(Template, 'mundo')).toEqual('Hello mundo!'); }); it('should update 2 sibling elements', () => { function Template(id: any, cm: boolean) { if (cm) { elementStart(0, 'b'); { elementStart(1, 'span'); elementEnd(); elementStart(2, 'span', ['class', 'foo']); {} elementEnd(); } elementEnd(); } elementAttribute(2, 'id', bind(id)); } expect(renderToHtml(Template, 'foo')) .toEqual(''); expect(renderToHtml(Template, 'bar')) .toEqual(''); }); it('should handle sibling text node after element with child text node', () => { function Template(ctx: any, cm: boolean) { if (cm) { elementStart(0, 'p'); { text(1, 'hello'); } elementEnd(); text(2, 'world'); } } expect(renderToHtml(Template, null)).toEqual('

hello

world'); }); }); describe('basic components', () => { class TodoComponent { value = ' one'; static ngComponentDef = defineComponent({ type: TodoComponent, tag: 'todo', template: function TodoTemplate(ctx: any, cm: boolean) { if (cm) { elementStart(0, 'p'); { text(1, 'Todo'); text(2); } elementEnd(); } textBinding(2, bind(ctx.value)); }, factory: () => new TodoComponent }); } it('should support a basic component template', () => { function Template(ctx: any, cm: boolean) { if (cm) { elementStart(0, TodoComponent); elementEnd(); } TodoComponent.ngComponentDef.h(1, 0); componentRefresh(1, 0); } expect(renderToHtml(Template, null)).toEqual('

Todo one

'); }); it('should support a component template with sibling', () => { function Template(ctx: any, cm: boolean) { if (cm) { elementStart(0, TodoComponent); elementEnd(); text(2, 'two'); } TodoComponent.ngComponentDef.h(1, 0); componentRefresh(1, 0); } expect(renderToHtml(Template, null)).toEqual('

Todo one

two'); }); it('should support a component template with component sibling', () => { /** * * */ function Template(ctx: any, cm: boolean) { if (cm) { elementStart(0, TodoComponent); elementEnd(); elementStart(2, TodoComponent); elementEnd(); } TodoComponent.ngComponentDef.h(1, 0); TodoComponent.ngComponentDef.h(3, 2); componentRefresh(1, 0); componentRefresh(3, 2); } expect(renderToHtml(Template, null)) .toEqual('

Todo one

Todo one

'); }); it('should support a component with binding on host element', () => { let cmptInstance: TodoComponentHostBinding|null; class TodoComponentHostBinding { title = 'one'; static ngComponentDef = defineComponent({ type: TodoComponentHostBinding, tag: 'todo', template: function TodoComponentHostBindingTemplate( ctx: TodoComponentHostBinding, cm: boolean) { if (cm) { text(0); } textBinding(0, bind(ctx.title)); }, factory: () => cmptInstance = new TodoComponentHostBinding, hostBindings: function(directiveIndex: number, elementIndex: number): void { // host bindings elementProperty( elementIndex, 'title', bind(memory(directiveIndex).title)); } }); } function Template(ctx: any, cm: boolean) { if (cm) { elementStart(0, TodoComponentHostBinding); elementEnd(); } TodoComponentHostBinding.ngComponentDef.h(1, 0); componentRefresh(1, 0); } expect(renderToHtml(Template, {})).toEqual('one'); cmptInstance !.title = 'two'; expect(renderToHtml(Template, {})).toEqual('two'); }); it('should support component with bindings in template', () => { /**

{{ name }}

*/ class MyComp { name = 'Bess'; static ngComponentDef = defineComponent({ type: MyComp, tag: 'comp', template: function MyCompTemplate(ctx: any, cm: boolean) { if (cm) { elementStart(0, 'p'); { text(1); } elementEnd(); } textBinding(1, bind(ctx.name)); }, factory: () => new MyComp }); } function Template(ctx: any, cm: boolean) { if (cm) { elementStart(0, MyComp); elementEnd(); } MyComp.ngComponentDef.h(1, 0); componentRefresh(1, 0); } expect(renderToHtml(Template, null)).toEqual('

Bess

'); }); it('should support a component with sub-views', () => { /** * % if (condition) { *
text
* % } */ class MyComp { condition: boolean; static ngComponentDef = defineComponent({ type: MyComp, tag: 'comp', template: function MyCompTemplate(ctx: any, cm: boolean) { if (cm) { container(0); } containerRefreshStart(0); { if (ctx.condition) { if (viewStart(0)) { elementStart(0, 'div'); { text(1, 'text'); } elementEnd(); } viewEnd(); } } containerRefreshEnd(); }, factory: () => new MyComp, inputs: {condition: 'condition'} }); } /** */ function Template(ctx: any, cm: boolean) { if (cm) { elementStart(0, MyComp); elementEnd(); } elementProperty(0, 'condition', bind(ctx.condition)); MyComp.ngComponentDef.h(1, 0); componentRefresh(1, 0); } expect(renderToHtml(Template, {condition: true})).toEqual('
text
'); expect(renderToHtml(Template, {condition: false})).toEqual(''); }); }); describe('tree', () => { interface Tree { beforeLabel?: string; subTrees?: Tree[]; afterLabel?: string; } interface ParentCtx { beforeTree: Tree; projectedTree: Tree; afterTree: Tree; } function showLabel(ctx: {label: string | undefined}, cm: boolean) { if (cm) { container(0); } containerRefreshStart(0); { if (ctx.label != null) { if (viewStart(0)) { text(0); } textBinding(0, bind(ctx.label)); viewEnd(); } } containerRefreshEnd(); } function showTree(ctx: {tree: Tree}, cm: boolean) { if (cm) { container(0); container(1); container(2); } containerRefreshStart(0); { const cm0 = viewStart(0); { showLabel({label: ctx.tree.beforeLabel}, cm0); } viewEnd(); } containerRefreshEnd(); containerRefreshStart(1); { for (let subTree of ctx.tree.subTrees || []) { const cm0 = viewStart(0); { showTree({tree: subTree}, cm0); } viewEnd(); } } containerRefreshEnd(); containerRefreshStart(2); { const cm0 = viewStart(0); { showLabel({label: ctx.tree.afterLabel}, cm0); } viewEnd(); } containerRefreshEnd(); } class ChildComponent { beforeTree: Tree; afterTree: Tree; static ngComponentDef = defineComponent({ tag: 'child', type: ChildComponent, template: function ChildComponentTemplate( ctx: {beforeTree: Tree, afterTree: Tree}, cm: boolean) { if (cm) { projectionDef(0); container(1); projection(2, 0); container(3); } containerRefreshStart(1); { const cm0 = viewStart(0); { showTree({tree: ctx.beforeTree}, cm0); } viewEnd(); } containerRefreshEnd(); containerRefreshStart(3); { const cm0 = viewStart(0); { showTree({tree: ctx.afterTree}, cm0); } viewEnd(); } containerRefreshEnd(); }, factory: () => new ChildComponent, inputs: {beforeTree: 'beforeTree', afterTree: 'afterTree'} }); } function parentTemplate(ctx: ParentCtx, cm: boolean) { if (cm) { elementStart(0, ChildComponent); { container(2); } elementEnd(); } elementProperty(0, 'beforeTree', bind(ctx.beforeTree)); elementProperty(0, 'afterTree', bind(ctx.afterTree)); containerRefreshStart(2); { const cm0 = viewStart(0); { showTree({tree: ctx.projectedTree}, cm0); } viewEnd(); } containerRefreshEnd(); ChildComponent.ngComponentDef.h(1, 0); componentRefresh(1, 0); } it('should work with a tree', () => { const ctx: ParentCtx = { beforeTree: {subTrees: [{beforeLabel: 'a'}]}, projectedTree: {beforeLabel: 'p'}, afterTree: {afterLabel: 'z'} }; expect(renderToHtml(parentTemplate, ctx)).toEqual('apz'); ctx.projectedTree = {subTrees: [{}, {}, {subTrees: [{}, {}]}, {}]}; ctx.beforeTree.subTrees !.push({afterLabel: 'b'}); expect(renderToHtml(parentTemplate, ctx)).toEqual('abz'); ctx.projectedTree.subTrees ![1].afterLabel = 'h'; expect(renderToHtml(parentTemplate, ctx)).toEqual('abhz'); ctx.beforeTree.subTrees !.push({beforeLabel: 'c'}); expect(renderToHtml(parentTemplate, ctx)).toEqual('abchz'); // To check the context easily: // console.log(JSON.stringify(ctx)); }); }); describe('element bindings', () => { describe('elementAttribute', () => { it('should support attribute bindings', () => { const ctx: {title: string | null} = {title: 'Hello'}; function Template(ctx: any, cm: boolean) { if (cm) { elementStart(0, 'span'); elementEnd(); } elementAttribute(0, 'title', bind(ctx.title)); } // initial binding expect(renderToHtml(Template, ctx)).toEqual(''); // update binding ctx.title = 'Hi!'; expect(renderToHtml(Template, ctx)).toEqual(''); // remove attribute ctx.title = null; expect(renderToHtml(Template, ctx)).toEqual(''); }); it('should stringify values used attribute bindings', () => { const ctx: {title: any} = {title: NaN}; function Template(ctx: any, cm: boolean) { if (cm) { elementStart(0, 'span'); elementEnd(); } elementAttribute(0, 'title', bind(ctx.title)); } expect(renderToHtml(Template, ctx)).toEqual(''); ctx.title = {toString: () => 'Custom toString'}; expect(renderToHtml(Template, ctx)).toEqual(''); }); it('should update bindings', () => { function Template(c: any, cm: boolean) { if (cm) { elementStart(0, 'b'); elementEnd(); } elementAttribute(0, 'a', bindV(c)); elementAttribute(0, 'a0', bind(c[1])); elementAttribute(0, 'a1', bind1(c[0], c[1], c[16])); elementAttribute(0, 'a2', bind2(c[0], c[1], c[2], c[3], c[16])); elementAttribute(0, 'a3', bind3(c[0], c[1], c[2], c[3], c[4], c[5], c[16])); elementAttribute(0, 'a4', bind4(c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], c[16])); elementAttribute( 0, 'a5', bind5(c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], c[8], c[9], c[16])); elementAttribute( 0, 'a6', bind6( c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], c[8], c[9], c[10], c[11], c[16])); elementAttribute( 0, 'a7', bind7( c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], c[8], c[9], c[10], c[11], c[12], c[13], c[16])); elementAttribute( 0, 'a8', bind8( c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], c[8], c[9], c[10], c[11], c[12], c[13], c[14], c[15], c[16])); } let args = ['(', 0, 'a', 1, 'b', 2, 'c', 3, 'd', 4, 'e', 5, 'f', 6, 'g', 7, ')']; expect(renderToHtml(Template, args)) .toEqual( ''); args = args.reverse(); expect(renderToHtml(Template, args)) .toEqual( ''); args = args.reverse(); expect(renderToHtml(Template, args)) .toEqual( ''); }); it('should not update DOM if context has not changed', () => { const ctx: {title: string | null} = {title: 'Hello'}; function Template(ctx: any, cm: boolean) { if (cm) { elementStart(0, 'span'); container(1); elementEnd(); } elementAttribute(0, 'title', bind(ctx.title)); containerRefreshStart(1); { if (true) { let cm1 = viewStart(1); { if (cm1) { elementStart(0, 'b'); {} elementEnd(); } elementAttribute(0, 'title', bind(ctx.title)); } viewEnd(); } } containerRefreshEnd(); } // initial binding expect(renderToHtml(Template, ctx)) .toEqual(''); // update DOM manually containerEl.querySelector('b') !.setAttribute('title', 'Goodbye'); // refresh with same binding expect(renderToHtml(Template, ctx)) .toEqual(''); // refresh again with same binding expect(renderToHtml(Template, ctx)) .toEqual(''); }); }); describe('elementStyle', () => { it('should support binding to styles', () => { function Template(ctx: any, cm: boolean) { if (cm) { elementStart(0, 'span'); elementEnd(); } elementStyle(0, 'border-color', bind(ctx)); } expect(renderToHtml(Template, 'red')).toEqual(''); expect(renderToHtml(Template, 'green')) .toEqual(''); expect(renderToHtml(Template, null)).toEqual(''); }); it('should support binding to styles with suffix', () => { function Template(ctx: any, cm: boolean) { if (cm) { elementStart(0, 'span'); elementEnd(); } elementStyle(0, 'font-size', bind(ctx), 'px'); } expect(renderToHtml(Template, '100')).toEqual(''); expect(renderToHtml(Template, 200)).toEqual(''); expect(renderToHtml(Template, null)).toEqual(''); }); }); describe('elementClass', () => { it('should support CSS class toggle', () => { function Template(ctx: any, cm: boolean) { if (cm) { elementStart(0, 'span'); elementEnd(); } elementClass(0, 'active', bind(ctx)); } expect(renderToHtml(Template, true)).toEqual(''); expect(renderToHtml(Template, false)).toEqual(''); // truthy values expect(renderToHtml(Template, 'a_string')).toEqual(''); expect(renderToHtml(Template, 10)).toEqual(''); // falsy values expect(renderToHtml(Template, '')).toEqual(''); expect(renderToHtml(Template, 0)).toEqual(''); }); it('should work correctly with existing static classes', () => { function Template(ctx: any, cm: boolean) { if (cm) { elementStart(0, 'span', ['class', 'existing']); elementEnd(); } elementClass(0, 'active', bind(ctx)); } expect(renderToHtml(Template, true)).toEqual(''); expect(renderToHtml(Template, false)).toEqual(''); }); }); }); describe('template data', () => { it('should re-use template data and node data', () => { /** * % if (condition) { *
* % } */ function Template(ctx: any, cm: boolean) { if (cm) { container(0); } containerRefreshStart(0); { if (ctx.condition) { if (viewStart(0)) { elementStart(0, 'div'); {} elementEnd(); } viewEnd(); } } containerRefreshEnd(); } expect((Template as any).ngPrivateData).toBeUndefined(); renderToHtml(Template, {condition: true}); const oldTemplateData = (Template as any).ngPrivateData; const oldContainerData = (oldTemplateData as any).data[0]; const oldElementData = oldContainerData.data[0][0]; expect(oldContainerData).not.toBeNull(); expect(oldElementData).not.toBeNull(); renderToHtml(Template, {condition: false}); renderToHtml(Template, {condition: true}); const newTemplateData = (Template as any).ngPrivateData; const newContainerData = (oldTemplateData as any).data[0]; const newElementData = oldContainerData.data[0][0]; expect(newTemplateData === oldTemplateData).toBe(true); expect(newContainerData === oldContainerData).toBe(true); expect(newElementData === oldElementData).toBe(true); }); }); });