2017-12-01 17:23:03 -05:00
|
|
|
/**
|
|
|
|
* @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
|
|
|
|
*/
|
|
|
|
|
2018-02-16 14:36:50 -05:00
|
|
|
import {defineComponent, defineDirective} from '../../src/render3/index';
|
2018-03-07 19:25:18 -05:00
|
|
|
import {NO_CHANGE, bind, container, containerRefreshEnd, containerRefreshStart, elementAttribute, elementClassNamed, elementEnd, elementProperty, elementStart, elementStyleNamed, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV, load, projection, projectionDef, text, textBinding} from '../../src/render3/instructions';
|
2017-12-01 17:23:03 -05:00
|
|
|
|
|
|
|
import {containerEl, renderToHtml} from './render_util';
|
|
|
|
|
2018-01-03 05:42:48 -05:00
|
|
|
describe('render3 integration test', () => {
|
2017-12-01 17:23:03 -05:00
|
|
|
|
|
|
|
describe('render', () => {
|
|
|
|
|
|
|
|
it('should render basic template', () => {
|
|
|
|
expect(renderToHtml(Template, {})).toEqual('<span title="Hello">Greetings</span>');
|
|
|
|
|
|
|
|
function Template(ctx: any, cm: boolean) {
|
|
|
|
if (cm) {
|
2018-02-06 19:11:20 -05:00
|
|
|
elementStart(0, 'span', ['title', 'Hello']);
|
|
|
|
{ text(1, 'Greetings'); }
|
|
|
|
elementEnd();
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should render and update basic "Hello, World" template', () => {
|
|
|
|
expect(renderToHtml(Template, 'World')).toEqual('<h1>Hello, World!</h1>');
|
|
|
|
expect(renderToHtml(Template, 'New World')).toEqual('<h1>Hello, New World!</h1>');
|
|
|
|
|
|
|
|
function Template(name: string, cm: boolean) {
|
|
|
|
if (cm) {
|
2018-02-06 19:11:20 -05:00
|
|
|
elementStart(0, 'h1');
|
|
|
|
{ text(1); }
|
|
|
|
elementEnd();
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
2018-02-14 14:22:14 -05:00
|
|
|
textBinding(1, interpolation1('Hello, ', name, '!'));
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('text bindings', () => {
|
|
|
|
it('should render "undefined" as "" when used with `bind()`', () => {
|
|
|
|
function Template(name: string, cm: boolean) {
|
|
|
|
if (cm) {
|
2018-02-06 19:11:20 -05:00
|
|
|
text(0);
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
textBinding(0, bind(name));
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
2018-02-06 19:11:20 -05:00
|
|
|
text(0);
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
textBinding(0, bind(name));
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
2018-02-06 19:11:20 -05:00
|
|
|
text(0);
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
2018-02-14 16:37:54 -05:00
|
|
|
textBinding(0, cm ? value : NO_CHANGE);
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
|
|
|
expect(renderToHtml(Template, 'once')).toEqual('once');
|
|
|
|
expect(renderToHtml(Template, 'twice')).toEqual('once');
|
|
|
|
});
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('Siblings update', () => {
|
|
|
|
it('should handle a flat list of static/bound text nodes', () => {
|
|
|
|
function Template(name: string, cm: boolean) {
|
|
|
|
if (cm) {
|
2018-02-06 19:11:20 -05:00
|
|
|
text(0, 'Hello ');
|
|
|
|
text(1);
|
|
|
|
text(2, '!');
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
textBinding(1, bind(name));
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
|
|
|
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) {
|
2018-02-06 19:11:20 -05:00
|
|
|
elementStart(0, 'b');
|
2017-12-01 17:23:03 -05:00
|
|
|
{
|
2018-02-06 19:11:20 -05:00
|
|
|
text(1, 'Hello ');
|
|
|
|
text(2);
|
|
|
|
text(3, '!');
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
elementEnd();
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
textBinding(2, bind(name));
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
|
|
|
expect(renderToHtml(Template, 'world')).toEqual('<b>Hello world!</b>');
|
|
|
|
expect(renderToHtml(Template, 'mundo')).toEqual('<b>Hello mundo!</b>');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should render/update text node as a child of a deep list of elements', () => {
|
|
|
|
function Template(name: string, cm: boolean) {
|
|
|
|
if (cm) {
|
2018-02-06 19:11:20 -05:00
|
|
|
elementStart(0, 'b');
|
2017-12-01 17:23:03 -05:00
|
|
|
{
|
2018-02-06 19:11:20 -05:00
|
|
|
elementStart(1, 'b');
|
2017-12-01 17:23:03 -05:00
|
|
|
{
|
2018-02-06 19:11:20 -05:00
|
|
|
elementStart(2, 'b');
|
2017-12-01 17:23:03 -05:00
|
|
|
{
|
2018-02-06 19:11:20 -05:00
|
|
|
elementStart(3, 'b');
|
|
|
|
{ text(4); }
|
|
|
|
elementEnd();
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
elementEnd();
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
elementEnd();
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
elementEnd();
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
2018-02-14 14:22:14 -05:00
|
|
|
textBinding(4, interpolation1('Hello ', name, '!'));
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
|
|
|
expect(renderToHtml(Template, 'world')).toEqual('<b><b><b><b>Hello world!</b></b></b></b>');
|
|
|
|
expect(renderToHtml(Template, 'mundo')).toEqual('<b><b><b><b>Hello mundo!</b></b></b></b>');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should update 2 sibling elements', () => {
|
|
|
|
function Template(id: any, cm: boolean) {
|
|
|
|
if (cm) {
|
2018-02-06 19:11:20 -05:00
|
|
|
elementStart(0, 'b');
|
2017-12-01 17:23:03 -05:00
|
|
|
{
|
2018-02-06 19:11:20 -05:00
|
|
|
elementStart(1, 'span');
|
|
|
|
elementEnd();
|
|
|
|
elementStart(2, 'span', ['class', 'foo']);
|
2017-12-01 17:23:03 -05:00
|
|
|
{}
|
2018-02-06 19:11:20 -05:00
|
|
|
elementEnd();
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
elementEnd();
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
elementAttribute(2, 'id', bind(id));
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
|
|
|
expect(renderToHtml(Template, 'foo'))
|
|
|
|
.toEqual('<b><span></span><span class="foo" id="foo"></span></b>');
|
|
|
|
expect(renderToHtml(Template, 'bar'))
|
|
|
|
.toEqual('<b><span></span><span class="foo" id="bar"></span></b>');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should handle sibling text node after element with child text node', () => {
|
|
|
|
function Template(ctx: any, cm: boolean) {
|
|
|
|
if (cm) {
|
2018-02-06 19:11:20 -05:00
|
|
|
elementStart(0, 'p');
|
|
|
|
{ text(1, 'hello'); }
|
|
|
|
elementEnd();
|
|
|
|
text(2, 'world');
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
expect(renderToHtml(Template, null)).toEqual('<p>hello</p>world');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('basic components', () => {
|
|
|
|
|
|
|
|
class TodoComponent {
|
|
|
|
value = ' one';
|
|
|
|
|
|
|
|
static ngComponentDef = defineComponent({
|
2018-01-22 18:27:21 -05:00
|
|
|
type: TodoComponent,
|
2017-12-01 17:23:03 -05:00
|
|
|
tag: 'todo',
|
|
|
|
template: function TodoTemplate(ctx: any, cm: boolean) {
|
|
|
|
if (cm) {
|
2018-02-06 19:11:20 -05:00
|
|
|
elementStart(0, 'p');
|
2017-12-01 17:23:03 -05:00
|
|
|
{
|
2018-02-06 19:11:20 -05:00
|
|
|
text(1, 'Todo');
|
|
|
|
text(2);
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
elementEnd();
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
textBinding(2, bind(ctx.value));
|
2017-12-01 17:23:03 -05:00
|
|
|
},
|
|
|
|
factory: () => new TodoComponent
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
it('should support a basic component template', () => {
|
|
|
|
function Template(ctx: any, cm: boolean) {
|
|
|
|
if (cm) {
|
2018-02-06 19:11:20 -05:00
|
|
|
elementStart(0, TodoComponent);
|
|
|
|
elementEnd();
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
expect(renderToHtml(Template, null)).toEqual('<todo><p>Todo one</p></todo>');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should support a component template with sibling', () => {
|
|
|
|
function Template(ctx: any, cm: boolean) {
|
|
|
|
if (cm) {
|
2018-02-06 19:11:20 -05:00
|
|
|
elementStart(0, TodoComponent);
|
|
|
|
elementEnd();
|
|
|
|
text(2, 'two');
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
expect(renderToHtml(Template, null)).toEqual('<todo><p>Todo one</p></todo>two');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should support a component template with component sibling', () => {
|
|
|
|
/**
|
|
|
|
* <todo></todo>
|
|
|
|
* <todo></todo>
|
|
|
|
*/
|
|
|
|
function Template(ctx: any, cm: boolean) {
|
|
|
|
if (cm) {
|
2018-02-06 19:11:20 -05:00
|
|
|
elementStart(0, TodoComponent);
|
|
|
|
elementEnd();
|
|
|
|
elementStart(2, TodoComponent);
|
|
|
|
elementEnd();
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
expect(renderToHtml(Template, null))
|
|
|
|
.toEqual('<todo><p>Todo one</p></todo><todo><p>Todo one</p></todo>');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should support a component with binding on host element', () => {
|
|
|
|
let cmptInstance: TodoComponentHostBinding|null;
|
|
|
|
|
|
|
|
class TodoComponentHostBinding {
|
|
|
|
title = 'one';
|
|
|
|
static ngComponentDef = defineComponent({
|
2018-01-22 18:27:21 -05:00
|
|
|
type: TodoComponentHostBinding,
|
2017-12-01 17:23:03 -05:00
|
|
|
tag: 'todo',
|
|
|
|
template: function TodoComponentHostBindingTemplate(
|
|
|
|
ctx: TodoComponentHostBinding, cm: boolean) {
|
|
|
|
if (cm) {
|
2018-02-06 19:11:20 -05:00
|
|
|
text(0);
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
textBinding(0, bind(ctx.title));
|
2017-12-01 17:23:03 -05:00
|
|
|
},
|
|
|
|
factory: () => cmptInstance = new TodoComponentHostBinding,
|
2017-12-20 19:26:07 -05:00
|
|
|
hostBindings: function(directiveIndex: number, elementIndex: number): void {
|
2017-12-01 17:23:03 -05:00
|
|
|
// host bindings
|
2018-02-06 19:11:20 -05:00
|
|
|
elementProperty(
|
2018-02-16 19:58:07 -05:00
|
|
|
elementIndex, 'title', bind(load<TodoComponentHostBinding>(directiveIndex).title));
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function Template(ctx: any, cm: boolean) {
|
|
|
|
if (cm) {
|
2018-02-06 19:11:20 -05:00
|
|
|
elementStart(0, TodoComponentHostBinding);
|
|
|
|
elementEnd();
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
expect(renderToHtml(Template, {})).toEqual('<todo title="one">one</todo>');
|
|
|
|
cmptInstance !.title = 'two';
|
|
|
|
expect(renderToHtml(Template, {})).toEqual('<todo title="two">two</todo>');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should support component with bindings in template', () => {
|
|
|
|
/** <p> {{ name }} </p>*/
|
|
|
|
class MyComp {
|
|
|
|
name = 'Bess';
|
|
|
|
static ngComponentDef = defineComponent({
|
2018-01-22 18:27:21 -05:00
|
|
|
type: MyComp,
|
2017-12-01 17:23:03 -05:00
|
|
|
tag: 'comp',
|
|
|
|
template: function MyCompTemplate(ctx: any, cm: boolean) {
|
|
|
|
if (cm) {
|
2018-02-06 19:11:20 -05:00
|
|
|
elementStart(0, 'p');
|
|
|
|
{ text(1); }
|
|
|
|
elementEnd();
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
textBinding(1, bind(ctx.name));
|
2017-12-01 17:23:03 -05:00
|
|
|
},
|
|
|
|
factory: () => new MyComp
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function Template(ctx: any, cm: boolean) {
|
|
|
|
if (cm) {
|
2018-02-06 19:11:20 -05:00
|
|
|
elementStart(0, MyComp);
|
|
|
|
elementEnd();
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
expect(renderToHtml(Template, null)).toEqual('<comp><p>Bess</p></comp>');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should support a component with sub-views', () => {
|
|
|
|
/**
|
|
|
|
* % if (condition) {
|
|
|
|
* <div>text</div>
|
|
|
|
* % }
|
|
|
|
*/
|
|
|
|
class MyComp {
|
|
|
|
condition: boolean;
|
|
|
|
static ngComponentDef = defineComponent({
|
2018-01-22 18:27:21 -05:00
|
|
|
type: MyComp,
|
2017-12-01 17:23:03 -05:00
|
|
|
tag: 'comp',
|
|
|
|
template: function MyCompTemplate(ctx: any, cm: boolean) {
|
|
|
|
if (cm) {
|
2018-02-06 19:11:20 -05:00
|
|
|
container(0);
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
containerRefreshStart(0);
|
2017-12-01 17:23:03 -05:00
|
|
|
{
|
|
|
|
if (ctx.condition) {
|
2018-02-06 20:27:16 -05:00
|
|
|
if (embeddedViewStart(0)) {
|
2018-02-06 19:11:20 -05:00
|
|
|
elementStart(0, 'div');
|
|
|
|
{ text(1, 'text'); }
|
|
|
|
elementEnd();
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
2018-02-06 20:27:16 -05:00
|
|
|
embeddedViewEnd();
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
containerRefreshEnd();
|
2017-12-01 17:23:03 -05:00
|
|
|
},
|
|
|
|
factory: () => new MyComp,
|
|
|
|
inputs: {condition: 'condition'}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/** <comp [condition]="condition"></comp> */
|
|
|
|
function Template(ctx: any, cm: boolean) {
|
|
|
|
if (cm) {
|
2018-02-06 19:11:20 -05:00
|
|
|
elementStart(0, MyComp);
|
|
|
|
elementEnd();
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
elementProperty(0, 'condition', bind(ctx.condition));
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
expect(renderToHtml(Template, {condition: true})).toEqual('<comp><div>text</div></comp>');
|
|
|
|
expect(renderToHtml(Template, {condition: false})).toEqual('<comp></comp>');
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
});
|
|
|
|
|
2018-01-25 09:32:21 -05:00
|
|
|
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) {
|
2018-02-06 19:11:20 -05:00
|
|
|
container(0);
|
2018-01-25 09:32:21 -05:00
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
containerRefreshStart(0);
|
2018-01-25 09:32:21 -05:00
|
|
|
{
|
|
|
|
if (ctx.label != null) {
|
2018-02-06 20:27:16 -05:00
|
|
|
if (embeddedViewStart(0)) {
|
2018-02-06 19:11:20 -05:00
|
|
|
text(0);
|
2018-01-25 09:32:21 -05:00
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
textBinding(0, bind(ctx.label));
|
2018-02-06 20:27:16 -05:00
|
|
|
embeddedViewEnd();
|
2018-01-25 09:32:21 -05:00
|
|
|
}
|
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
containerRefreshEnd();
|
2018-01-25 09:32:21 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
function showTree(ctx: {tree: Tree}, cm: boolean) {
|
|
|
|
if (cm) {
|
2018-02-06 19:11:20 -05:00
|
|
|
container(0);
|
|
|
|
container(1);
|
|
|
|
container(2);
|
2018-01-25 09:32:21 -05:00
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
containerRefreshStart(0);
|
2018-01-25 09:32:21 -05:00
|
|
|
{
|
2018-02-06 20:27:16 -05:00
|
|
|
const cm0 = embeddedViewStart(0);
|
2018-01-25 09:32:21 -05:00
|
|
|
{ showLabel({label: ctx.tree.beforeLabel}, cm0); }
|
2018-02-06 20:27:16 -05:00
|
|
|
embeddedViewEnd();
|
2018-01-25 09:32:21 -05:00
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
containerRefreshEnd();
|
|
|
|
containerRefreshStart(1);
|
2018-01-25 09:32:21 -05:00
|
|
|
{
|
|
|
|
for (let subTree of ctx.tree.subTrees || []) {
|
2018-02-06 20:27:16 -05:00
|
|
|
const cm0 = embeddedViewStart(0);
|
2018-01-25 09:32:21 -05:00
|
|
|
{ showTree({tree: subTree}, cm0); }
|
2018-02-06 20:27:16 -05:00
|
|
|
embeddedViewEnd();
|
2018-01-25 09:32:21 -05:00
|
|
|
}
|
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
containerRefreshEnd();
|
|
|
|
containerRefreshStart(2);
|
2018-01-25 09:32:21 -05:00
|
|
|
{
|
2018-02-06 20:27:16 -05:00
|
|
|
const cm0 = embeddedViewStart(0);
|
2018-01-25 09:32:21 -05:00
|
|
|
{ showLabel({label: ctx.tree.afterLabel}, cm0); }
|
2018-02-06 20:27:16 -05:00
|
|
|
embeddedViewEnd();
|
2018-01-25 09:32:21 -05:00
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
containerRefreshEnd();
|
2018-01-25 09:32:21 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
2018-02-06 19:11:20 -05:00
|
|
|
projectionDef(0);
|
|
|
|
container(1);
|
|
|
|
projection(2, 0);
|
|
|
|
container(3);
|
2018-01-25 09:32:21 -05:00
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
containerRefreshStart(1);
|
2018-01-25 09:32:21 -05:00
|
|
|
{
|
2018-02-06 20:27:16 -05:00
|
|
|
const cm0 = embeddedViewStart(0);
|
2018-01-25 09:32:21 -05:00
|
|
|
{ showTree({tree: ctx.beforeTree}, cm0); }
|
2018-02-06 20:27:16 -05:00
|
|
|
embeddedViewEnd();
|
2018-01-25 09:32:21 -05:00
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
containerRefreshEnd();
|
|
|
|
containerRefreshStart(3);
|
2018-01-25 09:32:21 -05:00
|
|
|
{
|
2018-02-06 20:27:16 -05:00
|
|
|
const cm0 = embeddedViewStart(0);
|
2018-01-25 09:32:21 -05:00
|
|
|
{ showTree({tree: ctx.afterTree}, cm0); }
|
2018-02-06 20:27:16 -05:00
|
|
|
embeddedViewEnd();
|
2018-01-25 09:32:21 -05:00
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
containerRefreshEnd();
|
2018-01-25 09:32:21 -05:00
|
|
|
},
|
|
|
|
factory: () => new ChildComponent,
|
|
|
|
inputs: {beforeTree: 'beforeTree', afterTree: 'afterTree'}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function parentTemplate(ctx: ParentCtx, cm: boolean) {
|
|
|
|
if (cm) {
|
2018-02-06 19:11:20 -05:00
|
|
|
elementStart(0, ChildComponent);
|
|
|
|
{ container(2); }
|
|
|
|
elementEnd();
|
2018-01-25 09:32:21 -05:00
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
elementProperty(0, 'beforeTree', bind(ctx.beforeTree));
|
|
|
|
elementProperty(0, 'afterTree', bind(ctx.afterTree));
|
|
|
|
containerRefreshStart(2);
|
2018-01-25 09:32:21 -05:00
|
|
|
{
|
2018-02-06 20:27:16 -05:00
|
|
|
const cm0 = embeddedViewStart(0);
|
2018-01-25 09:32:21 -05:00
|
|
|
{ showTree({tree: ctx.projectedTree}, cm0); }
|
2018-02-06 20:27:16 -05:00
|
|
|
embeddedViewEnd();
|
2018-01-25 09:32:21 -05:00
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
containerRefreshEnd();
|
2018-01-25 09:32:21 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
it('should work with a tree', () => {
|
|
|
|
|
|
|
|
const ctx: ParentCtx = {
|
|
|
|
beforeTree: {subTrees: [{beforeLabel: 'a'}]},
|
|
|
|
projectedTree: {beforeLabel: 'p'},
|
|
|
|
afterTree: {afterLabel: 'z'}
|
|
|
|
};
|
|
|
|
expect(renderToHtml(parentTemplate, ctx)).toEqual('<child>apz</child>');
|
|
|
|
ctx.projectedTree = {subTrees: [{}, {}, {subTrees: [{}, {}]}, {}]};
|
|
|
|
ctx.beforeTree.subTrees !.push({afterLabel: 'b'});
|
|
|
|
expect(renderToHtml(parentTemplate, ctx)).toEqual('<child>abz</child>');
|
|
|
|
ctx.projectedTree.subTrees ![1].afterLabel = 'h';
|
|
|
|
expect(renderToHtml(parentTemplate, ctx)).toEqual('<child>abhz</child>');
|
|
|
|
ctx.beforeTree.subTrees !.push({beforeLabel: 'c'});
|
|
|
|
expect(renderToHtml(parentTemplate, ctx)).toEqual('<child>abchz</child>');
|
|
|
|
|
|
|
|
// To check the context easily:
|
|
|
|
// console.log(JSON.stringify(ctx));
|
|
|
|
});
|
|
|
|
|
|
|
|
});
|
|
|
|
|
2017-12-01 17:23:03 -05:00
|
|
|
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) {
|
2018-02-06 19:11:20 -05:00
|
|
|
elementStart(0, 'span');
|
|
|
|
elementEnd();
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
elementAttribute(0, 'title', bind(ctx.title));
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// initial binding
|
|
|
|
expect(renderToHtml(Template, ctx)).toEqual('<span title="Hello"></span>');
|
|
|
|
|
|
|
|
// update binding
|
|
|
|
ctx.title = 'Hi!';
|
|
|
|
expect(renderToHtml(Template, ctx)).toEqual('<span title="Hi!"></span>');
|
|
|
|
|
|
|
|
// remove attribute
|
|
|
|
ctx.title = null;
|
|
|
|
expect(renderToHtml(Template, ctx)).toEqual('<span></span>');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should stringify values used attribute bindings', () => {
|
|
|
|
const ctx: {title: any} = {title: NaN};
|
|
|
|
|
|
|
|
function Template(ctx: any, cm: boolean) {
|
|
|
|
if (cm) {
|
2018-02-06 19:11:20 -05:00
|
|
|
elementStart(0, 'span');
|
|
|
|
elementEnd();
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
elementAttribute(0, 'title', bind(ctx.title));
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
expect(renderToHtml(Template, ctx)).toEqual('<span title="NaN"></span>');
|
|
|
|
|
|
|
|
ctx.title = {toString: () => 'Custom toString'};
|
|
|
|
expect(renderToHtml(Template, ctx)).toEqual('<span title="Custom toString"></span>');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should update bindings', () => {
|
|
|
|
function Template(c: any, cm: boolean) {
|
|
|
|
if (cm) {
|
2018-02-06 19:11:20 -05:00
|
|
|
elementStart(0, 'b');
|
|
|
|
elementEnd();
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
2018-02-14 14:22:14 -05:00
|
|
|
elementAttribute(0, 'a', interpolationV(c));
|
2018-02-06 19:11:20 -05:00
|
|
|
elementAttribute(0, 'a0', bind(c[1]));
|
2018-02-14 14:22:14 -05:00
|
|
|
elementAttribute(0, 'a1', interpolation1(c[0], c[1], c[16]));
|
|
|
|
elementAttribute(0, 'a2', interpolation2(c[0], c[1], c[2], c[3], c[16]));
|
|
|
|
elementAttribute(0, 'a3', interpolation3(c[0], c[1], c[2], c[3], c[4], c[5], c[16]));
|
|
|
|
elementAttribute(
|
|
|
|
0, 'a4', interpolation4(c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], c[16]));
|
2018-02-06 19:11:20 -05:00
|
|
|
elementAttribute(
|
2018-02-14 14:22:14 -05:00
|
|
|
0, 'a5',
|
|
|
|
interpolation5(c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], c[8], c[9], c[16]));
|
2018-02-06 19:11:20 -05:00
|
|
|
elementAttribute(
|
|
|
|
0, 'a6',
|
2018-02-14 14:22:14 -05:00
|
|
|
interpolation6(
|
2018-02-06 19:11:20 -05:00
|
|
|
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(
|
2018-02-14 14:22:14 -05:00
|
|
|
0, 'a7', interpolation7(
|
2018-02-06 19:11:20 -05:00
|
|
|
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(
|
2018-02-14 14:22:14 -05:00
|
|
|
0, 'a8', interpolation8(
|
2018-02-06 19:11:20 -05:00
|
|
|
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]));
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
|
|
|
let args = ['(', 0, 'a', 1, 'b', 2, 'c', 3, 'd', 4, 'e', 5, 'f', 6, 'g', 7, ')'];
|
|
|
|
expect(renderToHtml(Template, args))
|
|
|
|
.toEqual(
|
|
|
|
'<b a="(0a1b2c3d4e5f6g7)" a0="0" a1="(0)" a2="(0a1)" a3="(0a1b2)" a4="(0a1b2c3)" a5="(0a1b2c3d4)" a6="(0a1b2c3d4e5)" a7="(0a1b2c3d4e5f6)" a8="(0a1b2c3d4e5f6g7)"></b>');
|
|
|
|
args = args.reverse();
|
|
|
|
expect(renderToHtml(Template, args))
|
|
|
|
.toEqual(
|
|
|
|
'<b a=")7g6f5e4d3c2b1a0(" a0="7" a1=")7(" a2=")7g6(" a3=")7g6f5(" a4=")7g6f5e4(" a5=")7g6f5e4d3(" a6=")7g6f5e4d3c2(" a7=")7g6f5e4d3c2b1(" a8=")7g6f5e4d3c2b1a0("></b>');
|
|
|
|
args = args.reverse();
|
|
|
|
expect(renderToHtml(Template, args))
|
|
|
|
.toEqual(
|
|
|
|
'<b a="(0a1b2c3d4e5f6g7)" a0="0" a1="(0)" a2="(0a1)" a3="(0a1b2)" a4="(0a1b2c3)" a5="(0a1b2c3d4)" a6="(0a1b2c3d4e5)" a7="(0a1b2c3d4e5f6)" a8="(0a1b2c3d4e5f6g7)"></b>');
|
|
|
|
});
|
|
|
|
|
|
|
|
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) {
|
2018-02-06 19:11:20 -05:00
|
|
|
elementStart(0, 'span');
|
|
|
|
container(1);
|
|
|
|
elementEnd();
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
elementAttribute(0, 'title', bind(ctx.title));
|
|
|
|
containerRefreshStart(1);
|
2017-12-01 17:23:03 -05:00
|
|
|
{
|
|
|
|
if (true) {
|
2018-02-06 20:27:16 -05:00
|
|
|
let cm1 = embeddedViewStart(1);
|
2017-12-01 17:23:03 -05:00
|
|
|
{
|
|
|
|
if (cm1) {
|
2018-02-06 19:11:20 -05:00
|
|
|
elementStart(0, 'b');
|
2017-12-01 17:23:03 -05:00
|
|
|
{}
|
2018-02-06 19:11:20 -05:00
|
|
|
elementEnd();
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
elementAttribute(0, 'title', bind(ctx.title));
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
2018-02-06 20:27:16 -05:00
|
|
|
embeddedViewEnd();
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
containerRefreshEnd();
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// initial binding
|
|
|
|
expect(renderToHtml(Template, ctx))
|
|
|
|
.toEqual('<span title="Hello"><b title="Hello"></b></span>');
|
|
|
|
// update DOM manually
|
|
|
|
containerEl.querySelector('b') !.setAttribute('title', 'Goodbye');
|
|
|
|
// refresh with same binding
|
|
|
|
expect(renderToHtml(Template, ctx))
|
|
|
|
.toEqual('<span title="Hello"><b title="Goodbye"></b></span>');
|
|
|
|
// refresh again with same binding
|
|
|
|
expect(renderToHtml(Template, ctx))
|
|
|
|
.toEqual('<span title="Hello"><b title="Goodbye"></b></span>');
|
|
|
|
});
|
2018-02-16 14:36:50 -05:00
|
|
|
|
|
|
|
it('should support host attribute bindings', () => {
|
|
|
|
let hostBindingDir: HostBindingDir;
|
|
|
|
|
|
|
|
class HostBindingDir {
|
|
|
|
/* @HostBinding('attr.aria-label') */
|
|
|
|
label = 'some label';
|
|
|
|
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
|
|
type: HostBindingDir,
|
|
|
|
factory: function HostBindingDir_Factory() {
|
|
|
|
return hostBindingDir = new HostBindingDir();
|
|
|
|
},
|
|
|
|
hostBindings: function HostBindingDir_HostBindings(dirIndex: number, elIndex: number) {
|
2018-02-18 19:02:47 -05:00
|
|
|
elementAttribute(elIndex, 'aria-label', bind(load<HostBindingDir>(dirIndex).label));
|
2018-02-16 14:36:50 -05:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function Template(ctx: any, cm: boolean) {
|
|
|
|
if (cm) {
|
|
|
|
elementStart(0, 'div', ['hostBindingDir', ''], [HostBindingDir]);
|
|
|
|
elementEnd();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
expect(renderToHtml(Template, {}))
|
|
|
|
.toEqual(`<div aria-label="some label" hostbindingdir=""></div>`);
|
|
|
|
|
|
|
|
hostBindingDir !.label = 'other label';
|
|
|
|
expect(renderToHtml(Template, {}))
|
|
|
|
.toEqual(`<div aria-label="other label" hostbindingdir=""></div>`);
|
|
|
|
});
|
2017-12-01 17:23:03 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
describe('elementStyle', () => {
|
|
|
|
|
|
|
|
it('should support binding to styles', () => {
|
|
|
|
function Template(ctx: any, cm: boolean) {
|
|
|
|
if (cm) {
|
2018-02-06 19:11:20 -05:00
|
|
|
elementStart(0, 'span');
|
|
|
|
elementEnd();
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
2018-03-07 19:25:18 -05:00
|
|
|
elementStyleNamed(0, 'border-color', bind(ctx));
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
expect(renderToHtml(Template, 'red')).toEqual('<span style="border-color: red;"></span>');
|
|
|
|
expect(renderToHtml(Template, 'green'))
|
|
|
|
.toEqual('<span style="border-color: green;"></span>');
|
|
|
|
expect(renderToHtml(Template, null)).toEqual('<span></span>');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should support binding to styles with suffix', () => {
|
|
|
|
function Template(ctx: any, cm: boolean) {
|
|
|
|
if (cm) {
|
2018-02-06 19:11:20 -05:00
|
|
|
elementStart(0, 'span');
|
|
|
|
elementEnd();
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
2018-03-07 19:25:18 -05:00
|
|
|
elementStyleNamed(0, 'font-size', bind(ctx), 'px');
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
expect(renderToHtml(Template, '100')).toEqual('<span style="font-size: 100px;"></span>');
|
|
|
|
expect(renderToHtml(Template, 200)).toEqual('<span style="font-size: 200px;"></span>');
|
|
|
|
expect(renderToHtml(Template, null)).toEqual('<span></span>');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('elementClass', () => {
|
|
|
|
|
|
|
|
it('should support CSS class toggle', () => {
|
|
|
|
function Template(ctx: any, cm: boolean) {
|
|
|
|
if (cm) {
|
2018-02-06 19:11:20 -05:00
|
|
|
elementStart(0, 'span');
|
|
|
|
elementEnd();
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
2018-03-07 19:25:18 -05:00
|
|
|
elementClassNamed(0, 'active', bind(ctx));
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
expect(renderToHtml(Template, true)).toEqual('<span class="active"></span>');
|
|
|
|
expect(renderToHtml(Template, false)).toEqual('<span class=""></span>');
|
|
|
|
|
|
|
|
// truthy values
|
|
|
|
expect(renderToHtml(Template, 'a_string')).toEqual('<span class="active"></span>');
|
|
|
|
expect(renderToHtml(Template, 10)).toEqual('<span class="active"></span>');
|
|
|
|
|
|
|
|
// falsy values
|
|
|
|
expect(renderToHtml(Template, '')).toEqual('<span class=""></span>');
|
|
|
|
expect(renderToHtml(Template, 0)).toEqual('<span class=""></span>');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should work correctly with existing static classes', () => {
|
|
|
|
function Template(ctx: any, cm: boolean) {
|
|
|
|
if (cm) {
|
2018-02-06 19:11:20 -05:00
|
|
|
elementStart(0, 'span', ['class', 'existing']);
|
|
|
|
elementEnd();
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
2018-03-07 19:25:18 -05:00
|
|
|
elementClassNamed(0, 'active', bind(ctx));
|
2017-12-01 17:23:03 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
expect(renderToHtml(Template, true)).toEqual('<span class="existing active"></span>');
|
|
|
|
expect(renderToHtml(Template, false)).toEqual('<span class="existing"></span>');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2017-12-08 14:48:54 -05:00
|
|
|
describe('template data', () => {
|
|
|
|
|
|
|
|
it('should re-use template data and node data', () => {
|
|
|
|
/**
|
|
|
|
* % if (condition) {
|
|
|
|
* <div></div>
|
|
|
|
* % }
|
|
|
|
*/
|
|
|
|
function Template(ctx: any, cm: boolean) {
|
|
|
|
if (cm) {
|
2018-02-06 19:11:20 -05:00
|
|
|
container(0);
|
2017-12-08 14:48:54 -05:00
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
containerRefreshStart(0);
|
2017-12-08 14:48:54 -05:00
|
|
|
{
|
|
|
|
if (ctx.condition) {
|
2018-02-06 20:27:16 -05:00
|
|
|
if (embeddedViewStart(0)) {
|
2018-02-06 19:11:20 -05:00
|
|
|
elementStart(0, 'div');
|
2017-12-08 14:48:54 -05:00
|
|
|
{}
|
2018-02-06 19:11:20 -05:00
|
|
|
elementEnd();
|
2017-12-08 14:48:54 -05:00
|
|
|
}
|
2018-02-06 20:27:16 -05:00
|
|
|
embeddedViewEnd();
|
2017-12-08 14:48:54 -05:00
|
|
|
}
|
|
|
|
}
|
2018-02-06 19:11:20 -05:00
|
|
|
containerRefreshEnd();
|
2017-12-08 14:48:54 -05:00
|
|
|
}
|
|
|
|
|
2018-01-10 21:19:16 -05:00
|
|
|
expect((Template as any).ngPrivateData).toBeUndefined();
|
2017-12-08 14:48:54 -05:00
|
|
|
|
|
|
|
renderToHtml(Template, {condition: true});
|
|
|
|
|
2018-01-10 21:19:16 -05:00
|
|
|
const oldTemplateData = (Template as any).ngPrivateData;
|
|
|
|
const oldContainerData = (oldTemplateData as any).data[0];
|
|
|
|
const oldElementData = oldContainerData.data[0][0];
|
2017-12-08 14:48:54 -05:00
|
|
|
expect(oldContainerData).not.toBeNull();
|
|
|
|
expect(oldElementData).not.toBeNull();
|
|
|
|
|
|
|
|
renderToHtml(Template, {condition: false});
|
|
|
|
renderToHtml(Template, {condition: true});
|
|
|
|
|
2018-01-10 21:19:16 -05:00
|
|
|
const newTemplateData = (Template as any).ngPrivateData;
|
|
|
|
const newContainerData = (oldTemplateData as any).data[0];
|
|
|
|
const newElementData = oldContainerData.data[0][0];
|
2017-12-08 14:48:54 -05:00
|
|
|
expect(newTemplateData === oldTemplateData).toBe(true);
|
|
|
|
expect(newContainerData === oldContainerData).toBe(true);
|
|
|
|
expect(newElementData === oldElementData).toBe(true);
|
|
|
|
});
|
|
|
|
|
|
|
|
});
|
|
|
|
|
2017-12-01 17:23:03 -05:00
|
|
|
});
|