feat(core): Moving Renderer3 into @angular/core (#20855)

PR Close #20855
This commit is contained in:
Miško Hevery
2017-12-01 14:23:03 -08:00
committed by Igor Minar
parent bc66d27938
commit 0fa818b318
39 changed files with 8544 additions and 4 deletions

View File

@ -0,0 +1,29 @@
package(default_visibility = ["//visibility:public"])
load("@build_bazel_rules_typescript//:defs.bzl", "ts_library")
load("@build_bazel_rules_nodejs//:defs.bzl", "jasmine_node_test")
ts_library(
name = "lib",
testonly = 1,
srcs = glob(
["**/*.ts"],
exclude = ["**/*_perf.ts"],
),
tsconfig = "//packages:tsconfig.json",
deps = [
"//packages:types",
"//packages/core",
"//packages/platform-browser",
],
)
jasmine_node_test(
name = "render3",
bootstrap = [
"angular_src/packages/core/test/render3/load_domino",
],
deps = [
":lib",
],
)

View File

@ -0,0 +1,74 @@
/**
* @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 {C, E, T, V, c, defineComponent, e, rC, rc, v} from '../../src/render3/index';
import {document, renderComponent} from './render_util';
describe('iv perf test', () => {
const count = 100000;
const noOfIterations = 10;
describe('render', () => {
for (let iteration = 0; iteration < noOfIterations; iteration++) {
it(`${iteration}. create ${count} divs in DOM`, () => {
const start = new Date().getTime();
const container = document.createElement('div');
for (let i = 0; i < count; i++) {
const div = document.createElement('div');
div.appendChild(document.createTextNode('-'));
container.appendChild(div);
}
const end = new Date().getTime();
log(`${count} DIVs in DOM`, (end - start) / count);
});
it(`${iteration}. create ${count} divs in Render3`, () => {
class Component {
static ngComponentDef = defineComponent({
type: Component,
tag: 'div',
template: function Template(ctx: any, cm: any) {
if (cm) {
C(0);
c();
}
rC(0);
{
for (let i = 0; i < count; i++) {
let cm0 = V(0);
{
if (cm0) {
E(0, 'div');
T(1, '-');
e();
}
}
v();
}
}
rc();
},
factory: () => new Component
});
}
const start = new Date().getTime();
renderComponent(Component);
const end = new Date().getTime();
log(`${count} DIVs in Render3`, (end - start) / count);
});
}
});
});
function log(text: string, duration: number) {
// tslint:disable-next-line:no-console
console.log(text, duration * 1000, 'ns');
}

View File

@ -0,0 +1,62 @@
/**
* @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 {T, b, defineComponent, markDirty, t} from '../../src/render3/index';
import {containerEl, renderComponent, requestAnimationFrame} from './render_util';
describe('component', () => {
class CounterComponent {
count = 0;
increment() { this.count++; }
static ngComponentDef = defineComponent({
type: CounterComponent,
tag: 'counter',
template: function(ctx: CounterComponent, cm: boolean) {
if (cm) {
T(0);
}
t(0, b(ctx.count));
},
factory: () => new CounterComponent,
inputs: {count: 'count'},
methods: {increment: 'increment'}
});
}
beforeEach(
() => {
});
describe('renderComponent', () => {
it('should render on initial call', () => {
renderComponent(CounterComponent);
expect(containerEl.innerHTML).toEqual('0');
});
it('should re-render on input change or method invocation', () => {
const component = renderComponent(CounterComponent);
expect(containerEl.innerHTML).toEqual('0');
component.count = 123;
markDirty(component, requestAnimationFrame);
expect(containerEl.innerHTML).toEqual('0');
requestAnimationFrame.flush();
expect(containerEl.innerHTML).toEqual('123');
component.increment();
markDirty(component, requestAnimationFrame);
expect(containerEl.innerHTML).toEqual('123');
requestAnimationFrame.flush();
expect(containerEl.innerHTML).toEqual('124');
});
});
});

View File

@ -0,0 +1,816 @@
/**
* @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 {C, D, E, P, T, V, c, dP, detectChanges, e, m, rC, rc, v} from '../../src/render3/index';
import {createComponent, renderComponent, toHtml} from './render_util';
describe('content projection', () => {
it('should project content', () => {
/**
* <div><ng-content></ng-content></div>
*/
const Child = createComponent('child', function(ctx: any, cm: boolean) {
if (cm) {
m(0, dP());
E(0, 'div');
{ P(1, 0); }
e();
}
});
/**
* <child>content</child>
*/
const Parent = createComponent('parent', function(ctx: any, cm: boolean) {
if (cm) {
E(0, Child.ngComponentDef);
{
D(0, Child.ngComponentDef.n(), Child.ngComponentDef);
T(1, 'content');
}
e();
}
Child.ngComponentDef.r(0, 0);
});
const parent = renderComponent(Parent);
expect(toHtml(parent)).toEqual('<child><div>content</div></child>');
});
it('should project content when root.', () => {
const Child = createComponent('child', function(ctx: any, cm: boolean) {
if (cm) {
m(0, dP());
P(0, 0);
}
});
const Parent = createComponent('parent', function(ctx: any, cm: boolean) {
if (cm) {
E(0, Child.ngComponentDef);
{
D(0, Child.ngComponentDef.n(), Child.ngComponentDef);
T(1, 'content');
}
e();
}
Child.ngComponentDef.r(0, 0);
});
const parent = renderComponent(Parent);
expect(toHtml(parent)).toEqual('<child>content</child>');
});
it('should re-project content when root.', () => {
const GrandChild = createComponent('grand-child', function(ctx: any, cm: boolean) {
if (cm) {
m(0, dP());
E(0, 'div');
{ P(1, 0); }
e();
}
});
const Child = createComponent('child', function(ctx: any, cm: boolean) {
if (cm) {
m(0, dP());
E(0, GrandChild.ngComponentDef);
{
D(0, GrandChild.ngComponentDef.n(), GrandChild.ngComponentDef);
P(1, 0);
}
e();
GrandChild.ngComponentDef.r(0, 0);
}
});
const Parent = createComponent('parent', function(ctx: any, cm: boolean) {
if (cm) {
E(0, Child.ngComponentDef);
{
D(0, Child.ngComponentDef.n(), Child.ngComponentDef);
E(1, 'b');
T(2, 'Hello');
e();
T(3, 'World!');
}
e();
}
Child.ngComponentDef.r(0, 0);
});
const parent = renderComponent(Parent);
expect(toHtml(parent))
.toEqual('<child><grand-child><div><b>Hello</b>World!</div></grand-child></child>');
});
it('should project content with container.', () => {
const Child = createComponent('child', function(ctx: any, cm: boolean) {
if (cm) {
m(0, dP());
E(0, 'div');
{ P(1, 0); }
e();
}
});
const Parent = createComponent('parent', function(ctx: {value: any}, cm: boolean) {
if (cm) {
E(0, Child.ngComponentDef);
{
D(0, Child.ngComponentDef.n(), Child.ngComponentDef);
T(1, '(');
C(2);
c();
T(3, ')');
}
e();
}
rC(2);
{
if (ctx.value) {
if (V(0)) {
T(0, 'content');
}
v();
}
}
rc();
Child.ngComponentDef.r(0, 0);
});
const parent = renderComponent(Parent);
expect(toHtml(parent)).toEqual('<child><div>()</div></child>');
parent.value = true;
detectChanges(parent);
expect(toHtml(parent)).toEqual('<child><div>(content)</div></child>');
parent.value = false;
detectChanges(parent);
expect(toHtml(parent)).toEqual('<child><div>()</div></child>');
});
it('should project content with container and if-else.', () => {
const Child = createComponent('child', function(ctx: any, cm: boolean) {
if (cm) {
m(0, dP());
E(0, 'div');
{ P(1, 0); }
e();
}
});
const Parent = createComponent('parent', function(ctx: {value: any}, cm: boolean) {
if (cm) {
E(0, Child.ngComponentDef);
{
D(0, Child.ngComponentDef.n(), Child.ngComponentDef);
T(1, '(');
C(2);
c();
T(3, ')');
}
e();
}
rC(2);
{
if (ctx.value) {
if (V(0)) {
T(0, 'content');
}
v();
} else {
if (V(1)) {
T(0, 'else');
}
v();
}
}
rc();
Child.ngComponentDef.r(0, 0);
});
const parent = renderComponent(Parent);
expect(toHtml(parent)).toEqual('<child><div>(else)</div></child>');
parent.value = true;
detectChanges(parent);
expect(toHtml(parent)).toEqual('<child><div>(content)</div></child>');
parent.value = false;
detectChanges(parent);
expect(toHtml(parent)).toEqual('<child><div>(else)</div></child>');
});
it('should support projection in embedded views', () => {
let childCmptInstance: any;
/**
* <div>
* % if (!skipContent) {
* <span>
* <ng-content></ng-content>
* </span>
* % }
* </div>
*/
const Child = createComponent('child', function(ctx: any, cm: boolean) {
if (cm) {
m(0, dP());
E(0, 'div');
{
C(1);
c();
}
e();
}
rC(1);
{
if (!ctx.skipContent) {
if (V(0)) {
E(0, 'span');
P(1, 0);
e();
}
v();
}
}
rc();
});
/**
* <child>content</child>
*/
const Parent = createComponent('parent', function(ctx: any, cm: boolean) {
if (cm) {
E(0, Child.ngComponentDef);
{
D(0, childCmptInstance = Child.ngComponentDef.n(), Child.ngComponentDef);
T(1, 'content');
}
e();
}
Child.ngComponentDef.r(0, 0);
});
const parent = renderComponent(Parent);
expect(toHtml(parent)).toEqual('<child><div><span>content</span></div></child>');
childCmptInstance.skipContent = true;
detectChanges(parent);
expect(toHtml(parent)).toEqual('<child><div></div></child>');
});
it('should support projection in embedded views when ng-content is a root node of an embedded view',
() => {
let childCmptInstance: any;
/**
* <div>
* % if (!skipContent) {
* <ng-content></ng-content>
* % }
* </div>
*/
const Child = createComponent('child', function(ctx: any, cm: boolean) {
if (cm) {
m(0, dP());
E(0, 'div');
{
C(1);
c();
}
e();
}
rC(1);
{
if (!ctx.skipContent) {
if (V(0)) {
P(0, 0);
}
v();
}
}
rc();
});
/**
* <child>content</child>
*/
const Parent = createComponent('parent', function(ctx: any, cm: boolean) {
if (cm) {
E(0, Child.ngComponentDef);
{
D(0, childCmptInstance = Child.ngComponentDef.n(), Child.ngComponentDef);
T(1, 'content');
}
e();
}
Child.ngComponentDef.r(0, 0);
});
const parent = renderComponent(Parent);
expect(toHtml(parent)).toEqual('<child><div>content</div></child>');
childCmptInstance.skipContent = true;
detectChanges(parent);
expect(toHtml(parent)).toEqual('<child><div></div></child>');
});
it('should project nodes into the last ng-content', () => {
/**
* <div><ng-content></ng-content></div>
* <span><ng-content></ng-content></span>
*/
const Child = createComponent('child', function(ctx: any, cm: boolean) {
if (cm) {
m(0, dP());
E(0, 'div');
{ P(1, 0); }
e();
E(2, 'span');
{ P(3, 0); }
e();
}
});
/**
* <child>content</child>
*/
const Parent = createComponent('parent', function(ctx: any, cm: boolean) {
if (cm) {
E(0, Child.ngComponentDef);
{
D(0, Child.ngComponentDef.n(), Child.ngComponentDef);
T(1, 'content');
}
e();
}
Child.ngComponentDef.r(0, 0);
});
const parent = renderComponent(Parent);
expect(toHtml(parent)).toEqual('<child><div></div><span>content</span></child>');
});
/**
* Warning: this test is _not_ in-line with what Angular does atm.
* Moreover the current implementation logic will result in DOM nodes
* being re-assigned from one parent to another. Proposal: have compiler
* to remove all but the latest occurrence of <ng-content> so we generate
* only one P(n, m, 0) instruction. It would make it consistent with the
* current Angular behaviour:
* http://plnkr.co/edit/OAYkNawTDPkYBFTqovTP?p=preview
*/
it('should project nodes into the last available ng-content', () => {
let childCmptInstance: any;
/**
* <ng-content></ng-content>
* <div>
* % if (show) {
* <ng-content></ng-content>
* % }
* </div>
*/
const Child = createComponent('child', function(ctx: any, cm: boolean) {
if (cm) {
m(0, dP());
P(0, 0);
E(1, 'div');
{
C(2);
c();
}
e();
}
rC(2);
{
if (ctx.show) {
if (V(0)) {
P(0, 0);
}
v();
}
}
rc();
});
/**
* <child>content</child>
*/
const Parent = createComponent('parent', function(ctx: any, cm: boolean) {
if (cm) {
E(0, Child.ngComponentDef);
{
D(0, childCmptInstance = Child.ngComponentDef.n(), Child.ngComponentDef);
T(1, 'content');
}
e();
}
Child.ngComponentDef.r(0, 0);
});
const parent = renderComponent(Parent);
expect(toHtml(parent)).toEqual('<child>content<div></div></child>');
childCmptInstance.show = true;
detectChanges(parent);
expect(toHtml(parent)).toEqual('<child><div>content</div></child>');
});
describe('with selectors', () => {
it('should project nodes using attribute selectors', () => {
/**
* <div id="first"><ng-content select="span[title=toFirst]"></ng-content></div>
* <div id="second"><ng-content select="span[title=toSecond]"></ng-content></div>
*/
const Child = createComponent('child', function(ctx: any, cm: boolean) {
if (cm) {
m(0,
dP([[[['span', 'title', 'toFirst'], null]], [[['span', 'title', 'toSecond'], null]]]));
E(0, 'div', ['id', 'first']);
{ P(1, 0, 1); }
e();
E(2, 'div', ['id', 'second']);
{ P(3, 0, 2); }
e();
}
});
/**
* <child>
* <span title="toFirst">1</span>
* <span title="toSecond">2</span>
* </child>
*/
const Parent = createComponent('parent', function(ctx: any, cm: boolean) {
if (cm) {
E(0, Child.ngComponentDef);
{
D(0, Child.ngComponentDef.n(), Child.ngComponentDef);
E(1, 'span', ['title', 'toFirst']);
{ T(2, '1'); }
e();
E(3, 'span', ['title', 'toSecond']);
{ T(4, '2'); }
e();
}
e();
}
Child.ngComponentDef.r(0, 0);
});
const parent = renderComponent(Parent);
expect(toHtml(parent))
.toEqual(
'<child><div id="first"><span title="toFirst">1</span></div><div id="second"><span title="toSecond">2</span></div></child>');
});
it('should project nodes using class selectors', () => {
/**
* <div id="first"><ng-content select="span.toFirst"></ng-content></div>
* <div id="second"><ng-content select="span.toSecond"></ng-content></div>
*/
const Child = createComponent('child', function(ctx: any, cm: boolean) {
if (cm) {
m(0,
dP([[[['span', 'class', 'toFirst'], null]], [[['span', 'class', 'toSecond'], null]]]));
E(0, 'div', ['id', 'first']);
{ P(1, 0, 1); }
e();
E(2, 'div', ['id', 'second']);
{ P(3, 0, 2); }
e();
}
});
/**
* <child>
* <span class="toFirst">1</span>
* <span class="toSecond">2</span>
* </child>
*/
const Parent = createComponent('parent', function(ctx: any, cm: boolean) {
if (cm) {
E(0, Child.ngComponentDef);
{
D(0, Child.ngComponentDef.n(), Child.ngComponentDef);
E(1, 'span', ['class', 'toFirst']);
{ T(2, '1'); }
e();
E(3, 'span', ['class', 'toSecond']);
{ T(4, '2'); }
e();
}
e();
}
Child.ngComponentDef.r(0, 0);
});
const parent = renderComponent(Parent);
expect(toHtml(parent))
.toEqual(
'<child><div id="first"><span class="toFirst">1</span></div><div id="second"><span class="toSecond">2</span></div></child>');
});
it('should project nodes using class selectors when element has multiple classes', () => {
/**
* <div id="first"><ng-content select="span.toFirst"></ng-content></div>
* <div id="second"><ng-content select="span.toSecond"></ng-content></div>
*/
const Child = createComponent('child', function(ctx: any, cm: boolean) {
if (cm) {
m(0,
dP([[[['span', 'class', 'toFirst'], null]], [[['span', 'class', 'toSecond'], null]]]));
E(0, 'div', ['id', 'first']);
{ P(1, 0, 1); }
e();
E(2, 'div', ['id', 'second']);
{ P(3, 0, 2); }
e();
}
});
/**
* <child>
* <span class="other toFirst">1</span>
* <span class="toSecond noise">2</span>
* </child>
*/
const Parent = createComponent('parent', function(ctx: any, cm: boolean) {
if (cm) {
E(0, Child.ngComponentDef);
{
D(0, Child.ngComponentDef.n(), Child.ngComponentDef);
E(1, 'span', ['class', 'other toFirst']);
{ T(2, '1'); }
e();
E(3, 'span', ['class', 'toSecond noise']);
{ T(4, '2'); }
e();
}
e();
}
Child.ngComponentDef.r(0, 0);
});
const parent = renderComponent(Parent);
expect(toHtml(parent))
.toEqual(
'<child><div id="first"><span class="other toFirst">1</span></div><div id="second"><span class="toSecond noise">2</span></div></child>');
});
it('should project nodes into the first matching selector', () => {
/**
* <div id="first"><ng-content select="span"></ng-content></div>
* <div id="second"><ng-content select="span.toSecond"></ng-content></div>
*/
const Child = createComponent('child', function(ctx: any, cm: boolean) {
if (cm) {
m(0, dP([[[['span'], null]], [[['span', 'class', 'toSecond'], null]]]));
E(0, 'div', ['id', 'first']);
{ P(1, 0, 1); }
e();
E(2, 'div', ['id', 'second']);
{ P(3, 0, 2); }
e();
}
});
/**
* <child>
* <span class="toFirst">1</span>
* <span class="toSecond">2</span>
* </child>
*/
const Parent = createComponent('parent', function(ctx: any, cm: boolean) {
if (cm) {
E(0, Child.ngComponentDef);
{
D(0, Child.ngComponentDef.n(), Child.ngComponentDef);
E(1, 'span', ['class', 'toFirst']);
{ T(2, '1'); }
e();
E(3, 'span', ['class', 'toSecond']);
{ T(4, '2'); }
e();
}
e();
}
Child.ngComponentDef.r(0, 0);
});
const parent = renderComponent(Parent);
expect(toHtml(parent))
.toEqual(
'<child><div id="first"><span class="toFirst">1</span><span class="toSecond">2</span></div><div id="second"></div></child>');
});
it('should allow mixing ng-content with and without selectors', () => {
/**
* <div id="first"><ng-content select="span.toFirst"></ng-content></div>
* <div id="second"><ng-content></ng-content></div>
*/
const Child = createComponent('child', function(ctx: any, cm: boolean) {
if (cm) {
m(0, dP([[[['span', 'class', 'toFirst'], null]]]));
E(0, 'div', ['id', 'first']);
{ P(1, 0, 1); }
e();
E(2, 'div', ['id', 'second']);
{ P(3, 0); }
e();
}
});
/**
* <child>
* <span class="other toFirst">1</span>
* <span class="toSecond noise">2</span>
* </child>
*/
const Parent = createComponent('parent', function(ctx: any, cm: boolean) {
if (cm) {
E(0, Child.ngComponentDef);
{
D(0, Child.ngComponentDef.n(), Child.ngComponentDef);
E(1, 'span', ['class', 'toFirst']);
{ T(2, '1'); }
e();
E(3, 'span');
{ T(4, 'remaining'); }
e();
T(5, 'more remaining');
}
e();
}
Child.ngComponentDef.r(0, 0);
});
const parent = renderComponent(Parent);
expect(toHtml(parent))
.toEqual(
'<child><div id="first"><span class="toFirst">1</span></div><div id="second"><span>remaining</span>more remaining</div></child>');
});
it('should allow mixing ng-content with and without selectors - ng-content first', () => {
/**
* <div id="first"><ng-content></ng-content></div>
* <div id="second"><ng-content select="span.toSecond"></ng-content></div>
*/
const Child = createComponent('child', function(ctx: any, cm: boolean) {
if (cm) {
m(0, dP([[[['span', 'class', 'toSecond'], null]]]));
E(0, 'div', ['id', 'first']);
{ P(1, 0); }
e();
E(2, 'div', ['id', 'second']);
{ P(3, 0, 1); }
e();
}
});
/**
* <child>
* <span>1</span>
* <span class="toSecond">2</span>
* remaining
* </child>
*/
const Parent = createComponent('parent', function(ctx: any, cm: boolean) {
if (cm) {
E(0, Child.ngComponentDef);
{
D(0, Child.ngComponentDef.n(), Child.ngComponentDef);
E(1, 'span');
{ T(2, '1'); }
e();
E(3, 'span', ['class', 'toSecond']);
{ T(4, '2'); }
e();
T(5, 'remaining');
}
e();
}
Child.ngComponentDef.r(0, 0);
});
const parent = renderComponent(Parent);
expect(toHtml(parent))
.toEqual(
'<child><div id="first"><span>1</span>remaining</div><div id="second"><span class="toSecond">2</span></div></child>');
});
/**
* Descending into projected content for selector-matching purposes is not supported
* today: http://plnkr.co/edit/MYQcNfHSTKp9KvbzJWVQ?p=preview
*/
it('should not match selectors on re-projected content', () => {
/**
* <ng-content select="span"></ng-content>
* <hr>
* <ng-content></ng-content>
*/
const GrandChild = createComponent('grand-child', function(ctx: any, cm: boolean) {
if (cm) {
m(0, dP([[[['span'], null]]]));
P(0, 0, 1);
E(1, 'hr');
e();
P(2, 0, 0);
}
});
/**
* <grand-child>
* <ng-content></ng-content>
* <span>in child template</span>
* </grand-child>
*/
const Child = createComponent('child', function(ctx: any, cm: boolean) {
if (cm) {
m(0, dP());
E(0, GrandChild.ngComponentDef);
{
D(0, GrandChild.ngComponentDef.n(), GrandChild.ngComponentDef);
P(1, 0);
E(2, 'span');
{ T(3, 'in child template'); }
e();
}
e();
GrandChild.ngComponentDef.r(0, 0);
}
});
/**
* <child>
* <div>
* parent content
* </div>
* </child>
*/
const Parent = createComponent('parent', function(ctx: any, cm: boolean) {
if (cm) {
E(0, Child.ngComponentDef);
{
D(0, Child.ngComponentDef.n(), Child.ngComponentDef);
E(1, 'span');
{ T(2, 'parent content'); }
e();
}
e();
}
Child.ngComponentDef.r(0, 0);
});
const parent = renderComponent(Parent);
expect(toHtml(parent))
.toEqual(
'<child><grand-child><span>in child template</span><hr><span>parent content</span></grand-child></child>');
});
it('should match selectors against projected containers', () => {
/**
* <span>
* <ng-content select="div"></ng-content>
* </span>
*/
const Child = createComponent('child', function(ctx: any, cm: boolean) {
if (cm) {
m(0, dP([[[['div'], null]]]));
E(0, 'span');
{ P(1, 0, 1); }
e();
}
});
/**
* <child>
* <div *ngIf="true">content</div>
* </child>
*/
const Parent = createComponent('parent', function(ctx: {value: any}, cm: boolean) {
if (cm) {
E(0, Child.ngComponentDef);
{
D(0, Child.ngComponentDef.n(), Child.ngComponentDef);
C(1, undefined, 'div');
c();
}
e();
}
rC(1);
{
if (true) {
if (V(0)) {
E(0, 'div');
{ T(1, 'content'); }
e();
}
v();
}
}
rc();
Child.ngComponentDef.r(0, 0);
});
const parent = renderComponent(Parent);
expect(toHtml(parent)).toEqual('<child><span><div>content</div></span></child>');
});
});
});

View File

@ -0,0 +1,613 @@
/**
* @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 {C, E, T, V, b, c, e, rC, rc, t, v} from '../../src/render3/index';
import {renderToHtml} from './render_util';
describe('JS control flow', () => {
it('should work with if block', () => {
const ctx: {message: string | null, condition: boolean} = {message: 'Hello', condition: true};
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'div');
{
C(1);
c();
}
e();
}
rC(1);
{
if (ctx.condition) {
let cm1 = V(1);
{
if (cm1) {
E(0, 'span');
{ T(1); }
e();
}
t(1, b(ctx.message));
}
v();
}
}
rc();
}
expect(renderToHtml(Template, ctx)).toEqual('<div><span>Hello</span></div>');
ctx.condition = false;
ctx.message = 'Hi!';
expect(renderToHtml(Template, ctx)).toEqual('<div></div>');
ctx.condition = true;
expect(renderToHtml(Template, ctx)).toEqual('<div><span>Hi!</span></div>');
});
it('should work with nested if blocks', () => {
const ctx: {condition: boolean, condition2: boolean} = {condition: true, condition2: true};
/**
* <div>
* % if(ctx.condition) {
* <span>
* % if(ctx.condition2) {
* Hello
* % }
* </span>
* % }
* </div>
*/
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'div');
{
C(1);
c();
}
e();
}
rC(1);
{
if (ctx.condition) {
let cm1 = V(1);
{
if (cm1) {
E(0, 'span');
{
C(1);
c();
}
e();
}
rC(1);
{
if (ctx.condition2) {
let cm2 = V(2);
{
if (cm2) {
T(0, 'Hello');
}
}
v();
}
}
rc();
}
v();
}
}
rc();
}
expect(renderToHtml(Template, ctx)).toEqual('<div><span>Hello</span></div>');
ctx.condition = false;
expect(renderToHtml(Template, ctx)).toEqual('<div></div>');
ctx.condition = true;
expect(renderToHtml(Template, ctx)).toEqual('<div><span>Hello</span></div>');
ctx.condition2 = false;
expect(renderToHtml(Template, ctx)).toEqual('<div><span></span></div>');
ctx.condition2 = true;
expect(renderToHtml(Template, ctx)).toEqual('<div><span>Hello</span></div>');
ctx.condition2 = false;
expect(renderToHtml(Template, ctx)).toEqual('<div><span></span></div>');
ctx.condition = false;
expect(renderToHtml(Template, ctx)).toEqual('<div></div>');
ctx.condition = true;
expect(renderToHtml(Template, ctx)).toEqual('<div><span></span></div>');
ctx.condition2 = true;
expect(renderToHtml(Template, ctx)).toEqual('<div><span>Hello</span></div>');
});
it('should work with containers with views as parents', () => {
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'div');
{ T(1, 'hello'); }
e();
C(2);
c();
}
rC(2);
{
if (ctx.condition1) {
let cm0 = V(0);
{
if (cm0) {
C(0);
c();
}
rC(0);
{
if (ctx.condition2) {
let cm0 = V(0);
{
if (cm0) {
T(0, 'world');
}
}
v();
}
}
rc();
}
v();
}
}
rc();
}
expect(renderToHtml(Template, {condition1: true, condition2: true}))
.toEqual('<div>hello</div>world');
expect(renderToHtml(Template, {condition1: false, condition2: false}))
.toEqual('<div>hello</div>');
});
it('should work with loop block', () => {
const ctx: {data: string[] | null} = {data: ['a', 'b', 'c']};
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'ul');
{
C(1);
c();
}
e();
}
rC(1);
{
for (let i = 0; i < ctx.data.length; i++) {
let cm1 = V(1);
{
if (cm1) {
E(0, 'li');
{ T(1); }
e();
}
t(1, b(ctx.data[i]));
}
v();
}
}
rc();
}
expect(renderToHtml(Template, ctx)).toEqual('<ul><li>a</li><li>b</li><li>c</li></ul>');
ctx.data = ['e', 'f'];
expect(renderToHtml(Template, ctx)).toEqual('<ul><li>e</li><li>f</li></ul>');
ctx.data = [];
expect(renderToHtml(Template, ctx)).toEqual('<ul></ul>');
ctx.data = ['a', 'b', 'c'];
expect(renderToHtml(Template, ctx)).toEqual('<ul><li>a</li><li>b</li><li>c</li></ul>');
ctx.data.push('d');
expect(renderToHtml(Template, ctx))
.toEqual('<ul><li>a</li><li>b</li><li>c</li><li>d</li></ul>');
ctx.data = ['e'];
expect(renderToHtml(Template, ctx)).toEqual('<ul><li>e</li></ul>');
});
it('should work with nested loop blocks', () => {
const ctx: {data: string[][] | null} = {data: [['a', 'b', 'c'], ['m', 'n']]};
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'ul');
{
C(1);
c();
}
e();
}
rC(1);
{
for (let i = 0; i < ctx.data[0].length; i++) {
let cm1 = V(1);
{
if (cm1) {
E(0, 'li');
{
C(1);
c();
}
e();
}
rC(1);
{
ctx.data[1].forEach((value: string, ind: number) => {
if (V(2)) {
T(0);
}
t(0, b(ctx.data[0][i] + value));
v();
});
}
rc();
}
v();
}
}
rc();
}
expect(renderToHtml(Template, ctx)).toEqual('<ul><li>aman</li><li>bmbn</li><li>cmcn</li></ul>');
ctx.data = [[], []];
expect(renderToHtml(Template, ctx)).toEqual('<ul></ul>');
});
it('should work with nested loop blocks where nested container is a root node', () => {
/**
* <div>
* Before
* % for (let i = 0; i < cafes.length; i++) {
* <h2> {{ cafes[i].name }} </h2>
* % for (let j = 0; j < cafes[i].entrees; j++) {
* {{ cafes[i].entrees[j] }}
* % }
* -
* % }
* After
* <div>
*/
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'div');
{
T(1, 'Before');
C(2);
c();
T(3, 'After');
}
e();
}
rC(2);
{
for (let i = 0; i < ctx.cafes.length; i++) {
let cm1 = V(1);
{
if (cm1) {
E(0, 'h2');
{ T(1); }
e();
C(2);
c();
T(3, '-');
}
t(1, b(ctx.cafes[i].name));
rC(2);
{
for (let j = 0; j < ctx.cafes[i].entrees.length; j++) {
if (V(1)) {
T(0);
}
t(0, b(ctx.cafes[i].entrees[j]));
v();
}
}
rc();
}
v();
}
}
rc();
}
const ctx = {
cafes: [
{name: '1', entrees: ['a', 'b', 'c']}, {name: '2', entrees: ['d', 'e', 'f']},
{name: '3', entrees: ['g', 'h', 'i']}
]
};
expect(renderToHtml(Template, ctx))
.toEqual('<div>Before<h2>1</h2>abc-<h2>2</h2>def-<h2>3</h2>ghi-After</div>');
ctx.cafes = [];
expect(renderToHtml(Template, ctx)).toEqual('<div>BeforeAfter</div>');
ctx.cafes = [
{name: '1', entrees: ['a', 'c']},
{name: '2', entrees: ['d', 'e']},
];
expect(renderToHtml(Template, ctx)).toEqual('<div>Before<h2>1</h2>ac-<h2>2</h2>de-After</div>');
});
it('should work with loop blocks nested three deep', () => {
/**
* <div>
* Before
* % for (let i = 0; i < cafes.length; i++) {
* <h2> {{ cafes[i].name }} </h2>
* % for (let j = 0; j < cafes[i].entrees.length; j++) {
* <h3> {{ cafes[i].entrees[j].name }} </h3>
* % for (let k = 0; k < cafes[i].entrees[j].foods.length; k++) {
* {{ cafes[i].entrees[j].foods[k] }}
* % }
* % }
* -
* % }
* After
* <div>
*/
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'div');
{
T(1, 'Before');
C(2);
c();
T(3, 'After');
}
e();
}
rC(2);
{
for (let i = 0; i < ctx.cafes.length; i++) {
let cm1 = V(1);
{
if (cm1) {
E(0, 'h2');
{ T(1); }
e();
C(2);
c();
T(3, '-');
}
t(1, b(ctx.cafes[i].name));
rC(2);
{
for (let j = 0; j < ctx.cafes[i].entrees.length; j++) {
let cm1 = V(1);
{
if (cm1) {
E(0, 'h3');
{ T(1); }
e();
C(2);
c();
}
t(1, b(ctx.cafes[i].entrees[j].name));
rC(2);
{
for (let k = 0; k < ctx.cafes[i].entrees[j].foods.length; k++) {
if (V(1)) {
T(0);
}
t(0, b(ctx.cafes[i].entrees[j].foods[k]));
v();
}
}
rc();
}
v();
}
}
rc();
}
v();
}
}
rc();
}
const ctx = {
cafes: [
{
name: '1',
entrees:
[{name: 'a', foods: [1, 2]}, {name: 'b', foods: [3, 4]}, {name: 'c', foods: [5, 6]}]
},
{
name: '2',
entrees: [
{name: 'd', foods: [1, 2]}, {name: 'e', foods: [3, 4]}, {name: 'f', foods: [5, 6]}
]
}
]
};
expect(renderToHtml(Template, ctx))
.toEqual(
'<div>' +
'Before' +
'<h2>1</h2><h3>a</h3>12<h3>b</h3>34<h3>c</h3>56-' +
'<h2>2</h2><h3>d</h3>12<h3>e</h3>34<h3>f</h3>56-' +
'After' +
'</div>');
ctx.cafes = [];
expect(renderToHtml(Template, ctx)).toEqual('<div>BeforeAfter</div>');
});
it('should work with if/else blocks', () => {
const ctx: {message: string | null, condition: boolean} = {message: 'Hello', condition: true};
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'div');
{
C(1);
c();
}
e();
}
rC(1);
{
if (ctx.condition) {
let cm1 = V(1);
{
if (cm1) {
E(0, 'span');
{ T(1, 'Hello'); }
e();
}
}
v();
} else {
let cm2 = V(2);
{
if (cm2) {
E(0, 'div');
{ T(1, 'Goodbye'); }
e();
}
}
v();
}
}
rc();
}
expect(renderToHtml(Template, ctx)).toEqual('<div><span>Hello</span></div>');
ctx.condition = false;
expect(renderToHtml(Template, ctx)).toEqual('<div><div>Goodbye</div></div>');
ctx.condition = true;
expect(renderToHtml(Template, ctx)).toEqual('<div><span>Hello</span></div>');
});
});
describe('JS for loop', () => {
it('should work with sibling for blocks', () => {
const ctx: {data1: string[] | null,
data2: number[] | null} = {data1: ['a', 'b', 'c'], data2: [1, 2]};
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'div');
{
C(1);
c();
}
e();
}
rC(1);
{
for (let i = 0; i < ctx.data1.length; i++) {
if (V(1)) {
T(0);
}
t(0, b(ctx.data1[i]));
v();
}
for (let j = 0; j < ctx.data2.length; j++) {
if (V(2)) {
T(0);
}
t(0, b(ctx.data2[j]));
v();
}
}
rc();
}
expect(renderToHtml(Template, ctx)).toEqual('<div>abc12</div>');
ctx.data1 = ['e', 'f'];
expect(renderToHtml(Template, ctx)).toEqual('<div>ef12</div>');
ctx.data2 = [8];
expect(renderToHtml(Template, ctx)).toEqual('<div>ef8</div>');
ctx.data1 = ['x', 'y'];
expect(renderToHtml(Template, ctx)).toEqual('<div>xy8</div>');
});
});
describe('function calls', () => {
it('should work', () => {
const ctx: {data: string[]} = {data: ['foo', 'bar']};
function spanify(ctx: {message: string | null}, cm: boolean) {
const message = ctx.message;
if (cm) {
E(0, 'span');
{ T(1); }
e();
}
t(1, b(message));
}
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'div');
{
T(1, 'Before');
C(2);
c();
C(3);
c();
T(4, 'After');
}
e();
}
rC(2);
{
let cm0 = V(0);
{ spanify({message: ctx.data[0]}, cm0); }
v();
}
rc();
rC(3);
{
let cm0 = V(0);
{ spanify({message: ctx.data[1]}, cm0); }
v();
}
rc();
}
expect(renderToHtml(Template, ctx))
.toEqual('<div>Before<span>foo</span><span>bar</span>After</div>');
ctx.data = [];
expect(renderToHtml(Template, ctx)).toEqual('<div>Before<span></span><span></span>After</div>');
});
});

View File

@ -0,0 +1,339 @@
/**
* @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 {ElementRef, TemplateRef, ViewContainerRef} from '@angular/core';
import {bloomFindPossibleInjector} from '../../src/render3/di';
import {C, D, E, PublicFeature, T, V, b, b2, c, defineDirective, e, inject, injectElementRef, injectTemplateRef, injectViewContainerRef, rC, rc, t, v} from '../../src/render3/index';
import {bloomAdd, createNode, createViewState, enterView, getOrCreateNodeInjector, leaveView} from '../../src/render3/instructions';
import {LNodeFlags, LNodeInjector} from '../../src/render3/interfaces';
import {renderToHtml} from './render_util';
describe('di', () => {
describe('no dependencies', () => {
it('should create directive with no deps', () => {
class Directive {
value: string = 'Created';
}
const DirectiveDef = defineDirective({type: Directive, factory: () => new Directive});
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'div');
{
D(0, DirectiveDef.n(), DirectiveDef);
T(1);
}
e();
}
t(1, b(D<Directive>(0).value));
}
expect(renderToHtml(Template, {})).toEqual('<div>Created</div>');
});
});
describe('view dependencies', () => {
it('should create directive with inter view dependencies', () => {
class DirectiveA {
value: string = 'A';
}
const DirectiveADef = defineDirective(
{type: DirectiveA, factory: () => new DirectiveA, features: [PublicFeature]});
class DirectiveB {
value: string = 'B';
}
const DirectiveBDef = defineDirective(
{type: DirectiveB, factory: () => new DirectiveB, features: [PublicFeature]});
class DirectiveC {
value: string;
constructor(a: DirectiveA, b: DirectiveB) { this.value = a.value + b.value; }
}
const DirectiveCDef = defineDirective({
type: DirectiveC,
factory: () => new DirectiveC(inject(DirectiveA), inject(DirectiveB))
});
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'div');
{
D(0, DirectiveADef.n(), DirectiveADef);
E(1, 'span');
{
D(1, DirectiveBDef.n(), DirectiveBDef);
D(2, DirectiveCDef.n(), DirectiveCDef);
T(2);
}
e();
}
e();
}
t(2, b(D<DirectiveC>(2).value));
}
expect(renderToHtml(Template, {})).toEqual('<div><span>AB</span></div>');
});
});
describe('ElementRef', () => {
it('should create directive with ElementRef dependencies', () => {
class Directive {
value: string;
constructor(public elementRef: ElementRef) {
this.value = (elementRef.constructor as any).name;
}
}
const DirectiveDef = defineDirective({
type: Directive,
factory: () => new Directive(injectElementRef()),
features: [PublicFeature]
});
class DirectiveSameInstance {
value: boolean;
constructor(elementRef: ElementRef, directive: Directive) {
this.value = elementRef === directive.elementRef;
}
}
const DirectiveSameInstanceDef = defineDirective({
type: DirectiveSameInstance,
factory: () => new DirectiveSameInstance(injectElementRef(), inject(Directive))
});
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'div');
{
D(0, DirectiveDef.n(), DirectiveDef);
D(1, DirectiveSameInstanceDef.n(), DirectiveSameInstanceDef);
T(1);
}
e();
}
t(1, b2('', D<Directive>(0).value, '-', D<DirectiveSameInstance>(1).value, ''));
}
expect(renderToHtml(Template, {})).toEqual('<div>ElementRef-true</div>');
});
});
describe('TemplateRef', () => {
it('should create directive with TemplateRef dependencies', () => {
class Directive {
value: string;
constructor(public templateRef: TemplateRef<any>) {
this.value = (templateRef.constructor as any).name;
}
}
const DirectiveDef = defineDirective({
type: Directive,
factory: () => new Directive(injectTemplateRef()),
features: [PublicFeature]
});
class DirectiveSameInstance {
value: boolean;
constructor(templateRef: TemplateRef<any>, directive: Directive) {
this.value = templateRef === directive.templateRef;
}
}
const DirectiveSameInstanceDef = defineDirective({
type: DirectiveSameInstance,
factory: () => new DirectiveSameInstance(injectTemplateRef(), inject(Directive))
});
function Template(ctx: any, cm: any) {
if (cm) {
C(0, function() {});
{
D(0, DirectiveDef.n(), DirectiveDef);
D(1, DirectiveSameInstanceDef.n(), DirectiveSameInstanceDef);
}
c();
T(1);
}
t(1, b2('', D<Directive>(0).value, '-', D<DirectiveSameInstance>(1).value, ''));
}
expect(renderToHtml(Template, {})).toEqual('TemplateRef-true');
});
});
describe('ViewContainerRef', () => {
it('should create directive with ViewContainerRef dependencies', () => {
class Directive {
value: string;
constructor(public viewContainerRef: ViewContainerRef) {
this.value = (viewContainerRef.constructor as any).name;
}
}
const DirectiveDef = defineDirective({
type: Directive,
factory: () => new Directive(injectViewContainerRef()),
features: [PublicFeature]
});
class DirectiveSameInstance {
value: boolean;
constructor(viewContainerRef: ViewContainerRef, directive: Directive) {
this.value = viewContainerRef === directive.viewContainerRef;
}
}
const DirectiveSameInstanceDef = defineDirective({
type: DirectiveSameInstance,
factory: () => new DirectiveSameInstance(injectViewContainerRef(), inject(Directive))
});
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'div');
{
D(0, DirectiveDef.n(), DirectiveDef);
D(1, DirectiveSameInstanceDef.n(), DirectiveSameInstanceDef);
T(1);
}
e();
}
t(1, b2('', D<Directive>(0).value, '-', D<DirectiveSameInstance>(1).value, ''));
}
expect(renderToHtml(Template, {})).toEqual('<div>ViewContainerRef-true</div>');
});
});
describe('inject', () => {
describe('bloom filter', () => {
let di: LNodeInjector;
beforeEach(() => {
di = {} as any;
di.bf0 = 0;
di.bf1 = 0;
di.bf2 = 0;
di.bf3 = 0;
di.cbf0 = 0;
di.cbf1 = 0;
di.cbf2 = 0;
di.cbf3 = 0;
});
function bloomState() { return [di.bf3, di.bf2, di.bf1, di.bf0]; }
it('should add values', () => {
bloomAdd(di, { __NG_ELEMENT_ID__: 0 } as any);
expect(bloomState()).toEqual([0, 0, 0, 1]);
bloomAdd(di, { __NG_ELEMENT_ID__: 32 + 1 } as any);
expect(bloomState()).toEqual([0, 0, 2, 1]);
bloomAdd(di, { __NG_ELEMENT_ID__: 64 + 2 } as any);
expect(bloomState()).toEqual([0, 4, 2, 1]);
bloomAdd(di, { __NG_ELEMENT_ID__: 96 + 3 } as any);
expect(bloomState()).toEqual([8, 4, 2, 1]);
});
it('should query values', () => {
bloomAdd(di, { __NG_ELEMENT_ID__: 0 } as any);
bloomAdd(di, { __NG_ELEMENT_ID__: 32 } as any);
bloomAdd(di, { __NG_ELEMENT_ID__: 64 } as any);
bloomAdd(di, { __NG_ELEMENT_ID__: 96 } as any);
expect(bloomFindPossibleInjector(di, 0)).toEqual(di);
expect(bloomFindPossibleInjector(di, 1)).toEqual(null);
expect(bloomFindPossibleInjector(di, 32)).toEqual(di);
expect(bloomFindPossibleInjector(di, 64)).toEqual(di);
expect(bloomFindPossibleInjector(di, 96)).toEqual(di);
});
});
it('should inject from parent view', () => {
class ParentDirective {}
const ParentDirectiveDef = defineDirective(
{type: ParentDirective, factory: () => new ParentDirective(), features: [PublicFeature]});
class ChildDirective {
value: string;
constructor(public parent: ParentDirective) {
this.value = (parent.constructor as any).name;
}
}
const ChildDirectiveDef = defineDirective({
type: ChildDirective,
factory: () => new ChildDirective(inject(ParentDirective)),
features: [PublicFeature]
});
class Child2Directive {
value: boolean;
constructor(parent: ParentDirective, child: ChildDirective) {
this.value = parent === child.parent;
}
}
const Child2DirectiveDef = defineDirective({
type: Child2Directive,
factory: () => new Child2Directive(inject(ParentDirective), inject(ChildDirective))
});
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'div');
{
D(0, ParentDirectiveDef.n(), ParentDirectiveDef);
C(1);
c();
}
e();
}
rC(1);
{
if (V(0)) {
E(0, 'span');
{
D(0, ChildDirectiveDef.n(), ChildDirectiveDef);
D(1, Child2DirectiveDef.n(), Child2DirectiveDef);
T(1);
}
e();
}
t(1, b2('', D<ChildDirective>(0).value, '-', D<Child2Directive>(1).value, ''));
v();
}
rc();
}
expect(renderToHtml(Template, {})).toEqual('<div><span>ParentDirective-true</span></div>');
});
it('should inject from module Injector', () => {
});
});
describe('getOrCreateNodeInjector', () => {
it('should handle initial undefined state', () => {
const contentView = createViewState(-1, null !);
const oldView = enterView(contentView, null !);
try {
const parent = createNode(0, LNodeFlags.Element, null, null);
// Simulate the situation where the previous parent is not initialized.
// This happens on first bootstrap because we don't init existing values
// so that we have smaller HelloWorld.
(parent as{parent: any}).parent = undefined;
const injector = getOrCreateNodeInjector();
expect(injector).not.toBe(null);
} finally {
leaveView(oldView);
}
});
});
});

View File

@ -0,0 +1,46 @@
/**
* @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 {D, E, b, defineDirective, e, p} from '../../src/render3/index';
import {renderToHtml} from './render_util';
describe('directive', () => {
describe('host', () => {
it('should support host bindings in directives', () => {
let directiveInstance: Directive|undefined;
class Directive {
klass = 'foo';
}
const DirectiveDef = defineDirective({
type: Directive,
factory: () => directiveInstance = new Directive,
refresh: (directiveIndex: number, elementIndex: number) => {
p(elementIndex, 'className', b(D<Directive>(directiveIndex).klass));
}
});
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'span');
{ D(0, DirectiveDef.n(), DirectiveDef); }
e();
}
DirectiveDef.r(0, 0);
}
expect(renderToHtml(Template, {})).toEqual('<span class="foo"></span>');
directiveInstance !.klass = 'bar';
expect(renderToHtml(Template, {})).toEqual('<span class="bar"></span>');
});
});
});

12
packages/core/test/render3/domino.d.ts vendored Normal file
View File

@ -0,0 +1,12 @@
/**
* @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
*/
declare module 'domino' {
function createWindow(html: string, url: string): Window;
const impl: {Element: any};
}

View File

@ -0,0 +1,295 @@
/**
* @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 {C, D, E, T, V, a, b, c, defineComponent, defineDirective, e, k, p, rC, rc, t, v} from '../../src/render3/index';
import {renderToHtml} from './render_util';
describe('exports', () => {
it('should support export of DOM element', () => {
/** <input value="one" #myInput> {{ myInput.value }} */
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'input', ['value', 'one']);
e();
T(1);
}
let myInput = E(0);
t(1, (myInput as any).value);
}
expect(renderToHtml(Template, {})).toEqual('<input value="one">one');
});
it('should support basic export of component', () => {
/** <comp #myComp></comp> {{ myComp.name }} */
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, MyComponent.ngComponentDef);
{ D(0, MyComponent.ngComponentDef.n(), MyComponent.ngComponentDef); }
e();
T(1);
}
t(1, D<MyComponent>(0).name);
}
class MyComponent {
name = 'Nancy';
static ngComponentDef = defineComponent({
type: MyComponent,
tag: 'comp',
template: function() {},
factory: () => new MyComponent
});
}
expect(renderToHtml(Template, {})).toEqual('<comp></comp>Nancy');
});
it('should support component instance fed into directive', () => {
let myComponent: MyComponent;
let myDir: MyDir;
class MyComponent {
constructor() { myComponent = this; }
static ngComponentDef = defineComponent({
type: MyComponent,
tag: 'comp',
template: function() {},
factory: () => new MyComponent
});
}
class MyDir {
myDir: MyComponent;
constructor() { myDir = this; }
static ngDirectiveDef =
defineDirective({type: MyDir, factory: () => new MyDir, inputs: {myDir: 'myDir'}});
}
/** <comp #myComp></comp> <div [myDir]="myComp"></div> */
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, MyComponent.ngComponentDef);
{ D(0, MyComponent.ngComponentDef.n(), MyComponent.ngComponentDef); }
e();
E(1, 'div');
{ D(1, MyDir.ngDirectiveDef.n(), MyDir.ngDirectiveDef); }
e();
}
p(1, 'myDir', b(D<MyComponent>(0)));
}
renderToHtml(Template, {});
expect(myDir !.myDir).toEqual(myComponent !);
});
it('should work with directives with exportAs set', () => {
/** <div someDir #myDir="someDir"></div> {{ myDir.name }} */
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'div');
D(0, SomeDirDef.n(), SomeDirDef);
e();
T(1);
}
t(1, D<SomeDir>(0).name);
}
class SomeDir {
name = 'Drew';
}
const SomeDirDef = defineDirective({type: SomeDir, factory: () => new SomeDir});
expect(renderToHtml(Template, {})).toEqual('<div></div>Drew');
});
describe('forward refs', () => {
it('should work with basic text bindings', () => {
/** {{ myInput.value}} <input value="one" #myInput> */
function Template(ctx: any, cm: boolean) {
if (cm) {
T(0);
E(1, 'input', ['value', 'one']);
e();
}
let myInput = E(1);
t(0, b((myInput as any).value));
}
expect(renderToHtml(Template, {})).toEqual('one<input value="one">');
});
it('should work with element properties', () => {
/** <div [title]="myInput.value"</div> <input value="one" #myInput> */
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'div');
e();
E(1, 'input', ['value', 'one']);
e();
}
let myInput = E(1);
p(0, 'title', b(myInput && (myInput as any).value));
}
expect(renderToHtml(Template, {})).toEqual('<div title="one"></div><input value="one">');
});
it('should work with element attrs', () => {
/** <div [attr.aria-label]="myInput.value"</div> <input value="one" #myInput> */
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'div');
e();
E(1, 'input', ['value', 'one']);
e();
}
let myInput = E(1);
a(0, 'aria-label', b(myInput && (myInput as any).value));
}
expect(renderToHtml(Template, {})).toEqual('<div aria-label="one"></div><input value="one">');
});
it('should work with element classes', () => {
/** <div [class.red]="myInput.checked"</div> <input type="checkbox" checked #myInput> */
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'div');
e();
E(1, 'input', ['type', 'checkbox', 'checked', 'true']);
e();
}
let myInput = E(1);
k(0, 'red', b(myInput && (myInput as any).checked));
}
expect(renderToHtml(Template, {}))
.toEqual('<div class="red"></div><input type="checkbox" checked="true">');
});
it('should work with component refs', () => {
let myComponent: MyComponent;
let myDir: MyDir;
class MyComponent {
constructor() { myComponent = this; }
static ngComponentDef = defineComponent({
type: MyComponent,
tag: 'comp',
template: function(ctx: MyComponent, cm: boolean) {},
factory: () => new MyComponent
});
}
class MyDir {
myDir: MyComponent;
constructor() { myDir = this; }
static ngDirectiveDef =
defineDirective({type: MyDir, factory: () => new MyDir, inputs: {myDir: 'myDir'}});
}
/** <div [myDir]="myComp"></div><comp #myComp></comp> */
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'div');
{ D(0, MyDir.ngDirectiveDef.n(), MyDir.ngDirectiveDef); }
e();
E(1, MyComponent.ngComponentDef);
{ D(1, MyComponent.ngComponentDef.n(), MyComponent.ngComponentDef); }
e();
}
p(0, 'myDir', b(D<MyComponent>(1)));
}
renderToHtml(Template, {});
expect(myDir !.myDir).toEqual(myComponent !);
});
it('should work with multiple forward refs', () => {
/** {{ myInput.value }} {{ myComp.name }} <comp #myComp></comp> <input value="one" #myInput>
*/
function Template(ctx: any, cm: boolean) {
if (cm) {
T(0);
T(1);
E(2, 'comp');
{ D(0, MyComponent.ngComponentDef.n(), MyComponent.ngComponentDef); }
e();
E(3, 'input', ['value', 'one']);
e();
}
let myInput = E(3);
let myComp = D(0) as MyComponent;
t(0, b(myInput && (myInput as any).value));
t(1, b(myComp && myComp.name));
}
let myComponent: MyComponent;
class MyComponent {
name = 'Nancy';
constructor() { myComponent = this; }
static ngComponentDef = defineComponent({
type: MyComponent,
tag: 'comp',
template: function() {},
factory: () => new MyComponent
});
}
expect(renderToHtml(Template, {})).toEqual('oneNancy<comp></comp><input value="one">');
});
it('should work inside a view container', () => {
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'div');
{
C(1);
c();
}
e();
}
rC(1);
{
if (ctx.condition) {
let cm1 = V(1);
{
if (cm1) {
T(0);
E(1, 'input', ['value', 'one']);
e();
}
let myInput = E(1);
t(0, b(myInput && (myInput as any).value));
}
v();
}
}
rc();
}
expect(renderToHtml(Template, {
condition: true
})).toEqual('<div>one<input value="one"></div>');
expect(renderToHtml(Template, {condition: false})).toEqual('<div></div>');
});
});
});

View File

@ -0,0 +1,57 @@
/**
* @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 {EventEmitter, NgZone, Renderer2} from '@angular/core';
import {EventManager, ɵDomEventsPlugin, ɵDomRendererFactory2, ɵDomSharedStylesHost} from '@angular/platform-browser';
// Adapted renderer: it creates a Renderer2 instance and adapts it to Renderer3
// TODO: remove once this code is in angular/angular
export class NoopNgZone implements NgZone {
readonly hasPendingMicrotasks: boolean = false;
readonly hasPendingMacrotasks: boolean = false;
readonly isStable: boolean = true;
readonly onUnstable: EventEmitter<any> = new EventEmitter();
readonly onMicrotaskEmpty: EventEmitter<any> = new EventEmitter();
readonly onStable: EventEmitter<any> = new EventEmitter();
readonly onError: EventEmitter<any> = new EventEmitter();
run(fn: () => any): any { return fn(); }
runGuarded(fn: () => any): any { return fn(); }
runOutsideAngular(fn: () => any): any { return fn(); }
runTask<T>(fn: () => any): T { return fn(); }
}
// TODO: remove once this code is in angular/angular
export class SimpleDomEventsPlugin extends ɵDomEventsPlugin {
constructor(doc: any, ngZone: NgZone) { super(doc, ngZone); }
supports(eventName: string): boolean { return true; }
addEventListener(element: HTMLElement, eventName: string, handler: Function): Function {
let callback: EventListener = handler as EventListener;
element.addEventListener(eventName, callback, false);
return () => this.removeEventListener(element, eventName, callback);
}
removeEventListener(target: any, eventName: string, callback: Function): void {
return target.removeEventListener.apply(target, [eventName, callback, false]);
}
}
export function getRenderer2(document: any): Renderer2 {
const fakeNgZone: NgZone = new NoopNgZone();
const eventManager =
new EventManager([new SimpleDomEventsPlugin(document, fakeNgZone)], fakeNgZone);
const rendererFactory2 =
new ɵDomRendererFactory2(eventManager, new ɵDomSharedStylesHost(document));
return rendererFactory2.createRenderer(null, null);
}

View File

@ -0,0 +1,592 @@
/**
* @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 {C, D, E, NC, T, V, a, b, b1, b2, b3, b4, b5, b6, b7, b8, bV, c, defineComponent, e, k, p, r, rC, rc, s, t, v} from '../../src/render3/index';
import {NO_CHANGE} from '../../src/render3/instructions';
import {containerEl, renderToHtml} from './render_util';
describe('iv integration test', () => {
describe('render', () => {
it('should render basic template', () => {
expect(renderToHtml(Template, {})).toEqual('<span title="Hello">Greetings</span>');
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'span', ['title', 'Hello']);
{ T(1, 'Greetings'); }
e();
}
}
});
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) {
E(0, 'h1');
{ T(1); }
e();
}
t(1, b1('Hello, ', name, '!'));
}
});
});
describe('text bindings', () => {
it('should render "undefined" as "" when used with `bind()`', () => {
function Template(name: string, cm: boolean) {
if (cm) {
T(0);
}
t(0, b(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) {
T(0);
}
t(0, b(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) {
T(0);
}
t(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) {
T(0);
T(1);
T(2);
T(3);
T(4);
T(5);
T(6);
T(7);
T(8);
}
t(0, b1('', cm ? v : NC, '|'));
t(1, b2('', v, '_', cm ? v : NC, '|'));
t(2, b3('', v, '_', v, '_', cm ? v : NC, '|'));
t(3, b4('', v, '_', v, '_', v, '_', cm ? v : NC, '|'));
t(4, b5('', v, '_', v, '_', v, '_', v, '_', cm ? v : NC, '|'));
t(5, b6('', v, '_', v, '_', v, '_', v, '_', v, '_', cm ? v : NC, '|'));
t(6, b7('', v, '_', v, '_', v, '_', v, '_', v, '_', v, '_', cm ? v : NC, '|'));
t(7, b8('', v, '_', v, '_', v, '_', v, '_', v, '_', v, '_', v, '_', cm ? v : NC, '|'));
t(8, bV([
'', v, '_', v, '_', v, '_', v, '_', v, '_', v, '_', v, '_', v, '_', cm ? v : NC, ''
]));
}
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) {
T(0, 'Hello ');
T(1);
T(2, '!');
}
t(1, b(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) {
E(0, 'b');
{
T(1, 'Hello ');
T(2);
T(3, '!');
}
e();
}
t(2, b(name));
}
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) {
E(0, 'b');
{
E(1, 'b');
{
E(2, 'b');
{
E(3, 'b');
{ T(4); }
e();
}
e();
}
e();
}
e();
}
t(4, b1('Hello ', name, '!'));
}
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) {
E(0, 'b');
{
E(1, 'span');
e();
E(2, 'span', ['class', 'foo']);
{}
e();
}
e();
}
a(2, 'id', b(id));
}
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) {
E(0, 'p');
{ T(1, 'hello'); }
e();
T(2, 'world');
}
}
expect(renderToHtml(Template, null)).toEqual('<p>hello</p>world');
});
});
describe('basic components', () => {
class TodoComponent {
value = ' one';
static ngComponentDef = defineComponent({
type: TodoComponent,
tag: 'todo',
template: function TodoTemplate(ctx: any, cm: boolean) {
if (cm) {
E(0, 'p');
{
T(1, 'Todo');
T(2);
}
e();
}
t(2, b(ctx.value));
},
factory: () => new TodoComponent
});
}
it('should support a basic component template', () => {
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, TodoComponent.ngComponentDef);
{ D(0, TodoComponent.ngComponentDef.n(), TodoComponent.ngComponentDef); }
e();
}
TodoComponent.ngComponentDef.r(0, 0);
}
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) {
E(0, TodoComponent.ngComponentDef);
{ D(0, TodoComponent.ngComponentDef.n(), TodoComponent.ngComponentDef); }
e();
T(1, 'two');
}
TodoComponent.ngComponentDef.r(0, 0);
}
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) {
E(0, TodoComponent.ngComponentDef);
{ D(0, TodoComponent.ngComponentDef.n(), TodoComponent.ngComponentDef); }
e();
E(1, TodoComponent.ngComponentDef);
{ D(1, TodoComponent.ngComponentDef.n(), TodoComponent.ngComponentDef); }
e();
}
TodoComponent.ngComponentDef.r(0, 0);
TodoComponent.ngComponentDef.r(1, 1);
}
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({
type: TodoComponentHostBinding,
tag: 'todo',
template: function TodoComponentHostBindingTemplate(
ctx: TodoComponentHostBinding, cm: boolean) {
if (cm) {
T(0);
}
t(0, b(ctx.title));
},
factory: () => cmptInstance = new TodoComponentHostBinding,
refresh: function(directiveIndex: number, elementIndex: number): void {
// host bindings
p(elementIndex, 'title', b(D<TodoComponentHostBinding>(directiveIndex).title));
// refresh component's template
r(directiveIndex, elementIndex, this.template);
}
});
}
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, TodoComponentHostBinding.ngComponentDef);
{
D(0, TodoComponentHostBinding.ngComponentDef.n(),
TodoComponentHostBinding.ngComponentDef);
}
e();
}
TodoComponentHostBinding.ngComponentDef.r(0, 0);
}
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({
type: MyComp,
tag: 'comp',
template: function MyCompTemplate(ctx: any, cm: boolean) {
if (cm) {
E(0, 'p');
{ T(1); }
e();
}
t(1, b(ctx.name));
},
factory: () => new MyComp
});
}
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, MyComp.ngComponentDef);
{ D(0, MyComp.ngComponentDef.n(), MyComp.ngComponentDef); }
e();
}
MyComp.ngComponentDef.r(0, 0);
}
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({
type: MyComp,
tag: 'comp',
template: function MyCompTemplate(ctx: any, cm: boolean) {
if (cm) {
C(0);
c();
}
rC(0);
{
if (ctx.condition) {
if (V(0)) {
E(0, 'div');
{ T(1, 'text'); }
e();
}
v();
}
}
rc();
},
factory: () => new MyComp,
inputs: {condition: 'condition'}
});
}
/** <comp [condition]="condition"></comp> */
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, MyComp.ngComponentDef);
{ D(0, MyComp.ngComponentDef.n(), MyComp.ngComponentDef); }
e();
}
p(0, 'condition', b(ctx.condition));
MyComp.ngComponentDef.r(0, 0);
}
expect(renderToHtml(Template, {condition: true})).toEqual('<comp><div>text</div></comp>');
expect(renderToHtml(Template, {condition: false})).toEqual('<comp></comp>');
});
});
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) {
E(0, 'span');
e();
}
a(0, 'title', b(ctx.title));
}
// 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) {
E(0, 'span');
e();
}
a(0, 'title', b(ctx.title));
}
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) {
E(0, 'b');
e();
}
a(0, 'a', bV(c));
a(0, 'a0', b(c[1]));
a(0, 'a1', b1(c[0], c[1], c[16]));
a(0, 'a2', b2(c[0], c[1], c[2], c[3], c[16]));
a(0, 'a3', b3(c[0], c[1], c[2], c[3], c[4], c[5], c[16]));
a(0, 'a4', b4(c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], c[16]));
a(0, 'a5', b5(c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], c[8], c[9], c[16]));
a(0, 'a6',
b6(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]));
a(0, 'a7', b7(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]));
a(0, 'a8', b8(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(
'<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) {
E(0, 'span');
C(1);
c();
e();
}
a(0, 'title', b(ctx.title));
rC(1);
{
if (true) {
let cm1 = V(1);
{
if (cm1) {
E(0, 'b');
{}
e();
}
a(0, 'title', b(ctx.title));
}
v();
}
}
rc();
}
// 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>');
});
});
describe('elementStyle', () => {
it('should support binding to styles', () => {
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'span');
e();
}
s(0, 'border-color', b(ctx));
}
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) {
E(0, 'span');
e();
}
s(0, 'font-size', b(ctx), 'px');
}
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) {
E(0, 'span');
e();
}
k(0, 'active', b(ctx));
}
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) {
E(0, 'span', ['class', 'existing']);
e();
}
k(0, 'active', b(ctx));
}
expect(renderToHtml(Template, true)).toEqual('<span class="existing active"></span>');
expect(renderToHtml(Template, false)).toEqual('<span class="existing"></span>');
});
});
});
});

View File

@ -0,0 +1,425 @@
/**
* @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 {C, ComponentTemplate, D, E, L, LifeCycleGuard, T, V, b, c, defineComponent, e, l, p, rC, rc, v} from '../../src/render3/index';
import {containerEl, renderToHtml} from './render_util';
describe('lifecycles', () => {
describe('onDestroy', () => {
let events: string[];
beforeEach(() => { events = []; });
let Comp = createOnDestroyComponent('comp', function(ctx: any, cm: boolean) {});
let Parent = createOnDestroyComponent('parent', function(ctx: any, cm: boolean) {
if (cm) {
E(0, Comp.ngComponentDef);
{ D(0, Comp.ngComponentDef.n(), Comp.ngComponentDef); }
e();
}
Comp.ngComponentDef.r(0, 0);
});
function createOnDestroyComponent(name: string, template: ComponentTemplate<any>) {
return class Component {
val: string = '';
ngOnDestroy() { events.push(`${name}${this.val}`); }
static ngComponentDef = defineComponent({
type: Component,
tag: name,
factory: () => {
const comp = new Component();
l(LifeCycleGuard.ON_DESTROY, comp, comp.ngOnDestroy);
return comp;
},
inputs: {val: 'val'},
template: template
});
};
}
it('should call destroy when view is removed', () => {
/**
* % if (condition) {
* <comp></comp>
* % }
*/
function Template(ctx: any, cm: boolean) {
if (cm) {
C(0);
c();
}
rC(0);
{
if (ctx.condition) {
if (V(0)) {
E(0, Comp.ngComponentDef);
{ D(0, Comp.ngComponentDef.n(), Comp.ngComponentDef); }
e();
}
Comp.ngComponentDef.r(0, 0);
v();
}
}
rc();
}
renderToHtml(Template, {condition: true});
renderToHtml(Template, {condition: false});
expect(events).toEqual(['comp']);
});
it('should call destroy when multiple views are removed', () => {
/**
* % if (condition) {
* <comp [val]="1"></comp>
* <comp [val]="2"></comp>
* % }
*/
function Template(ctx: any, cm: boolean) {
if (cm) {
C(0);
c();
}
rC(0);
{
if (ctx.condition) {
if (V(0)) {
E(0, Comp.ngComponentDef);
{ D(0, Comp.ngComponentDef.n(), Comp.ngComponentDef); }
e();
E(1, Comp.ngComponentDef);
{ D(1, Comp.ngComponentDef.n(), Comp.ngComponentDef); }
e();
}
p(0, 'val', b('1'));
p(1, 'val', b('2'));
Comp.ngComponentDef.r(0, 0);
Comp.ngComponentDef.r(1, 1);
v();
}
}
rc();
}
renderToHtml(Template, {condition: true});
renderToHtml(Template, {condition: false});
expect(events).toEqual(['comp1', 'comp2']);
});
it('should be called in child components before parent components', () => {
/**
* % if (condition) {
* <parent></parent>
* % }
*
* parent template: <comp></comp>
*/
function Template(ctx: any, cm: boolean) {
if (cm) {
C(0);
c();
}
rC(0);
{
if (ctx.condition) {
if (V(0)) {
E(0, Parent.ngComponentDef);
{ D(0, Parent.ngComponentDef.n(), Parent.ngComponentDef); }
e();
}
Parent.ngComponentDef.r(0, 0);
v();
}
}
rc();
}
renderToHtml(Template, {condition: true});
renderToHtml(Template, {condition: false});
expect(events).toEqual(['comp', 'parent']);
});
it('should be called bottom up with children nested 2 levels deep', () => {
/**
* % if (condition) {
* <grandparent></grandparent>
* % }
*
* grandparent template: <parent></parent>
* parent template: <comp></comp>
*/
let Grandparent = createOnDestroyComponent('grandparent', function(ctx: any, cm: boolean) {
if (cm) {
E(0, Parent.ngComponentDef);
{ D(0, Parent.ngComponentDef.n(), Parent.ngComponentDef); }
e();
}
Parent.ngComponentDef.r(0, 0);
});
function Template(ctx: any, cm: boolean) {
if (cm) {
C(0);
c();
}
rC(0);
{
if (ctx.condition) {
if (V(0)) {
E(0, Grandparent.ngComponentDef);
{ D(0, Grandparent.ngComponentDef.n(), Grandparent.ngComponentDef); }
e();
}
Grandparent.ngComponentDef.r(0, 0);
v();
}
}
rc();
}
renderToHtml(Template, {condition: true});
renderToHtml(Template, {condition: false});
expect(events).toEqual(['comp', 'parent', 'grandparent']);
});
it('should be called in consistent order if views are removed and re-added', () => {
/**
* % if (condition) {
* <comp [val]="1"></comp>
* % if (condition2) {
* <comp [val]="2"></comp>
* % }
* <comp [val]="3"></comp>
* % }
*/
function Template(ctx: any, cm: boolean) {
if (cm) {
C(0);
c();
}
rC(0);
{
if (ctx.condition) {
if (V(0)) {
E(0, Comp.ngComponentDef);
{ D(0, Comp.ngComponentDef.n(), Comp.ngComponentDef); }
e();
C(1);
c();
E(2, Comp.ngComponentDef);
{ D(1, Comp.ngComponentDef.n(), Comp.ngComponentDef); }
e();
}
p(0, 'val', b('1'));
Comp.ngComponentDef.r(0, 0);
rC(1);
{
if (ctx.condition2) {
if (V(0)) {
E(0, Comp.ngComponentDef);
{ D(0, Comp.ngComponentDef.n(), Comp.ngComponentDef); }
e();
}
p(0, 'val', b('2'));
Comp.ngComponentDef.r(0, 0);
v();
}
}
rc();
p(2, 'val', b('3'));
Comp.ngComponentDef.r(1, 2);
v();
}
}
rc();
}
renderToHtml(Template, {condition: true, condition2: true});
renderToHtml(Template, {condition: false});
/**
* Current angular will process in this same order (root is the top-level removed view):
*
* root.child (comp1 view) onDestroy: null
* root.child.next (container) -> embeddedView
* embeddedView.child (comp2 view) onDestroy: null
* embeddedView onDestroy: [comp2]
* root.child.next.next (comp3 view) onDestroy: null
* root onDestroy: [comp1, comp3]
*/
expect(events).toEqual(['comp2', 'comp1', 'comp3']);
events = [];
renderToHtml(Template, {condition: true, condition2: false});
renderToHtml(Template, {condition: false});
expect(events).toEqual(['comp1', 'comp3']);
events = [];
renderToHtml(Template, {condition: true, condition2: true});
renderToHtml(Template, {condition: false});
expect(events).toEqual(['comp2', 'comp1', 'comp3']);
});
it('should be called in every iteration of a destroyed for loop', () => {
/**
* % if (condition) {
* <comp [val]="1"></comp>
* % for (let i = 2; i < len; i++) {
* <comp [val]="i"></comp>
* % }
* <comp [val]="5"></comp>
* % }
*/
function Template(ctx: any, cm: boolean) {
if (cm) {
C(0);
c();
}
rC(0);
{
if (ctx.condition) {
if (V(0)) {
E(0, Comp.ngComponentDef);
{ D(0, Comp.ngComponentDef.n(), Comp.ngComponentDef); }
e();
C(1);
c();
E(2, Comp.ngComponentDef);
{ D(1, Comp.ngComponentDef.n(), Comp.ngComponentDef); }
e();
}
p(0, 'val', b('1'));
Comp.ngComponentDef.r(0, 0);
rC(1);
{
for (let j = 2; j < ctx.len; j++) {
if (V(0)) {
E(0, Comp.ngComponentDef);
{ D(0, Comp.ngComponentDef.n(), Comp.ngComponentDef); }
e();
}
p(0, 'val', b(j));
Comp.ngComponentDef.r(0, 0);
v();
}
}
rc();
p(2, 'val', b('5'));
Comp.ngComponentDef.r(1, 2);
v();
}
}
rc();
}
/**
* Current angular will process in this same order (root is the top-level removed view):
*
* root.child (comp1 view) onDestroy: null
* root.child.next (container) -> embeddedView (children[0].data)
* embeddedView.child (comp2 view) onDestroy: null
* embeddedView onDestroy: [comp2]
* embeddedView.next.child (comp3 view) onDestroy: null
* embeddedView.next onDestroy: [comp3]
* embeddedView.next.next.child (comp4 view) onDestroy: null
* embeddedView.next.next onDestroy: [comp4]
* embeddedView.next.next -> container -> root
* root onDestroy: [comp1, comp5]
*/
renderToHtml(Template, {condition: true, len: 5});
renderToHtml(Template, {condition: false});
expect(events).toEqual(['comp2', 'comp3', 'comp4', 'comp1', 'comp5']);
events = [];
renderToHtml(Template, {condition: true, len: 4});
renderToHtml(Template, {condition: false});
expect(events).toEqual(['comp2', 'comp3', 'comp1', 'comp5']);
events = [];
renderToHtml(Template, {condition: true, len: 5});
renderToHtml(Template, {condition: false});
expect(events).toEqual(['comp2', 'comp3', 'comp4', 'comp1', 'comp5']);
});
it('should call destroy properly if view also has listeners', () => {
/**
* % if (condition) {
* <button (click)="onClick()">Click me</button>
* <comp></comp>
* <button (click)="onClick()">Click me</button>
* % }
*/
function Template(ctx: any, cm: boolean) {
if (cm) {
C(0);
c();
}
rC(0);
{
if (ctx.condition) {
if (V(0)) {
E(0, 'button');
{
L('click', ctx.onClick.bind(ctx));
T(1, 'Click me');
}
e();
E(2, Comp.ngComponentDef);
{ D(0, Comp.ngComponentDef.n(), Comp.ngComponentDef); }
e();
E(3, 'button');
{
L('click', ctx.onClick.bind(ctx));
T(4, 'Click me');
}
e();
}
Comp.ngComponentDef.r(0, 2);
v();
}
}
rc();
}
class App {
counter = 0;
condition = true;
onClick() { this.counter++; }
}
const ctx: {counter: number} = new App();
renderToHtml(Template, ctx);
const buttons = containerEl.querySelectorAll('button') !;
buttons[0].click();
expect(ctx.counter).toEqual(1);
buttons[1].click();
expect(ctx.counter).toEqual(2);
renderToHtml(Template, {condition: false});
buttons[0].click();
buttons[1].click();
expect(events).toEqual(['comp']);
expect(ctx.counter).toEqual(2);
});
});
});

View File

@ -0,0 +1,324 @@
/**
* @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 {C, D, E, L, T, V, c, defineComponent, e, rC, rc, v} from '../../src/render3/index';
import {containerEl, renderComponent, renderToHtml} from './render_util';
describe('event listeners', () => {
let comps: MyComp[] = [];
class MyComp {
showing = true;
counter = 0;
onClick() { this.counter++; }
static ngComponentDef = defineComponent({
type: MyComp,
tag: 'comp',
/** <button (click)="onClick()"> Click me </button> */
template: function CompTemplate(ctx: any, cm: boolean) {
if (cm) {
E(0, 'button');
{
L('click', ctx.onClick.bind(ctx));
T(1, 'Click me');
}
e();
}
},
factory: () => {
let comp = new MyComp();
comps.push(comp);
return comp;
}
});
}
beforeEach(() => { comps = []; });
it('should call function on event emit', () => {
const comp = renderComponent(MyComp);
const button = containerEl.querySelector('button') !;
button.click();
expect(comp.counter).toEqual(1);
button.click();
expect(comp.counter).toEqual(2);
});
it('should evaluate expression on event emit', () => {
/** <button (click)="showing=!showing"> Click me </button> */
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'button');
{
L('click', () => ctx.showing = !ctx.showing);
T(1, 'Click me');
}
e();
}
}
const ctx = {showing: false};
renderToHtml(Template, ctx);
const button = containerEl.querySelector('button') !;
button.click();
expect(ctx.showing).toBe(true);
button.click();
expect(ctx.showing).toBe(false);
});
it('should support listeners in views', () => {
/**
* % if (ctx.showing) {
* <button (click)="onClick()"> Click me </button>
* % }
*/
function Template(ctx: any, cm: boolean) {
if (cm) {
C(0);
}
rC(0);
{
if (ctx.showing) {
if (V(1)) {
E(0, 'button');
{
L('click', ctx.onClick.bind(ctx));
T(1, 'Click me');
}
e();
}
v();
}
}
rc();
}
let comp = new MyComp();
renderToHtml(Template, comp);
const button = containerEl.querySelector('button') !;
button.click();
expect(comp.counter).toEqual(1);
button.click();
expect(comp.counter).toEqual(2);
// the listener should be removed when the view is removed
comp.showing = false;
renderToHtml(Template, comp);
button.click();
expect(comp.counter).toEqual(2);
});
it('should destroy listeners in nested views', () => {
/**
* % if (showing) {
* Hello
* % if (button) {
* <button (click)="onClick()"> Click </button>
* % }
* % }
*/
function Template(ctx: any, cm: boolean) {
if (cm) {
C(0);
c();
}
rC(0);
{
if (ctx.showing) {
if (V(0)) {
T(0, 'Hello');
C(1);
c();
}
rC(1);
{
if (ctx.button) {
if (V(0)) {
E(0, 'button');
{
L('click', ctx.onClick.bind(ctx));
T(1, 'Click');
}
e();
}
v();
}
}
rc();
v();
}
}
rc();
}
const comp = {showing: true, counter: 0, button: true, onClick: function() { this.counter++; }};
renderToHtml(Template, comp);
const button = containerEl.querySelector('button') !;
button.click();
expect(comp.counter).toEqual(1);
// the child view listener should be removed when the parent view is removed
comp.showing = false;
renderToHtml(Template, comp);
button.click();
expect(comp.counter).toEqual(1);
});
it('should destroy listeners in component views', () => {
/**
* % if (showing) {
* Hello
* <comp></comp>
* <comp></comp>
* % }
*
* comp:
* <button (click)="onClick()"> Click </button>
*/
function Template(ctx: any, cm: boolean) {
if (cm) {
C(0);
c();
}
rC(0);
{
if (ctx.showing) {
if (V(0)) {
T(0, 'Hello');
E(1, MyComp.ngComponentDef);
{ D(0, MyComp.ngComponentDef.n(), MyComp.ngComponentDef); }
e();
E(2, MyComp.ngComponentDef);
{ D(1, MyComp.ngComponentDef.n(), MyComp.ngComponentDef); }
e();
}
MyComp.ngComponentDef.r(0, 1);
MyComp.ngComponentDef.r(1, 2);
v();
}
}
rc();
}
const ctx = {showing: true};
renderToHtml(Template, ctx);
const buttons = containerEl.querySelectorAll('button') !;
buttons[0].click();
expect(comps[0] !.counter).toEqual(1);
buttons[1].click();
expect(comps[1] !.counter).toEqual(1);
// the child view listener should be removed when the parent view is removed
ctx.showing = false;
renderToHtml(Template, ctx);
buttons[0].click();
buttons[1].click();
expect(comps[0] !.counter).toEqual(1);
expect(comps[1] !.counter).toEqual(1);
});
it('should support listeners with sibling nested containers', () => {
/**
* % if (condition) {
* Hello
* % if (sub1) {
* <button (click)="counter1++">there</button>
* % }
*
* % if (sub2) {
* <button (click)="counter2++">world</button>
* % }
* % }
*/
function Template(ctx: any, cm: boolean) {
if (cm) {
C(0);
c();
}
rC(0);
{
if (ctx.condition) {
if (V(0)) {
T(0, 'Hello');
C(1);
c();
C(2);
c();
}
rC(1);
{
if (ctx.sub1) {
if (V(0)) {
E(0, 'button');
{
L('click', () => ctx.counter1++);
T(1, 'Click');
}
e();
}
v();
}
}
rc();
rC(2);
{
if (ctx.sub2) {
if (V(0)) {
E(0, 'button');
{
L('click', () => ctx.counter2++);
T(1, 'Click');
}
e();
}
v();
}
}
rc();
v();
}
}
rc();
}
const ctx = {condition: true, counter1: 0, counter2: 0, sub1: true, sub2: true};
renderToHtml(Template, ctx);
const buttons = containerEl.querySelectorAll('button') !;
buttons[0].click();
expect(ctx.counter1).toEqual(1);
buttons[1].click();
expect(ctx.counter2).toEqual(1);
// the child view listeners should be removed when the parent view is removed
ctx.condition = false;
renderToHtml(Template, ctx);
buttons[0].click();
buttons[1].click();
expect(ctx.counter1).toEqual(1);
expect(ctx.counter2).toEqual(1);
});
});

View File

@ -0,0 +1,19 @@
/**
* @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
*/
if (typeof window == 'undefined') {
const createWindow = require('domino').createWindow;
const window = createWindow('', 'http://localhost');
(global as any).document = window.document;
// Trick to avoid Event patching from
// https://github.com/angular/angular/blob/7cf5e95ac9f0f2648beebf0d5bd9056b79946970/packages/platform-browser/src/dom/events/dom_events.ts#L112-L132
// It fails with Domino with TypeError: Cannot assign to read only property
// 'stopImmediatePropagation' of object '#<Event>'
(global as any).Event = null;
}

View File

@ -0,0 +1,177 @@
/**
* @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 {CSSSelector, CSSSelectorWithNegations, NodeBindings, SimpleCSSSelector} from '../../src/render3/interfaces';
import {isNodeMatchingSelector, isNodeMatchingSelectorWithNegations, isNodeMatchingSimpleSelector} from '../../src/render3/node_selector_matcher';
function testLStaticData(tagName: string, attrs: string[] | null): NodeBindings {
return {tagName, attrs, initialInputs: undefined, inputs: undefined, outputs: undefined};
}
describe('css selector matching', () => {
describe('isNodeMatchingSimpleSelector', () => {
function isMatching(
tagName: string, attrs: string[] | null, selector: SimpleCSSSelector): boolean {
return isNodeMatchingSimpleSelector(testLStaticData(tagName, attrs), selector);
}
describe('element matching', () => {
it('should match element name only if names are the same', () => {
expect(isMatching('span', null, ['span'])).toBeTruthy();
expect(isMatching('span', null, ['div'])).toBeFalsy();
});
/**
* We assume that compiler will lower-case tag names both in LNode
* and in a selector.
*/
it('should match element name case-sensitively', () => {
expect(isMatching('span', null, ['SPAN'])).toBeFalsy();
expect(isMatching('SPAN', null, ['span'])).toBeFalsy();
});
});
describe('attributes matching', () => {
// TODO: do we need to differentiate no value and empty value? that is: title vs. title="" ?
it('should match single attribute without value', () => {
expect(isMatching('span', ['title', ''], ['', 'title', ''])).toBeTruthy();
expect(isMatching('span', ['title', 'my title'], ['', 'title', ''])).toBeTruthy();
expect(isMatching('span', null, ['', 'title', ''])).toBeFalsy();
expect(isMatching('span', ['title', ''], ['', 'other', ''])).toBeFalsy();
});
it('should match selector with one attribute without value when element has several attributes',
() => {
expect(isMatching('span', ['id', 'my_id', 'title', 'test_title'], [
'', 'title', ''
])).toBeTruthy();
});
it('should match single attribute with value', () => {
expect(isMatching('span', ['title', 'My Title'], ['', 'title', 'My Title'])).toBeTruthy();
expect(isMatching('span', ['title', 'My Title'], ['', 'title', 'Other Title'])).toBeFalsy();
});
it('should match single attribute with value', () => {
expect(isMatching('span', ['title', 'My Title'], ['', 'title', 'My Title'])).toBeTruthy();
expect(isMatching('span', ['title', 'My Title'], ['', 'title', 'Other Title'])).toBeFalsy();
});
it('should not match attribute when element name does not match', () => {
expect(isMatching('span', ['title', 'My Title'], ['div', 'title', ''])).toBeFalsy();
expect(isMatching('span', ['title', 'My Title'], ['div', 'title', 'My title'])).toBeFalsy();
});
/**
* We assume that compiler will lower-case all attribute names when generating code
*/
it('should match attribute name case-sensitively', () => {
expect(isMatching('span', ['foo', ''], ['', 'foo', ''])).toBeTruthy();
expect(isMatching('span', ['foo', ''], ['', 'Foo', ''])).toBeFalsy();
});
it('should match attribute values case-sensitively', () => {
expect(isMatching('span', ['foo', 'Bar'], ['', 'foo', 'Bar'])).toBeTruthy();
expect(isMatching('span', ['foo', 'Bar'], ['', 'Foo', 'bar'])).toBeFalsy();
});
it('should match class as an attribute', () => {
expect(isMatching('span', ['class', 'foo'], ['', 'class', ''])).toBeTruthy();
expect(isMatching('span', ['class', 'foo'], ['', 'class', 'foo'])).toBeTruthy();
});
});
describe('class matching', () => {
it('should match with a class selector when an element has multiple classes', () => {
expect(isMatching('span', ['class', 'foo bar'], ['', 'class', 'foo'])).toBeTruthy();
expect(isMatching('span', ['class', 'foo bar'], ['', 'class', 'bar'])).toBeTruthy();
expect(isMatching('span', ['class', 'foo bar'], ['', 'class', 'baz'])).toBeFalsy();
});
it('should not match on partial class name', () => {
expect(isMatching('span', ['class', 'foobar'], ['', 'class', 'foo'])).toBeFalsy();
expect(isMatching('span', ['class', 'foobar'], ['', 'class', 'bar'])).toBeFalsy();
expect(isMatching('span', ['class', 'foobar'], ['', 'class', 'ob'])).toBeFalsy();
expect(isMatching('span', ['class', 'foobar'], ['', 'class', 'foobar'])).toBeTruthy();
});
it('should support selectors with multiple classes', () => {
expect(isMatching('span', ['class', 'foo bar'], ['', 'class', 'foo', 'bar'])).toBeTruthy();
expect(isMatching('span', ['class', 'foo'], ['', 'class', 'foo', 'bar'])).toBeFalsy();
expect(isMatching('span', ['class', 'bar'], ['', 'class', 'foo', 'bar'])).toBeFalsy();
});
it('should support selectors with multiple classes regardless of class name order', () => {
expect(isMatching('span', ['class', 'foo bar'], ['', 'class', 'foo', 'bar'])).toBeTruthy();
expect(isMatching('span', ['class', 'foo bar'], ['', 'class', 'bar', 'foo'])).toBeTruthy();
expect(isMatching('span', ['class', 'bar foo'], ['', 'class', 'foo', 'bar'])).toBeTruthy();
expect(isMatching('span', ['class', 'bar foo'], ['', 'class', 'bar', 'foo'])).toBeTruthy();
});
it('should match class name case-sensitively', () => {
expect(isMatching('span', ['class', 'Foo'], ['', 'class', 'Foo'])).toBeTruthy();
expect(isMatching('span', ['class', 'Foo'], ['', 'class', 'foo'])).toBeFalsy();
});
});
});
describe('isNodeMatchingSelectorWithNegations', () => {
function isMatching(
tagName: string, attrs: string[] | null, selector: CSSSelectorWithNegations): boolean {
return isNodeMatchingSelectorWithNegations(testLStaticData(tagName, attrs), selector);
}
it('should match when negation part is null', () => {
expect(isMatching('span', null, [['span'], null])).toBeTruthy();
});
it('should not match when negation part does not match', () => {
// <span foo=""> not matching ":not(span)"
expect(isMatching('span', ['foo', ''], [null, [['span']]])).toBeFalsy();
// <span foo=""> not matching ":not([foo])"
expect(isMatching('span', ['foo', ''], [['span'], [['', 'foo', '']]])).toBeFalsy();
});
});
describe('isNodeMatchingSelector', () => {
function isMatching(tagName: string, attrs: string[] | null, selector: CSSSelector): boolean {
return isNodeMatchingSelector(testLStaticData(tagName, attrs), selector);
}
it('should match when there is only one simple selector without negations', () => {
expect(isMatching('span', null, [[['span'], null]])).toBeTruthy();
expect(isMatching('span', null, [[['div'], null]])).toBeFalsy();
});
it('should atch when there are multiple parts and only one is matching', () => {
// <span foo="bar"> matching "div, [foo=bar]"
expect(isMatching('span', ['foo', 'bar'], [
[['div'], null], [['', 'foo', 'bar'], null]
])).toBeTruthy();
});
it('should not match when there are multiple parts and none is matching', () => {
// <span foo="bar"> not matching "div, [foo=baz]"
expect(isMatching('span', ['foo', 'bar'], [
[['div'], null], [['', 'foo', 'baz'], null]
])).toBeFalsy();
});
});
});

View File

@ -0,0 +1,400 @@
/**
* @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 {EventEmitter} from '@angular/core';
import {C, D, E, L, LifeCycleGuard, T, V, b, c, defineComponent, defineDirective, e, l, p, rC, rc, v} from '../../src/render3/index';
import {containerEl, renderToHtml} from './render_util';
describe('outputs', () => {
let buttonToggle: ButtonToggle;
class ButtonToggle {
change = new EventEmitter();
resetStream = new EventEmitter();
static ngComponentDef = defineComponent({
tag: 'button-toggle',
type: ButtonToggle,
template: function(ctx: any, cm: boolean) {},
factory: () => buttonToggle = new ButtonToggle(),
outputs: {change: 'change', resetStream: 'reset'}
});
}
it('should call component output function when event is emitted', () => {
/** <button-toggle (change)="onChange()"></button-toggle> */
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, ButtonToggle.ngComponentDef);
{
D(0, ButtonToggle.ngComponentDef.n(), ButtonToggle.ngComponentDef);
L('change', ctx.onChange.bind(ctx));
}
e();
}
ButtonToggle.ngComponentDef.r(0, 0);
}
let counter = 0;
const ctx = {onChange: () => counter++};
renderToHtml(Template, ctx);
buttonToggle !.change.next();
expect(counter).toEqual(1);
buttonToggle !.change.next();
expect(counter).toEqual(2);
});
it('should support more than 1 output function on the same node', () => {
/** <button-toggle (change)="onChange()" (reset)="onReset()"></button-toggle> */
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, ButtonToggle.ngComponentDef);
{
D(0, ButtonToggle.ngComponentDef.n(), ButtonToggle.ngComponentDef);
L('change', ctx.onChange.bind(ctx));
L('reset', ctx.onReset.bind(ctx));
}
e();
}
ButtonToggle.ngComponentDef.r(0, 0);
}
let counter = 0;
let resetCounter = 0;
const ctx = {onChange: () => counter++, onReset: () => resetCounter++};
renderToHtml(Template, ctx);
buttonToggle !.change.next();
expect(counter).toEqual(1);
buttonToggle !.resetStream.next();
expect(resetCounter).toEqual(1);
});
it('should eval component output expression when event is emitted', () => {
/** <button-toggle (change)="counter++"></button-toggle> */
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, ButtonToggle.ngComponentDef);
{
D(0, ButtonToggle.ngComponentDef.n(), ButtonToggle.ngComponentDef);
L('change', () => ctx.counter++);
}
e();
}
ButtonToggle.ngComponentDef.r(0, 0);
}
const ctx = {counter: 0};
renderToHtml(Template, ctx);
buttonToggle !.change.next();
expect(ctx.counter).toEqual(1);
buttonToggle !.change.next();
expect(ctx.counter).toEqual(2);
});
it('should unsubscribe from output when view is destroyed', () => {
/**
* % if (condition) {
* <button-toggle (change)="onChange()"></button-toggle>
* % }
*/
function Template(ctx: any, cm: boolean) {
if (cm) {
C(0);
c();
}
rC(0);
{
if (ctx.condition) {
if (V(0)) {
E(0, ButtonToggle.ngComponentDef);
{
D(0, ButtonToggle.ngComponentDef.n(), ButtonToggle.ngComponentDef);
L('change', ctx.onChange.bind(ctx));
}
e();
}
ButtonToggle.ngComponentDef.r(0, 0);
v();
}
}
rc();
}
let counter = 0;
const ctx = {onChange: () => counter++, condition: true};
renderToHtml(Template, ctx);
buttonToggle !.change.next();
expect(counter).toEqual(1);
ctx.condition = false;
renderToHtml(Template, ctx);
buttonToggle !.change.next();
expect(counter).toEqual(1);
});
it('should unsubscribe from output in nested view', () => {
/**
* % if (condition) {
* % if (condition2) {
* <button-toggle (change)="onChange()"></button-toggle>
* % }
* % }
*/
function Template(ctx: any, cm: boolean) {
if (cm) {
C(0);
c();
}
rC(0);
{
if (ctx.condition) {
if (V(0)) {
C(0);
c();
}
rC(0);
{
if (ctx.condition2) {
if (V(0)) {
E(0, ButtonToggle.ngComponentDef);
{
D(0, ButtonToggle.ngComponentDef.n(), ButtonToggle.ngComponentDef);
L('change', ctx.onChange.bind(ctx));
}
e();
}
ButtonToggle.ngComponentDef.r(0, 0);
v();
}
}
rc();
v();
}
}
rc();
}
let counter = 0;
const ctx = {onChange: () => counter++, condition: true, condition2: true};
renderToHtml(Template, ctx);
buttonToggle !.change.next();
expect(counter).toEqual(1);
ctx.condition = false;
renderToHtml(Template, ctx);
buttonToggle !.change.next();
expect(counter).toEqual(1);
});
it('should work properly when view also has listeners and destroys', () => {
let destroyComp: DestroyComp;
class DestroyComp {
events: string[] = [];
ngOnDestroy() { this.events.push('destroy'); }
static ngComponentDef = defineComponent({
tag: 'destroy-comp',
type: DestroyComp,
template: function(ctx: any, cm: boolean) {},
factory: () => {
destroyComp = new DestroyComp();
l(LifeCycleGuard.ON_DESTROY, destroyComp, destroyComp.ngOnDestroy);
return destroyComp;
}
});
}
/**
* % if (condition) {
* <button (click)="onClick()">Click me</button>
* <button-toggle (change)="onChange()"></button-toggle>
* <destroy-comp></destroy-comp>
* % }
*/
function Template(ctx: any, cm: boolean) {
if (cm) {
C(0);
c();
}
rC(0);
{
if (ctx.condition) {
if (V(0)) {
E(0, 'button');
{
L('click', ctx.onClick.bind(ctx));
T(1, 'Click me');
}
e();
E(2, ButtonToggle.ngComponentDef);
{
D(0, ButtonToggle.ngComponentDef.n(), ButtonToggle.ngComponentDef);
L('change', ctx.onChange.bind(ctx));
}
e();
E(3, DestroyComp.ngComponentDef);
{ D(1, DestroyComp.ngComponentDef.n(), DestroyComp.ngComponentDef); }
e();
}
ButtonToggle.ngComponentDef.r(0, 2);
DestroyComp.ngComponentDef.r(1, 3);
v();
}
}
rc();
}
let clickCounter = 0;
let changeCounter = 0;
const ctx = {condition: true, onChange: () => changeCounter++, onClick: () => clickCounter++};
renderToHtml(Template, ctx);
buttonToggle !.change.next();
expect(changeCounter).toEqual(1);
expect(clickCounter).toEqual(0);
const button = containerEl.querySelector('button');
button !.click();
expect(changeCounter).toEqual(1);
expect(clickCounter).toEqual(1);
ctx.condition = false;
renderToHtml(Template, ctx);
expect(destroyComp !.events).toEqual(['destroy']);
buttonToggle !.change.next();
button !.click();
expect(changeCounter).toEqual(1);
expect(clickCounter).toEqual(1);
});
it('should fire event listeners along with outputs if they match', () => {
let buttonDir: MyButton;
/** <button myButton (click)="onClick()">Click me</button> */
class MyButton {
click = new EventEmitter();
static ngDirectiveDef = defineDirective(
{type: MyButton, factory: () => buttonDir = new MyButton, outputs: {click: 'click'}});
}
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'button');
{
D(0, MyButton.ngDirectiveDef.n(), MyButton.ngDirectiveDef);
L('click', ctx.onClick.bind(ctx));
}
e();
}
}
let counter = 0;
renderToHtml(Template, {counter, onClick: () => counter++});
// To match current Angular behavior, the click listener is still
// set up in addition to any matching outputs.
const button = containerEl.querySelector('button') !;
button.click();
expect(counter).toEqual(1);
buttonDir !.click.next();
expect(counter).toEqual(2);
});
it('should work with two outputs of the same name', () => {
let otherDir: OtherDir;
class OtherDir {
change = new EventEmitter();
static ngDirectiveDef = defineDirective(
{type: OtherDir, factory: () => otherDir = new OtherDir, outputs: {change: 'change'}});
}
/** <button-toggle (change)="onChange()" otherDir></button-toggle> */
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, ButtonToggle.ngComponentDef);
{
D(0, ButtonToggle.ngComponentDef.n(), ButtonToggle.ngComponentDef);
D(1, OtherDir.ngDirectiveDef.n(), OtherDir.ngDirectiveDef);
L('change', ctx.onChange.bind(ctx));
}
e();
}
ButtonToggle.ngComponentDef.r(0, 0);
}
let counter = 0;
renderToHtml(Template, {counter, onChange: () => counter++});
buttonToggle !.change.next();
expect(counter).toEqual(1);
otherDir !.change.next();
expect(counter).toEqual(2);
});
it('should work with an input and output of the same name', () => {
let otherDir: OtherDir;
class OtherDir {
change: boolean;
static ngDirectiveDef = defineDirective(
{type: OtherDir, factory: () => otherDir = new OtherDir, inputs: {change: 'change'}});
}
/** <button-toggle (change)="onChange()" otherDir [change]="change"></button-toggle> */
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, ButtonToggle.ngComponentDef);
{
D(0, ButtonToggle.ngComponentDef.n(), ButtonToggle.ngComponentDef);
D(1, OtherDir.ngDirectiveDef.n(), OtherDir.ngDirectiveDef);
L('change', ctx.onChange.bind(ctx));
}
e();
}
p(0, 'change', b(ctx.change));
ButtonToggle.ngComponentDef.r(0, 0);
}
let counter = 0;
renderToHtml(Template, {counter, onChange: () => counter++, change: true});
expect(otherDir !.change).toEqual(true);
renderToHtml(Template, {counter, onChange: () => counter++, change: false});
expect(otherDir !.change).toEqual(false);
buttonToggle !.change.next();
expect(counter).toEqual(1);
});
});

View File

@ -0,0 +1,436 @@
/**
* @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 {EventEmitter} from '@angular/core';
import {C, D, E, L, T, V, b, b1, c, defineComponent, defineDirective, e, p, rC, rc, t, v} from '../../src/render3/index';
import {NO_CHANGE} from '../../src/render3/instructions';
import {renderToHtml} from './render_util';
describe('elementProperty', () => {
it('should support bindings to properties', () => {
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'span');
e();
}
p(0, 'id', b(ctx));
}
expect(renderToHtml(Template, 'testId')).toEqual('<span id="testId"></span>');
expect(renderToHtml(Template, 'otherId')).toEqual('<span id="otherId"></span>');
});
it('should support creation time bindings to properties', () => {
function expensive(ctx: string): any {
if (ctx === 'cheapId') {
return ctx;
} else {
throw 'Too expensive!';
}
}
function Template(ctx: string, cm: boolean) {
if (cm) {
E(0, 'span');
e();
}
p(0, 'id', cm ? expensive(ctx) : NO_CHANGE);
}
expect(renderToHtml(Template, 'cheapId')).toEqual('<span id="cheapId"></span>');
expect(renderToHtml(Template, 'expensiveId')).toEqual('<span id="cheapId"></span>');
});
it('should support interpolation for properties', () => {
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'span');
e();
}
p(0, 'id', b1('_', ctx, '_'));
}
expect(renderToHtml(Template, 'testId')).toEqual('<span id="_testId_"></span>');
expect(renderToHtml(Template, 'otherId')).toEqual('<span id="_otherId_"></span>');
});
describe('input properties', () => {
let button: MyButton;
let otherDir: OtherDir;
class MyButton {
disabled: boolean;
static ngDirectiveDef = defineDirective(
{type: MyButton, factory: () => button = new MyButton(), inputs: {disabled: 'disabled'}});
}
class OtherDir {
id: boolean;
clickStream = new EventEmitter();
static ngDirectiveDef = defineDirective({
type: OtherDir,
factory: () => otherDir = new OtherDir(),
inputs: {id: 'id'},
outputs: {clickStream: 'click'}
});
}
it('should check input properties before setting (directives)', () => {
/** <button myButton [id]="id" [disabled]="isDisabled">Click me</button> */
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'button');
{
D(0, MyButton.ngDirectiveDef.n(), MyButton.ngDirectiveDef);
D(1, OtherDir.ngDirectiveDef.n(), OtherDir.ngDirectiveDef);
T(1, 'Click me');
}
e();
}
p(0, 'disabled', b(ctx.isDisabled));
p(0, 'id', b(ctx.id));
}
const ctx: any = {isDisabled: true, id: 0};
expect(renderToHtml(Template, ctx)).toEqual(`<button>Click me</button>`);
expect(button !.disabled).toEqual(true);
expect(otherDir !.id).toEqual(0);
ctx.isDisabled = false;
ctx.id = 1;
expect(renderToHtml(Template, ctx)).toEqual(`<button>Click me</button>`);
expect(button !.disabled).toEqual(false);
expect(otherDir !.id).toEqual(1);
});
it('should support mixed element properties and input properties', () => {
/** <button myButton [id]="id" [disabled]="isDisabled">Click me</button> */
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'button');
{
D(0, MyButton.ngDirectiveDef.n(), MyButton.ngDirectiveDef);
T(1, 'Click me');
}
e();
}
p(0, 'disabled', b(ctx.isDisabled));
p(0, 'id', b(ctx.id));
}
const ctx: any = {isDisabled: true, id: 0};
expect(renderToHtml(Template, ctx)).toEqual(`<button id="0">Click me</button>`);
expect(button !.disabled).toEqual(true);
ctx.isDisabled = false;
ctx.id = 1;
expect(renderToHtml(Template, ctx)).toEqual(`<button id="1">Click me</button>`);
expect(button !.disabled).toEqual(false);
});
it('should check that property is not an input property before setting (component)', () => {
let comp: Comp;
class Comp {
id: number;
static ngComponentDef = defineComponent({
tag: 'comp',
type: Comp,
template: function(ctx: any, cm: boolean) {},
factory: () => comp = new Comp(),
inputs: {id: 'id'}
});
}
/** <comp [id]="id"></comp> */
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, Comp.ngComponentDef);
{ D(0, Comp.ngComponentDef.n(), Comp.ngComponentDef); }
e();
}
p(0, 'id', b(ctx.id));
Comp.ngComponentDef.r(0, 0);
}
expect(renderToHtml(Template, {id: 1})).toEqual(`<comp></comp>`);
expect(comp !.id).toEqual(1);
expect(renderToHtml(Template, {id: 2})).toEqual(`<comp></comp>`);
expect(comp !.id).toEqual(2);
});
it('should support two input properties with the same name', () => {
let otherDisabledDir: OtherDisabledDir;
class OtherDisabledDir {
disabled: boolean;
static ngDirectiveDef = defineDirective({
type: OtherDisabledDir,
factory: () => otherDisabledDir = new OtherDisabledDir(),
inputs: {disabled: 'disabled'}
});
}
/** <button myButton otherDisabledDir [disabled]="isDisabled">Click me</button> */
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'button');
{
D(0, MyButton.ngDirectiveDef.n(), MyButton.ngDirectiveDef);
D(1, OtherDisabledDir.ngDirectiveDef.n(), OtherDisabledDir.ngDirectiveDef);
T(1, 'Click me');
}
e();
}
p(0, 'disabled', b(ctx.isDisabled));
}
const ctx: any = {isDisabled: true};
expect(renderToHtml(Template, ctx)).toEqual(`<button>Click me</button>`);
expect(button !.disabled).toEqual(true);
expect(otherDisabledDir !.disabled).toEqual(true);
ctx.isDisabled = false;
expect(renderToHtml(Template, ctx)).toEqual(`<button>Click me</button>`);
expect(button !.disabled).toEqual(false);
expect(otherDisabledDir !.disabled).toEqual(false);
});
it('should set input property if there is an output first', () => {
/** <button otherDir [id]="id" (click)="onClick()">Click me</button> */
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'button');
{
D(0, OtherDir.ngDirectiveDef.n(), OtherDir.ngDirectiveDef);
L('click', ctx.onClick.bind(ctx));
T(1, 'Click me');
}
e();
}
p(0, 'id', b(ctx.id));
}
let counter = 0;
const ctx: any = {id: 1, onClick: () => counter++};
expect(renderToHtml(Template, ctx)).toEqual(`<button>Click me</button>`);
expect(otherDir !.id).toEqual(1);
otherDir !.clickStream.next();
expect(counter).toEqual(1);
ctx.id = 2;
renderToHtml(Template, ctx);
expect(otherDir !.id).toEqual(2);
});
});
describe('attributes and input properties', () => {
let myDir: MyDir;
class MyDir {
role: string;
direction: string;
changeStream = new EventEmitter();
static ngDirectiveDef = defineDirective({
type: MyDir,
factory: () => myDir = new MyDir(),
inputs: {role: 'role', direction: 'dir'},
outputs: {changeStream: 'change'}
});
}
let dirB: MyDirB;
class MyDirB {
roleB: string;
static ngDirectiveDef = defineDirective(
{type: MyDirB, factory: () => dirB = new MyDirB(), inputs: {roleB: 'role'}});
}
it('should set input property based on attribute if existing', () => {
/** <div role="button" myDir></div> */
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'div', ['role', 'button']);
{ D(0, MyDir.ngDirectiveDef.n(), MyDir.ngDirectiveDef); }
e();
}
}
expect(renderToHtml(Template, {})).toEqual(`<div role="button"></div>`);
expect(myDir !.role).toEqual('button');
});
it('should set input property and attribute if both defined', () => {
/** <div role="button" [role]="role" myDir></div> */
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'div', ['role', 'button']);
{ D(0, MyDir.ngDirectiveDef.n(), MyDir.ngDirectiveDef); }
e();
}
p(0, 'role', b(ctx.role));
}
expect(renderToHtml(Template, {role: 'listbox'})).toEqual(`<div role="button"></div>`);
expect(myDir !.role).toEqual('listbox');
renderToHtml(Template, {role: 'button'});
expect(myDir !.role).toEqual('button');
});
it('should set two directive input properties based on same attribute', () => {
/** <div role="button" myDir myDirB></div> */
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'div', ['role', 'button']);
{
D(0, MyDir.ngDirectiveDef.n(), MyDir.ngDirectiveDef);
D(1, MyDirB.ngDirectiveDef.n(), MyDirB.ngDirectiveDef);
}
e();
}
}
expect(renderToHtml(Template, {})).toEqual(`<div role="button"></div>`);
expect(myDir !.role).toEqual('button');
expect(dirB !.roleB).toEqual('button');
});
it('should process two attributes on same directive', () => {
/** <div role="button" dir="rtl" myDir></div> */
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'div', ['role', 'button', 'dir', 'rtl']);
{ D(0, MyDir.ngDirectiveDef.n(), MyDir.ngDirectiveDef); }
e();
}
}
expect(renderToHtml(Template, {})).toEqual(`<div role="button" dir="rtl"></div>`);
expect(myDir !.role).toEqual('button');
expect(myDir !.direction).toEqual('rtl');
});
it('should process attributes and outputs properly together', () => {
/** <div role="button" (change)="onChange()" myDir></div> */
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'div', ['role', 'button']);
{
D(0, MyDir.ngDirectiveDef.n(), MyDir.ngDirectiveDef);
L('change', ctx.onChange.bind(ctx));
}
e();
}
}
let counter = 0;
expect(renderToHtml(Template, {
onChange: () => counter++
})).toEqual(`<div role="button"></div>`);
expect(myDir !.role).toEqual('button');
myDir !.changeStream.next();
expect(counter).toEqual(1);
});
it('should process attributes properly for directives with later indices', () => {
/**
* <div role="button" dir="rtl" myDir></div>
* <div role="listbox" myDirB></div>
*/
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'div', ['role', 'button', 'dir', 'rtl']);
{ D(0, MyDir.ngDirectiveDef.n(), MyDir.ngDirectiveDef); }
e();
E(1, 'div', ['role', 'listbox']);
{ D(1, MyDirB.ngDirectiveDef.n(), MyDirB.ngDirectiveDef); }
e();
}
}
expect(renderToHtml(Template, {}))
.toEqual(`<div role="button" dir="rtl"></div><div role="listbox"></div>`);
expect(myDir !.role).toEqual('button');
expect(myDir !.direction).toEqual('rtl');
expect(dirB !.roleB).toEqual('listbox');
});
it('should process attributes properly inside a for loop', () => {
class Comp {
static ngComponentDef = defineComponent({
tag: 'comp',
type: Comp,
template: function(ctx: any, cm: boolean) {
if (cm) {
E(0, 'div', ['role', 'button']);
{ D(0, MyDir.ngDirectiveDef.n(), MyDir.ngDirectiveDef); }
e();
T(1);
}
t(1, b(D<MyDir>(0).role));
},
factory: () => new Comp()
});
}
/**
* % for (let i = 0; i < 3; i++) {
* <comp></comp>
* % }
*/
function Template(ctx: any, cm: boolean) {
if (cm) {
C(0);
c();
}
rC(0);
{
for (let i = 0; i < 2; i++) {
if (V(0)) {
E(0, Comp.ngComponentDef);
{ D(0, Comp.ngComponentDef.n(), Comp.ngComponentDef); }
e();
}
Comp.ngComponentDef.r(0, 0);
v();
}
}
rc();
}
expect(renderToHtml(Template, {}))
.toEqual(
`<comp><div role="button"></div>button</comp><comp><div role="button"></div>button</comp>`);
});
});
});

View File

@ -0,0 +1,51 @@
/**
* @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 {D, E, Q, QueryList, e, m, rQ} from '../../src/render3/index';
import {createComponent, renderComponent} from './render_util';
describe('query', () => {
it('should project query children', () => {
const Child = createComponent('child', function(ctx: any, cm: boolean) {});
let child1 = null;
let child2 = null;
const Cmp = createComponent('cmp', function(ctx: any, cm: boolean) {
/**
* <child>
* <child>
* </child>
* </child>
* class Cmp {
* @ViewChildren(Child) query0;
* @ViewChildren(Child, {descend: true}) query1;
* }
*/
let tmp: any;
if (cm) {
m(0, Q(Child, false));
m(1, Q(Child, true));
E(0, Child.ngComponentDef);
{
child1 = D(0, Child.ngComponentDef.n(), Child.ngComponentDef);
E(1, Child.ngComponentDef);
{ child2 = D(1, Child.ngComponentDef.n(), Child.ngComponentDef); }
e();
}
e();
}
rQ(tmp = m<QueryList<any>>(0)) && (ctx.query0 = tmp as QueryList<any>);
rQ(tmp = m<QueryList<any>>(1)) && (ctx.query1 = tmp as QueryList<any>);
});
const parent = renderComponent(Cmp);
expect((parent.query0 as QueryList<any>).toArray()).toEqual([child1]);
expect((parent.query1 as QueryList<any>).toArray()).toEqual([child1, child2]);
});
});

View File

@ -0,0 +1,83 @@
/**
* @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 {ComponentTemplate, ComponentType, PublicFeature, defineComponent, renderComponent as _renderComponent} from '../../src/render3/index';
import {NG_HOST_SYMBOL, createNode, createViewState, renderTemplate} from '../../src/render3/instructions';
import {LElement, LNodeFlags} from '../../src/render3/interfaces';
import {RElement, RText, Renderer3} from '../../src/render3/renderer';
import {getRenderer2} from './imported_renderer2';
export const document = ((global || window) as any).document;
export let containerEl: HTMLElement = null !;
let host: LElement;
let activeRenderer: Renderer3 =
(typeof process !== 'undefined' && process.argv[3] && process.argv[3] === '--r=renderer2') ?
getRenderer2(document) :
document;
// tslint:disable-next-line:no-console
console.log(
`Running tests with ${activeRenderer === document ? 'document' : 'Renderer2'} renderer...`);
export const requestAnimationFrame:
{(fn: () => void): void; flush(): void; queue: (() => void)[];} = function(fn: () => void) {
requestAnimationFrame.queue.push(fn);
} as any;
requestAnimationFrame.flush = function() {
while (requestAnimationFrame.queue.length) {
requestAnimationFrame.queue.shift() !();
}
};
export function resetDOM() {
requestAnimationFrame.queue = [];
containerEl = document.createElement('div');
containerEl.setAttribute('host', '');
host = createNode(null, LNodeFlags.Element, containerEl, createViewState(-1, activeRenderer));
// TODO: assert that the global state is clean (e.g. ngData, previousOrParentNode, etc)
}
export function renderToHtml(template: ComponentTemplate<any>, ctx: any) {
renderTemplate(host, template, ctx);
return toHtml(host.native);
}
beforeEach(resetDOM);
export function renderComponent<T>(type: ComponentType<T>): T {
return _renderComponent(type, {renderer: activeRenderer, host: containerEl});
}
export function toHtml<T>(componentOrElement: T | RElement): string {
const node = (componentOrElement as any)[NG_HOST_SYMBOL] as LElement;
if (node) {
return toHtml(node.native);
} else {
return containerEl.innerHTML.replace(' style=""', '').replace(/<!--[\w]*-->/g, '');
}
}
export function createComponent(
name: string, template: ComponentTemplate<any>): ComponentType<any> {
return class Component {
value: any;
static ngComponentDef = defineComponent({
type: Component,
tag: name,
factory: () => new Component,
template: template,
features: [PublicFeature]
});
};
}
// Verify that DOM is a type of render. This is here for error checking only and has no use.
export const renderer: Renderer3 = null as any as Document;
export const element: RElement = null as any as HTMLElement;
export const text: RText = null as any as Text;

View File

@ -0,0 +1,35 @@
/**
* @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 {isDifferent} from '../../src/render3/util';
describe('util', () => {
describe('isDifferent', () => {
it('should mark non-equal arguments as different', () => {
expect(isDifferent({}, {})).toBeTruthy();
expect(isDifferent('foo', 'bar')).toBeTruthy();
expect(isDifferent(0, 1)).toBeTruthy();
});
it('should not mark equal arguments as different', () => {
const obj = {};
expect(isDifferent(obj, obj)).toBeFalsy();
expect(isDifferent('foo', 'foo')).toBeFalsy();
expect(isDifferent(1, 1)).toBeFalsy();
});
it('should not mark NaN as different', () => { expect(isDifferent(NaN, NaN)).toBeFalsy(); });
it('should mark NaN with other values as different', () => {
expect(isDifferent(NaN, 'foo')).toBeTruthy();
expect(isDifferent(5, NaN)).toBeTruthy();
});
});
});