feat(core): add initial view engine (#14014)

The new view engine allows our codegen to produce less code,
as it can interpret view definitions during runtime.

The view engine is not feature complete yet, but already
allows to implement a tree benchmark based on it.

Part of #14013
This commit is contained in:
Tobias Bosch
2017-01-20 13:10:57 -08:00
committed by Alex Rickabaugh
parent 9d8c467cb0
commit 2f87eb52fe
26 changed files with 3133 additions and 11 deletions

View File

@ -0,0 +1,70 @@
/**
* @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 {RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation} from '@angular/core';
import {DefaultServices, NodeDef, NodeFlags, NodeUpdater, Services, ViewData, ViewDefinition, ViewFlags, ViewUpdateFn, anchorDef, checkAndUpdateView, checkNoChangesView, createRootView, elementDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index';
import {inject} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {isBrowser, setupAndCheckRenderer} from './helper';
export function main() {
if (isBrowser()) {
defineTests({directDom: true, viewFlags: ViewFlags.DirectDom});
}
defineTests({directDom: false, viewFlags: 0});
}
function defineTests(config: {directDom: boolean, viewFlags: number}) {
describe(`View Anchor, directDom: ${config.directDom}`, () => {
setupAndCheckRenderer(config);
let services: Services;
let renderComponentType: RenderComponentType;
beforeEach(
inject([RootRenderer, Sanitizer], (rootRenderer: RootRenderer, sanitizer: Sanitizer) => {
services = new DefaultServices(rootRenderer, sanitizer);
renderComponentType =
new RenderComponentType('1', 'someUrl', 0, ViewEncapsulation.None, [], {});
}));
function compViewDef(nodes: NodeDef[], updater?: ViewUpdateFn): ViewDefinition {
return viewDef(config.viewFlags, nodes, updater, renderComponentType);
}
function createAndGetRootNodes(viewDef: ViewDefinition): {rootNodes: any[], view: ViewData} {
const view = createRootView(services, viewDef);
const rootNodes = rootRenderNodes(view);
return {rootNodes, view};
}
describe('create', () => {
it('should create anchor nodes without parents', () => {
const rootNodes =
createAndGetRootNodes(compViewDef([anchorDef(NodeFlags.None, 0)])).rootNodes;
expect(rootNodes.length).toBe(1);
});
it('should create views with multiple root anchor nodes', () => {
const rootNodes = createAndGetRootNodes(compViewDef([
anchorDef(NodeFlags.None, 0), anchorDef(NodeFlags.None, 0)
])).rootNodes;
expect(rootNodes.length).toBe(2);
});
it('should create anchor nodes with parents', () => {
const rootNodes = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, 1, 'div'),
anchorDef(NodeFlags.None, 0),
])).rootNodes;
expect(getDOM().childNodes(rootNodes[0]).length).toBe(1);
});
});
});
}

View File

@ -0,0 +1,128 @@
/**
* @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 {RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation} from '@angular/core';
import {BindingType, DefaultServices, NodeDef, NodeFlags, NodeUpdater, Services, ViewData, ViewDefinition, ViewFlags, ViewUpdateFn, anchorDef, checkAndUpdateView, checkNoChangesView, createRootView, destroyView, elementDef, providerDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index';
import {inject} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {isBrowser, setupAndCheckRenderer} from './helper';
export function main() {
if (isBrowser()) {
defineTests({directDom: true, viewFlags: ViewFlags.DirectDom});
}
defineTests({directDom: false, viewFlags: 0});
}
function defineTests(config: {directDom: boolean, viewFlags: number}) {
describe(`Component Views, directDom: ${config.directDom}`, () => {
setupAndCheckRenderer(config);
let services: Services;
let renderComponentType: RenderComponentType;
beforeEach(
inject([RootRenderer, Sanitizer], (rootRenderer: RootRenderer, sanitizer: Sanitizer) => {
services = new DefaultServices(rootRenderer, sanitizer);
renderComponentType =
new RenderComponentType('1', 'someUrl', 0, ViewEncapsulation.None, [], {});
}));
function compViewDef(nodes: NodeDef[], updater?: ViewUpdateFn): ViewDefinition {
return viewDef(config.viewFlags, nodes, updater, renderComponentType);
}
function createAndGetRootNodes(viewDef: ViewDefinition): {rootNodes: any[], view: ViewData} {
const view = createRootView(services, viewDef);
const rootNodes = rootRenderNodes(view);
return {rootNodes, view};
}
it('should create and attach component views', () => {
class AComp {}
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, 1, 'div'),
providerDef(NodeFlags.None, AComp, [], null, () => compViewDef([
elementDef(NodeFlags.None, 0, 'span'),
])),
]));
const compRootEl = getDOM().childNodes(rootNodes[0])[0];
expect(getDOM().nodeName(compRootEl).toLowerCase()).toBe('span');
});
it('should dirty check component views', () => {
let value = 'v1';
let instance: AComp;
class AComp {
a: any;
constructor() { instance = this; }
}
const updater = jasmine.createSpy('updater').and.callFake(
(updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, value));
const {view, rootNodes} = createAndGetRootNodes(
compViewDef([
elementDef(NodeFlags.None, 1, 'div'),
providerDef(NodeFlags.None, AComp, [], null, () => compViewDef(
[
elementDef(NodeFlags.None, 0, 'span', null, [[BindingType.ElementAttribute, 'a', SecurityContext.NONE]]),
], updater
)),
], jasmine.createSpy('parentUpdater')));
checkAndUpdateView(view);
expect(updater).toHaveBeenCalled();
// component
expect(updater.calls.mostRecent().args[2]).toBe(instance);
// view context
expect(updater.calls.mostRecent().args[3]).toBe(instance);
updater.calls.reset();
checkNoChangesView(view);
expect(updater).toHaveBeenCalled();
// component
expect(updater.calls.mostRecent().args[2]).toBe(instance);
// view context
expect(updater.calls.mostRecent().args[3]).toBe(instance);
value = 'v2';
expect(() => checkNoChangesView(view))
.toThrowError(
`Expression has changed after it was checked. Previous value: 'v1'. Current value: 'v2'.`);
});
it('should destroy component views', () => {
const log: string[] = [];
class AComp {}
class ChildProvider {
ngOnDestroy() { log.push('ngOnDestroy'); };
}
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, 1, 'div'),
providerDef(
NodeFlags.None, AComp, [], null, () => compViewDef([
elementDef(NodeFlags.None, 1, 'span'),
providerDef(NodeFlags.OnDestroy, ChildProvider, [])
])),
]));
destroyView(view);
expect(log).toEqual(['ngOnDestroy']);
});
});
}

View File

@ -0,0 +1,223 @@
/**
* @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 {RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation} from '@angular/core';
import {BindingType, DefaultServices, NodeDef, NodeFlags, NodeUpdater, Services, ViewData, ViewDefinition, ViewFlags, ViewUpdateFn, anchorDef, checkAndUpdateView, checkNoChangesView, createRootView, elementDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index';
import {inject} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {isBrowser, setupAndCheckRenderer} from './helper';
export function main() {
if (isBrowser()) {
defineTests({directDom: true, viewFlags: ViewFlags.DirectDom});
}
defineTests({directDom: false, viewFlags: 0});
}
function defineTests(config: {directDom: boolean, viewFlags: number}) {
describe(`View Elements, directDom: ${config.directDom}`, () => {
setupAndCheckRenderer(config);
let services: Services;
let renderComponentType: RenderComponentType;
beforeEach(
inject([RootRenderer, Sanitizer], (rootRenderer: RootRenderer, sanitizer: Sanitizer) => {
services = new DefaultServices(rootRenderer, sanitizer);
renderComponentType =
new RenderComponentType('1', 'someUrl', 0, ViewEncapsulation.None, [], {});
}));
function compViewDef(nodes: NodeDef[], updater?: ViewUpdateFn): ViewDefinition {
return viewDef(config.viewFlags, nodes, updater, renderComponentType);
}
function createAndGetRootNodes(viewDef: ViewDefinition): {rootNodes: any[], view: ViewData} {
const view = createRootView(services, viewDef);
const rootNodes = rootRenderNodes(view);
return {rootNodes, view};
}
describe('create', () => {
it('should create elements without parents', () => {
const rootNodes =
createAndGetRootNodes(compViewDef([elementDef(NodeFlags.None, 0, 'span')])).rootNodes;
expect(rootNodes.length).toBe(1);
expect(getDOM().nodeName(rootNodes[0]).toLowerCase()).toBe('span');
});
it('should create views with multiple root elements', () => {
const rootNodes =
createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, 0, 'span'), elementDef(NodeFlags.None, 0, 'span')
])).rootNodes;
expect(rootNodes.length).toBe(2);
});
it('should create elements with parents', () => {
const rootNodes = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, 1, 'div'),
elementDef(NodeFlags.None, 0, 'span'),
])).rootNodes;
expect(rootNodes.length).toBe(1);
const spanEl = getDOM().childNodes(rootNodes[0])[0];
expect(getDOM().nodeName(spanEl).toLowerCase()).toBe('span');
});
it('should set fixed attributes', () => {
const rootNodes = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, 0, 'div', {'title': 'a'}),
])).rootNodes;
expect(rootNodes.length).toBe(1);
expect(getDOM().getAttribute(rootNodes[0], 'title')).toBe('a');
});
});
it('should checkNoChanges', () => {
let attrValue = 'v1';
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(
NodeFlags.None, 0, 'div', null,
[[BindingType.ElementAttribute, 'a1', SecurityContext.NONE]]),
],
(updater, view) => updater.checkInline(view, 0, attrValue)));
checkAndUpdateView(view);
checkNoChangesView(view);
attrValue = 'v2';
expect(() => checkNoChangesView(view))
.toThrowError(
`Expression has changed after it was checked. Previous value: 'v1'. Current value: 'v2'.`);
});
describe('change properties', () => {
[{
name: 'inline',
updater: (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, 'v1', 'v2')
},
{
name: 'dynamic',
updater: (updater: NodeUpdater, view: ViewData) =>
updater.checkDynamic(view, 0, ['v1', 'v2'])
}].forEach((config) => {
it(`should update ${config.name}`, () => {
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(
NodeFlags.None, 0, 'input', null,
[
[BindingType.ElementProperty, 'title', SecurityContext.NONE],
[BindingType.ElementProperty, 'value', SecurityContext.NONE]
]),
],
config.updater));
checkAndUpdateView(view);
const el = rootNodes[0];
expect(getDOM().getProperty(el, 'title')).toBe('v1');
expect(getDOM().getProperty(el, 'value')).toBe('v2');
});
});
});
describe('change attributes', () => {
[{
name: 'inline',
updater: (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, 'v1', 'v2')
},
{
name: 'dynamic',
updater: (updater: NodeUpdater, view: ViewData) =>
updater.checkDynamic(view, 0, ['v1', 'v2'])
}].forEach((config) => {
it(`should update ${config.name}`, () => {
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(
NodeFlags.None, 0, 'div', null,
[
[BindingType.ElementAttribute, 'a1', SecurityContext.NONE],
[BindingType.ElementAttribute, 'a2', SecurityContext.NONE]
]),
],
config.updater));
checkAndUpdateView(view);
const el = rootNodes[0];
expect(getDOM().getAttribute(el, 'a1')).toBe('v1');
expect(getDOM().getAttribute(el, 'a2')).toBe('v2');
});
});
});
describe('change classes', () => {
[{
name: 'inline',
updater: (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, true, true)
},
{
name: 'dynamic',
updater: (updater: NodeUpdater, view: ViewData) =>
updater.checkDynamic(view, 0, [true, true])
}].forEach((config) => {
it(`should update ${config.name}`, () => {
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(
NodeFlags.None, 0, 'div', null,
[[BindingType.ElementClass, 'c1'], [BindingType.ElementClass, 'c2']]),
],
config.updater));
checkAndUpdateView(view);
const el = rootNodes[0];
expect(getDOM().hasClass(el, 'c1')).toBeTruthy();
expect(getDOM().hasClass(el, 'c2')).toBeTruthy();
});
});
});
describe('change styles', () => {
[{
name: 'inline',
updater: (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, 10, 'red')
},
{
name: 'dynamic',
updater: (updater: NodeUpdater, view: ViewData) =>
updater.checkDynamic(view, 0, [10, 'red'])
}].forEach((config) => {
it(`should update ${config.name}`, () => {
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(
NodeFlags.None, 0, 'div', null,
[
[BindingType.ElementStyle, 'width', 'px'],
[BindingType.ElementStyle, 'color', null]
]),
],
config.updater));
checkAndUpdateView(view);
const el = rootNodes[0];
expect(getDOM().getStyle(el, 'width')).toBe('10px');
expect(getDOM().getStyle(el, 'color')).toBe('red');
});
});
});
});
}

View File

@ -0,0 +1,169 @@
/**
* @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 {RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation} from '@angular/core';
import {BindingType, DefaultServices, NodeDef, NodeFlags, NodeUpdater, Services, ViewData, ViewDefinition, ViewFlags, ViewUpdateFn, anchorDef, attachEmbeddedView, checkAndUpdateView, checkNoChangesView, createEmbeddedView, createRootView, destroyView, detachEmbeddedView, elementDef, providerDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index';
import {inject} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {isBrowser, setupAndCheckRenderer} from './helper';
export function main() {
if (isBrowser()) {
defineTests({directDom: true, viewFlags: ViewFlags.DirectDom});
}
defineTests({directDom: false, viewFlags: 0});
}
function defineTests(config: {directDom: boolean, viewFlags: number}) {
describe(`Embedded Views, directDom: ${config.directDom}`, () => {
setupAndCheckRenderer(config);
let services: Services;
let renderComponentType: RenderComponentType;
beforeEach(
inject([RootRenderer, Sanitizer], (rootRenderer: RootRenderer, sanitizer: Sanitizer) => {
services = new DefaultServices(rootRenderer, sanitizer);
renderComponentType =
new RenderComponentType('1', 'someUrl', 0, ViewEncapsulation.None, [], {});
}));
function compViewDef(nodes: NodeDef[], updater?: ViewUpdateFn): ViewDefinition {
return viewDef(config.viewFlags, nodes, updater, renderComponentType);
}
function embeddedViewDef(nodes: NodeDef[], updater?: ViewUpdateFn): ViewDefinition {
return viewDef(config.viewFlags, nodes, updater);
}
function createAndGetRootNodes(
viewDef: ViewDefinition, context: any = null): {rootNodes: any[], view: ViewData} {
const view = createRootView(services, viewDef, context);
const rootNodes = rootRenderNodes(view);
return {rootNodes, view};
}
it('should attach and detach embedded views', () => {
const {view: parentView, rootNodes} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, 2, 'div'),
anchorDef(
NodeFlags.HasEmbeddedViews, 0,
embeddedViewDef([elementDef(NodeFlags.None, 0, 'span', {'name': 'child0'})])),
anchorDef(NodeFlags.None, 0, embeddedViewDef([elementDef(
NodeFlags.None, 0, 'span', {'name': 'child1'})]))
]));
const childView0 = createEmbeddedView(parentView, parentView.def.nodes[1]);
const childView1 = createEmbeddedView(parentView, parentView.def.nodes[2]);
const rootChildren = getDOM().childNodes(rootNodes[0]);
attachEmbeddedView(parentView.nodes[1], 0, childView0);
attachEmbeddedView(parentView.nodes[1], 1, childView1);
// 2 anchors + 2 elements
expect(rootChildren.length).toBe(4);
expect(getDOM().getAttribute(rootChildren[1], 'name')).toBe('child0');
expect(getDOM().getAttribute(rootChildren[2], 'name')).toBe('child1');
detachEmbeddedView(parentView.nodes[1], 1);
detachEmbeddedView(parentView.nodes[1], 0);
expect(getDOM().childNodes(rootNodes[0]).length).toBe(2);
});
it('should include embedded views in root nodes', () => {
const {view: parentView} = createAndGetRootNodes(compViewDef([
anchorDef(
NodeFlags.HasEmbeddedViews, 0,
embeddedViewDef([elementDef(NodeFlags.None, 0, 'span', {'name': 'child0'})])),
elementDef(NodeFlags.None, 0, 'span', {'name': 'after'})
]));
const childView0 = createEmbeddedView(parentView, parentView.def.nodes[0]);
attachEmbeddedView(parentView.nodes[0], 0, childView0);
const rootNodes = rootRenderNodes(parentView);
expect(rootNodes.length).toBe(3);
expect(getDOM().getAttribute(rootNodes[1], 'name')).toBe('child0');
expect(getDOM().getAttribute(rootNodes[2], 'name')).toBe('after');
});
it('should dirty check embedded views', () => {
let childValue = 'v1';
const parentContext = new Object();
const childContext = new Object();
const updater = jasmine.createSpy('updater').and.callFake(
(updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, childValue));
const {view: parentView, rootNodes} = createAndGetRootNodes(
compViewDef([
elementDef(NodeFlags.None, 1, 'div'),
anchorDef(
NodeFlags.HasEmbeddedViews, 0,
embeddedViewDef(
[elementDef(
NodeFlags.None, 0, 'span', null,
[[BindingType.ElementAttribute, 'name', SecurityContext.NONE]])],
updater))
]),
parentContext);
const childView0 = createEmbeddedView(parentView, parentView.def.nodes[1], childContext);
const rootEl = rootNodes[0];
attachEmbeddedView(parentView.nodes[1], 0, childView0);
checkAndUpdateView(parentView);
expect(updater).toHaveBeenCalled();
// component
expect(updater.calls.mostRecent().args[2]).toBe(parentContext);
// view context
expect(updater.calls.mostRecent().args[3]).toBe(childContext);
updater.calls.reset();
checkNoChangesView(parentView);
expect(updater).toHaveBeenCalled();
// component
expect(updater.calls.mostRecent().args[2]).toBe(parentContext);
// view context
expect(updater.calls.mostRecent().args[3]).toBe(childContext);
childValue = 'v2';
expect(() => checkNoChangesView(parentView))
.toThrowError(
`Expression has changed after it was checked. Previous value: 'v1'. Current value: 'v2'.`);
});
it('should destroy embedded views', () => {
const log: string[] = [];
class ChildProvider {
ngOnDestroy() { log.push('ngOnDestroy'); };
}
const {view: parentView, rootNodes} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, 1, 'div'),
anchorDef(NodeFlags.HasEmbeddedViews, 0, embeddedViewDef([
elementDef(NodeFlags.None, 1, 'span'),
providerDef(NodeFlags.OnDestroy, ChildProvider, [])
]))
]));
const childView0 = createEmbeddedView(parentView, parentView.def.nodes[1]);
attachEmbeddedView(parentView.nodes[1], 0, childView0);
destroyView(parentView);
expect(log).toEqual(['ngOnDestroy']);
});
});
}

View File

@ -0,0 +1,36 @@
/**
* @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 {RootRenderer} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
export function isBrowser() {
return getDOM().supportsDOMEvents();
}
export function setupAndCheckRenderer(config: {directDom: boolean}) {
let rootRenderer: any;
if (config.directDom) {
beforeEach(() => {
rootRenderer = <any>{
renderComponent: jasmine.createSpy('renderComponent')
.and.throwError('Renderer should not have been called!')
};
TestBed.configureTestingModule(
{providers: [{provide: RootRenderer, useValue: rootRenderer}]});
});
afterEach(() => { expect(rootRenderer.renderComponent).not.toHaveBeenCalled(); });
} else {
beforeEach(() => {
rootRenderer = TestBed.get(RootRenderer);
spyOn(rootRenderer, 'renderComponent').and.callThrough();
});
afterEach(() => { expect(rootRenderer.renderComponent).toHaveBeenCalled(); });
}
}

View File

@ -0,0 +1,328 @@
/**
* @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 {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, DoCheck, ElementRef, OnChanges, OnDestroy, OnInit, RenderComponentType, Renderer, RootRenderer, Sanitizer, SecurityContext, SimpleChange, TemplateRef, ViewContainerRef, ViewEncapsulation} from '@angular/core';
import {BindingType, DefaultServices, NodeDef, NodeFlags, NodeUpdater, Services, ViewData, ViewDefinition, ViewFlags, ViewUpdateFn, anchorDef, checkAndUpdateView, checkNoChangesView, createRootView, destroyView, elementDef, providerDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index';
import {inject} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {isBrowser, setupAndCheckRenderer} from './helper';
export function main() {
if (isBrowser()) {
defineTests({directDom: true, viewFlags: ViewFlags.DirectDom});
}
defineTests({directDom: false, viewFlags: 0});
}
function defineTests(config: {directDom: boolean, viewFlags: number}) {
describe(`View Providers, directDom: ${config.directDom}`, () => {
setupAndCheckRenderer(config);
let services: Services;
let renderComponentType: RenderComponentType;
beforeEach(
inject([RootRenderer, Sanitizer], (rootRenderer: RootRenderer, sanitizer: Sanitizer) => {
services = new DefaultServices(rootRenderer, sanitizer);
renderComponentType =
new RenderComponentType('1', 'someUrl', 0, ViewEncapsulation.None, [], {});
}));
function compViewDef(nodes: NodeDef[], updater?: ViewUpdateFn): ViewDefinition {
return viewDef(config.viewFlags, nodes, updater, renderComponentType);
}
function embeddedViewDef(nodes: NodeDef[], updater?: ViewUpdateFn): ViewDefinition {
return viewDef(config.viewFlags, nodes, updater);
}
function createAndGetRootNodes(viewDef: ViewDefinition): {rootNodes: any[], view: ViewData} {
const view = createRootView(services, viewDef);
const rootNodes = rootRenderNodes(view);
return {rootNodes, view};
}
describe('create', () => {
it('should create providers eagerly', () => {
let instances: SomeService[] = [];
class SomeService {
constructor() { instances.push(this); }
}
createAndGetRootNodes(compViewDef(
[elementDef(NodeFlags.None, 1, 'span'), providerDef(NodeFlags.None, SomeService, [])]));
expect(instances.length).toBe(1);
});
describe('deps', () => {
let instance: SomeService;
class Dep {}
class SomeService {
constructor(public dep: any) { instance = this; }
}
beforeEach(() => { instance = null; });
it('should inject deps from the same element', () => {
createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, 2, 'span'), providerDef(NodeFlags.None, Dep, []),
providerDef(NodeFlags.None, SomeService, [Dep])
]));
expect(instance.dep instanceof Dep).toBeTruthy();
});
it('should inject deps from a parent element', () => {
createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, 3, 'span'), providerDef(NodeFlags.None, Dep, []),
elementDef(NodeFlags.None, 1, 'span'), providerDef(NodeFlags.None, SomeService, [Dep])
]));
expect(instance.dep instanceof Dep).toBeTruthy();
});
it('should not inject deps from sibling root elements', () => {
const nodes = [
elementDef(NodeFlags.None, 1, 'span'), providerDef(NodeFlags.None, Dep, []),
elementDef(NodeFlags.None, 1, 'span'), providerDef(NodeFlags.None, SomeService, [Dep])
];
// root elements
expect(() => createAndGetRootNodes(compViewDef(nodes)))
.toThrowError('No provider for Dep!');
// non root elements
expect(
() => createAndGetRootNodes(
compViewDef([elementDef(NodeFlags.None, 4, 'span')].concat(nodes))))
.toThrowError('No provider for Dep!');
});
it('should inject from a parent elment in a parent view', () => {
createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, 1, 'div'),
providerDef(
NodeFlags.None, Dep, [], null, () => compViewDef([
elementDef(NodeFlags.None, 1, 'span'),
providerDef(NodeFlags.None, SomeService, [Dep])
])),
]));
expect(instance.dep instanceof Dep).toBeTruthy();
});
describe('builtin tokens', () => {
it('should inject ViewContainerRef', () => {
createAndGetRootNodes(compViewDef([
anchorDef(NodeFlags.HasEmbeddedViews, 1),
providerDef(NodeFlags.None, SomeService, [ViewContainerRef])
]));
expect(instance.dep.createEmbeddedView).toBeTruthy();
});
it('should inject TemplateRef', () => {
createAndGetRootNodes(compViewDef([
anchorDef(NodeFlags.None, 1, embeddedViewDef([anchorDef(NodeFlags.None, 0)])),
providerDef(NodeFlags.None, SomeService, [TemplateRef])
]));
expect(instance.dep.createEmbeddedView).toBeTruthy();
});
it('should inject ElementRef', () => {
createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, 1, 'span'),
providerDef(NodeFlags.None, SomeService, [ElementRef])
]));
expect(getDOM().nodeName(instance.dep.nativeElement).toLowerCase()).toBe('span');
});
if (config.directDom) {
it('should not inject Renderer when using directDom', () => {
expect(() => createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, 1, 'span'),
providerDef(NodeFlags.None, SomeService, [Renderer])
])))
.toThrowError('No provider for Renderer!');
});
} else {
it('should inject Renderer when not using directDom', () => {
createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, 1, 'span'),
providerDef(NodeFlags.None, SomeService, [Renderer])
]));
expect(instance.dep.createElement).toBeTruthy();
});
}
});
});
});
describe('data binding', () => {
[{
name: 'inline',
updater: (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 1, 'v1', 'v2')
},
{
name: 'dynamic',
updater: (updater: NodeUpdater, view: ViewData) =>
updater.checkDynamic(view, 1, ['v1', 'v2'])
}].forEach((config) => {
it(`should update ${config.name}`, () => {
let instance: SomeService;
class SomeService {
a: any;
b: any;
constructor() { instance = this; }
}
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(NodeFlags.None, 1, 'span'),
providerDef(NodeFlags.None, SomeService, [], {a: [0, 'a'], b: [1, 'b']})
],
config.updater));
checkAndUpdateView(view);
expect(instance.a).toBe('v1');
expect(instance.b).toBe('v2');
});
});
it('should checkNoChanges', () => {
class SomeService {
a: any;
}
let propValue = 'v1';
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(NodeFlags.None, 1, 'span'),
providerDef(NodeFlags.None, SomeService, [], {a: [0, 'a']})
],
(updater, view) => updater.checkInline(view, 1, propValue)));
checkAndUpdateView(view);
checkNoChangesView(view);
propValue = 'v2';
expect(() => checkNoChangesView(view))
.toThrowError(
`Expression has changed after it was checked. Previous value: 'v1'. Current value: 'v2'.`);
});
});
describe('lifecycle hooks', () => {
it('should call the lifecycle hooks in the right order', () => {
let instanceCount = 0;
let log: string[] = [];
class SomeService implements OnInit, DoCheck, OnChanges, AfterContentInit,
AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy {
id: number;
a: any;
ngOnInit() { log.push(`${this.id}_ngOnInit`); }
ngDoCheck() { log.push(`${this.id}_ngDoCheck`); }
ngOnChanges() { log.push(`${this.id}_ngOnChanges`); }
ngAfterContentInit() { log.push(`${this.id}_ngAfterContentInit`); }
ngAfterContentChecked() { log.push(`${this.id}_ngAfterContentChecked`); }
ngAfterViewInit() { log.push(`${this.id}_ngAfterViewInit`); }
ngAfterViewChecked() { log.push(`${this.id}_ngAfterViewChecked`); }
ngOnDestroy() { log.push(`${this.id}_ngOnDestroy`); }
constructor() { this.id = instanceCount++; }
}
const allFlags = NodeFlags.OnInit | NodeFlags.DoCheck | NodeFlags.OnChanges |
NodeFlags.AfterContentInit | NodeFlags.AfterContentChecked | NodeFlags.AfterViewInit |
NodeFlags.AfterViewChecked | NodeFlags.OnDestroy;
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(NodeFlags.None, 3, 'span'),
providerDef(allFlags, SomeService, [], {a: [0, 'a']}),
elementDef(NodeFlags.None, 1, 'span'),
providerDef(allFlags, SomeService, [], {a: [0, 'a']})
],
(updater) => {
updater.checkInline(view, 1, 'someValue');
updater.checkInline(view, 3, 'someValue');
}));
checkAndUpdateView(view);
// Note: After... hooks are called bottom up.
expect(log).toEqual([
'0_ngOnChanges',
'0_ngOnInit',
'0_ngDoCheck',
'1_ngOnChanges',
'1_ngOnInit',
'1_ngDoCheck',
'1_ngAfterContentInit',
'1_ngAfterContentChecked',
'0_ngAfterContentInit',
'0_ngAfterContentChecked',
'1_ngAfterViewInit',
'1_ngAfterViewChecked',
'0_ngAfterViewInit',
'0_ngAfterViewChecked',
]);
log = [];
checkAndUpdateView(view);
// Note: After... hooks are called bottom up.
expect(log).toEqual([
'0_ngDoCheck', '1_ngDoCheck', '1_ngAfterContentChecked', '0_ngAfterContentChecked',
'1_ngAfterViewChecked', '0_ngAfterViewChecked'
]);
log = [];
destroyView(view);
// Note: ngOnDestroy ist called bottom up.
expect(log).toEqual(['1_ngOnDestroy', '0_ngOnDestroy']);
});
it('should call ngOnChanges with the changed values and the non minified names', () => {
let changesLog: SimpleChange[] = [];
let currValue = 'v1';
class SomeService implements OnChanges {
a: any;
ngOnChanges(changes: {[name: string]: SimpleChange}) {
changesLog.push(changes['nonMinifiedA']);
}
}
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(NodeFlags.None, 1, 'span'),
providerDef(NodeFlags.OnChanges, SomeService, [], {a: [0, 'nonMinifiedA']})
],
(updater) => updater.checkInline(view, 1, currValue)));
checkAndUpdateView(view);
expect(changesLog).toEqual([new SimpleChange(undefined, 'v1', true)]);
currValue = 'v2';
changesLog = [];
checkAndUpdateView(view);
expect(changesLog).toEqual([new SimpleChange('v1', 'v2', false)]);
});
});
});
}

View File

@ -0,0 +1,114 @@
/**
* @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 {RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation} from '@angular/core';
import {DefaultServices, NodeDef, NodeFlags, NodeUpdater, Services, ViewData, ViewDefinition, ViewFlags, ViewUpdateFn, anchorDef, checkAndUpdateView, checkNoChangesView, createRootView, elementDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index';
import {inject} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {isBrowser, setupAndCheckRenderer} from './helper';
export function main() {
if (isBrowser()) {
defineTests({directDom: true, viewFlags: ViewFlags.DirectDom});
}
defineTests({directDom: false, viewFlags: 0});
}
function defineTests(config: {directDom: boolean, viewFlags: number}) {
describe(`View Text, directDom: ${config.directDom}`, () => {
setupAndCheckRenderer(config);
let services: Services;
let renderComponentType: RenderComponentType;
beforeEach(
inject([RootRenderer, Sanitizer], (rootRenderer: RootRenderer, sanitizer: Sanitizer) => {
services = new DefaultServices(rootRenderer, sanitizer);
renderComponentType =
new RenderComponentType('1', 'someUrl', 0, ViewEncapsulation.None, [], {});
}));
function compViewDef(nodes: NodeDef[], updater?: ViewUpdateFn): ViewDefinition {
return viewDef(config.viewFlags, nodes, updater, renderComponentType);
}
function createAndGetRootNodes(viewDef: ViewDefinition): {rootNodes: any[], view: ViewData} {
const view = createRootView(services, viewDef);
const rootNodes = rootRenderNodes(view);
return {rootNodes, view};
}
describe('create', () => {
it('should create text nodes without parents', () => {
const rootNodes = createAndGetRootNodes(compViewDef([textDef(['a'])])).rootNodes;
expect(rootNodes.length).toBe(1);
expect(getDOM().getText(rootNodes[0])).toBe('a');
});
it('should create views with multiple root text nodes', () => {
const rootNodes =
createAndGetRootNodes(compViewDef([textDef(['a']), textDef(['b'])])).rootNodes;
expect(rootNodes.length).toBe(2);
});
it('should create text nodes with parents', () => {
const rootNodes = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, 1, 'div'),
textDef(['a']),
])).rootNodes;
expect(rootNodes.length).toBe(1);
const textNode = getDOM().firstChild(rootNodes[0]);
expect(getDOM().getText(textNode)).toBe('a');
});
});
it('should checkNoChanges', () => {
let textValue = 'v1';
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
textDef(['', '']),
],
(updater, view) => updater.checkInline(view, 0, textValue)));
checkAndUpdateView(view);
checkNoChangesView(view);
textValue = 'v2';
expect(() => checkNoChangesView(view))
.toThrowError(
`Expression has changed after it was checked. Previous value: 'v1'. Current value: 'v2'.`);
});
describe('change text', () => {
[{
name: 'inline',
updater: (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, 'a', 'b')
},
{
name: 'dynamic',
updater: (updater: NodeUpdater, view: ViewData) =>
updater.checkDynamic(view, 0, ['a', 'b'])
}].forEach((config) => {
it(`should update ${config.name}`, () => {
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
textDef(['0', '1', '2']),
],
config.updater));
checkAndUpdateView(view);
const node = rootNodes[0];
expect(getDOM().getText(rootNodes[0])).toBe('0a1b2');
});
});
});
});
}

View File

@ -0,0 +1,167 @@
/**
* @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 {NodeFlags, NodeUpdater, ViewData, ViewDefinition, ViewFlags, anchorDef, checkAndUpdateView, checkNoChangesView, elementDef, providerDef, textDef, viewDef} from '@angular/core/src/view/index';
export function main() {
describe('viewDef', () => {
describe('reverseChild order', () => {
function reverseChildOrder(viewDef: ViewDefinition): number[] {
return viewDef.reverseChildNodes.map(node => node.index);
}
it('should reverse child order for root nodes', () => {
const vd = viewDef(ViewFlags.None, [
textDef(['a']), // level 0, index 0
textDef(['a']), // level 0, index 0
]);
expect(reverseChildOrder(vd)).toEqual([1, 0]);
});
it('should reverse child order for one level, one root', () => {
const vd = viewDef(ViewFlags.None, [
elementDef(NodeFlags.None, 2, 'span'), // level 0, index 0
textDef(['a']), // level 1, index 1
textDef(['a']), // level 1, index 2
]);
expect(reverseChildOrder(vd)).toEqual([0, 2, 1]);
});
it('should reverse child order for 1 level, 2 roots', () => {
const vd = viewDef(ViewFlags.None, [
elementDef(NodeFlags.None, 2, 'span'), // level 0, index 0
textDef(['a']), // level 1, index 1
textDef(['a']), // level 1, index 2
elementDef(NodeFlags.None, 1, 'span'), // level 0, index 3
textDef(['a']), // level 1, index 4
]);
expect(reverseChildOrder(vd)).toEqual([3, 4, 0, 2, 1]);
});
it('should reverse child order for 2 levels', () => {
const vd = viewDef(ViewFlags.None, [
elementDef(NodeFlags.None, 4, 'span'), // level 0, index 0
elementDef(NodeFlags.None, 1, 'span'), // level 1, index 1
textDef(['a']), // level 2, index 2
elementDef(NodeFlags.None, 1, 'span'), // level 1, index 3
textDef(['a']), // level 2, index 4
]);
expect(reverseChildOrder(vd)).toEqual([0, 3, 4, 1, 2]);
});
it('should reverse child order for mixed levels', () => {
const vd = viewDef(ViewFlags.None, [
textDef(['a']), // level 0, index 0
elementDef(NodeFlags.None, 5, 'span'), // level 0, index 1
textDef(['a']), // level 1, index 2
elementDef(NodeFlags.None, 1, 'span'), // level 1, index 3
textDef(['a']), // level 2, index 4
elementDef(NodeFlags.None, 1, 'span'), // level 1, index 5
textDef(['a']), // level 2, index 6
textDef(['a']), // level 0, index 7
]);
expect(reverseChildOrder(vd)).toEqual([7, 1, 5, 6, 3, 4, 2, 0]);
});
});
describe('parent', () => {
function parents(viewDef: ViewDefinition): number[] {
return viewDef.nodes.map(node => node.parent);
}
it('should calculate parents for one level', () => {
const vd = viewDef(ViewFlags.None, [
elementDef(NodeFlags.None, 2, 'span'),
textDef(['a']),
textDef(['a']),
]);
expect(parents(vd)).toEqual([undefined, 0, 0]);
});
it('should calculate parents for one level, multiple roots', () => {
const vd = viewDef(ViewFlags.None, [
elementDef(NodeFlags.None, 1, 'span'),
textDef(['a']),
elementDef(NodeFlags.None, 1, 'span'),
textDef(['a']),
textDef(['a']),
]);
expect(parents(vd)).toEqual([undefined, 0, undefined, 2, undefined]);
});
it('should calculate parents for multiple levels', () => {
const vd = viewDef(ViewFlags.None, [
elementDef(NodeFlags.None, 2, 'span'),
elementDef(NodeFlags.None, 1, 'span'),
textDef(['a']),
elementDef(NodeFlags.None, 1, 'span'),
textDef(['a']),
textDef(['a']),
]);
expect(parents(vd)).toEqual([undefined, 0, 1, undefined, 3, undefined]);
});
});
describe('childFlags', () => {
function childFlags(viewDef: ViewDefinition): number[] {
return viewDef.nodes.map(node => node.childFlags);
}
it('should calculate childFlags for one level', () => {
const vd = viewDef(ViewFlags.None, [
elementDef(NodeFlags.None, 1, 'span'),
providerDef(NodeFlags.AfterContentChecked, AService, [])
]);
expect(childFlags(vd)).toEqual([NodeFlags.AfterContentChecked, NodeFlags.None]);
});
it('should calculate childFlags for one level, multiple roots', () => {
const vd = viewDef(ViewFlags.None, [
elementDef(NodeFlags.None, 1, 'span'),
providerDef(NodeFlags.AfterContentChecked, AService, []),
elementDef(NodeFlags.None, 2, 'span'),
providerDef(NodeFlags.AfterContentInit, AService, []),
providerDef(NodeFlags.AfterViewChecked, AService, []),
]);
expect(childFlags(vd)).toEqual([
NodeFlags.AfterContentChecked, NodeFlags.None,
NodeFlags.AfterContentInit | NodeFlags.AfterViewChecked, NodeFlags.None, NodeFlags.None
]);
});
it('should calculate childFlags for multiple levels', () => {
const vd = viewDef(ViewFlags.None, [
elementDef(NodeFlags.None, 2, 'span'),
elementDef(NodeFlags.None, 1, 'span'),
providerDef(NodeFlags.AfterContentChecked, AService, []),
elementDef(NodeFlags.None, 2, 'span'),
providerDef(NodeFlags.AfterContentInit, AService, []),
providerDef(NodeFlags.AfterViewInit, AService, []),
]);
expect(childFlags(vd)).toEqual([
NodeFlags.AfterContentChecked, NodeFlags.AfterContentChecked, NodeFlags.None,
NodeFlags.AfterContentInit | NodeFlags.AfterViewInit, NodeFlags.None, NodeFlags.None
]);
});
});
});
}
class AService {}