feat(core): add event support to view engine

Part of #14013
This commit is contained in:
Tobias Bosch 2017-01-19 10:25:03 -08:00 committed by Alex Rickabaugh
parent f20d1a8af5
commit 0adb97bffb
13 changed files with 429 additions and 135 deletions

View File

@ -18,11 +18,13 @@ export function anchorDef(
parent: undefined, parent: undefined,
childFlags: undefined, childFlags: undefined,
bindingIndex: undefined, bindingIndex: undefined,
disposableIndex: undefined,
providerIndices: undefined, providerIndices: undefined,
// regular values // regular values
flags, flags,
childCount, childCount,
bindings: [], bindings: [],
disposableCount: 0,
element: undefined, element: undefined,
provider: undefined, provider: undefined,
text: undefined, text: undefined,

View File

@ -8,14 +8,16 @@
import {SecurityContext} from '../security'; import {SecurityContext} from '../security';
import {BindingDef, BindingType, NodeData, NodeDef, NodeFlags, NodeType, ViewData, ViewFlags} from './types'; import {BindingDef, BindingType, DisposableFn, ElementOutputDef, NodeData, NodeDef, NodeFlags, NodeType, ViewData, ViewFlags} from './types';
import {checkAndUpdateBinding, setBindingDebugInfo} from './util'; import {checkAndUpdateBinding, setBindingDebugInfo} from './util';
export function elementDef( export function elementDef(
flags: NodeFlags, childCount: number, name: string, fixedAttrs: {[name: string]: string} = {}, flags: NodeFlags, childCount: number, name: string, fixedAttrs: {[name: string]: string} = {},
bindings: ([BindingType.ElementClass, string] | [BindingType.ElementStyle, string, string] | [ bindings?:
BindingType.ElementAttribute | BindingType.ElementProperty, string, SecurityContext ([BindingType.ElementClass, string] | [BindingType.ElementStyle, string, string] |
])[] = []): NodeDef { [BindingType.ElementAttribute | BindingType.ElementProperty, string, SecurityContext])[],
outputs?: (string | [string, string])[]): NodeDef {
bindings = bindings || [];
const bindingDefs = new Array(bindings.length); const bindingDefs = new Array(bindings.length);
for (let i = 0; i < bindings.length; i++) { for (let i = 0; i < bindings.length; i++) {
const entry = bindings[i]; const entry = bindings[i];
@ -35,6 +37,19 @@ export function elementDef(
} }
bindingDefs[i] = {type: bindingType, name, nonMinfiedName: name, securityContext, suffix}; bindingDefs[i] = {type: bindingType, name, nonMinfiedName: name, securityContext, suffix};
} }
outputs = outputs || [];
const outputDefs: ElementOutputDef[] = new Array(outputs.length);
for (let i = 0; i < outputs.length; i++) {
const output = outputs[i];
let target: string;
let eventName: string;
if (Array.isArray(output)) {
[target, eventName] = output;
} else {
eventName = output;
}
outputDefs[i] = {eventName: eventName, target: target};
}
return { return {
type: NodeType.Element, type: NodeType.Element,
// will bet set by the view definition // will bet set by the view definition
@ -43,12 +58,14 @@ export function elementDef(
parent: undefined, parent: undefined,
childFlags: undefined, childFlags: undefined,
bindingIndex: undefined, bindingIndex: undefined,
disposableIndex: undefined,
providerIndices: undefined, providerIndices: undefined,
// regular values // regular values
flags, flags,
childCount, childCount,
bindings: bindingDefs, bindings: bindingDefs,
element: {name, attrs: fixedAttrs}, disposableCount: outputDefs.length,
element: {name, attrs: fixedAttrs, outputs: outputDefs},
provider: undefined, provider: undefined,
text: undefined, text: undefined,
component: undefined, component: undefined,
@ -62,22 +79,52 @@ export function createElement(view: ViewData, renderHost: any, def: NodeDef): No
let el: any; let el: any;
if (view.renderer) { if (view.renderer) {
el = view.renderer.createElement(parentNode, elDef.name); el = view.renderer.createElement(parentNode, elDef.name);
if (elDef.attrs) {
for (let attrName in elDef.attrs) {
view.renderer.setElementAttribute(el, attrName, elDef.attrs[attrName]);
}
}
} else { } else {
el = document.createElement(elDef.name); el = document.createElement(elDef.name);
if (parentNode) { if (parentNode) {
parentNode.appendChild(el); parentNode.appendChild(el);
} }
if (elDef.attrs) { }
for (let attrName in elDef.attrs) { if (elDef.attrs) {
for (let attrName in elDef.attrs) {
if (view.renderer) {
view.renderer.setElementAttribute(el, attrName, elDef.attrs[attrName]);
} else {
el.setAttribute(attrName, elDef.attrs[attrName]); el.setAttribute(attrName, elDef.attrs[attrName]);
} }
} }
} }
if (elDef.outputs.length) {
for (let i = 0; i < elDef.outputs.length; i++) {
const output = elDef.outputs[i];
let disposable: DisposableFn;
if (view.renderer) {
const handleEventClosure = renderEventHandlerClosure(view, def.index, output.eventName);
if (output.target) {
disposable =
<any>view.renderer.listenGlobal(output.target, output.eventName, handleEventClosure);
} else {
disposable = <any>view.renderer.listen(el, output.eventName, handleEventClosure);
}
} else {
let target: any;
switch (output.target) {
case 'window':
target = window;
break;
case 'document':
target = document;
break;
default:
target = el;
}
const handleEventClosure = directDomEventHandlerClosure(view, def.index, output.eventName);
target.addEventListener(output.eventName, handleEventClosure);
disposable = target.removeEventListener.bind(target, output.eventName, handleEventClosure);
}
view.disposables[def.disposableIndex + i] = disposable;
}
}
return { return {
renderNode: el, renderNode: el,
provider: undefined, provider: undefined,
@ -86,6 +133,21 @@ export function createElement(view: ViewData, renderHost: any, def: NodeDef): No
}; };
} }
function renderEventHandlerClosure(view: ViewData, index: number, eventName: string) {
return (event: any) => { return view.def.handleEvent(view, index, eventName, event); };
}
function directDomEventHandlerClosure(view: ViewData, index: number, eventName: string) {
return (event: any) => {
const result = view.def.handleEvent(view, index, eventName, event);
if (result === false) {
event.preventDefault();
}
return result;
};
}
export function checkAndUpdateElementInline( export function checkAndUpdateElementInline(
view: ViewData, def: NodeDef, v0: any, v1: any, v2: any, v3: any, v4: any, v5: any, v6: any, view: ViewData, def: NodeDef, v0: any, v1: any, v2: any, v3: any, v4: any, v5: any, v6: any,
v7: any, v8: any, v9: any) { v7: any, v8: any, v9: any) {

View File

@ -14,7 +14,7 @@ import {TemplateRef} from '../linker/template_ref';
import {ViewContainerRef} from '../linker/view_container_ref'; import {ViewContainerRef} from '../linker/view_container_ref';
import {Renderer} from '../render/api'; import {Renderer} from '../render/api';
import {BindingDef, BindingType, DepDef, DepFlags, NodeData, NodeDef, NodeFlags, NodeType, Services, ViewData, ViewDefinition, ViewFlags} from './types'; import {BindingDef, BindingType, DepDef, DepFlags, DisposableFn, NodeData, NodeDef, NodeFlags, NodeType, ProviderOutputDef, Services, ViewData, ViewDefinition, ViewFlags} from './types';
import {checkAndUpdateBinding, checkAndUpdateBindingWithChange, setBindingDebugInfo} from './util'; import {checkAndUpdateBinding, checkAndUpdateBindingWithChange, setBindingDebugInfo} from './util';
const _tokenKeyCache = new Map<any, string>(); const _tokenKeyCache = new Map<any, string>();
@ -26,7 +26,8 @@ const TemplateRefTokenKey = tokenKey(TemplateRef);
export function providerDef( export function providerDef(
flags: NodeFlags, ctor: any, deps: ([DepFlags, any] | any)[], flags: NodeFlags, ctor: any, deps: ([DepFlags, any] | any)[],
props?: {[name: string]: [number, string]}, component?: () => ViewDefinition): NodeDef { props?: {[name: string]: [number, string]}, outputs?: {[name: string]: string},
component?: () => ViewDefinition): NodeDef {
const bindings: BindingDef[] = []; const bindings: BindingDef[] = [];
if (props) { if (props) {
for (let prop in props) { for (let prop in props) {
@ -39,6 +40,12 @@ export function providerDef(
}; };
} }
} }
const outputDefs: ProviderOutputDef[] = [];
if (outputs) {
for (let propName in outputs) {
outputDefs.push({propName, eventName: outputs[propName]});
}
}
const depDefs: DepDef[] = deps.map(value => { const depDefs: DepDef[] = deps.map(value => {
let token: any; let token: any;
let flags: DepFlags; let flags: DepFlags;
@ -61,16 +68,14 @@ export function providerDef(
parent: undefined, parent: undefined,
childFlags: undefined, childFlags: undefined,
bindingIndex: undefined, bindingIndex: undefined,
disposableIndex: undefined,
providerIndices: undefined, providerIndices: undefined,
// regular values // regular values
flags, flags,
childCount: 0, bindings, childCount: 0, bindings,
disposableCount: outputDefs.length,
element: undefined, element: undefined,
provider: { provider: {tokenKey: tokenKey(ctor), ctor, deps: depDefs, outputs: outputDefs},
tokenKey: tokenKey(ctor),
ctor,
deps: depDefs,
},
text: undefined, component, text: undefined, component,
template: undefined template: undefined
}; };
@ -87,10 +92,19 @@ export function tokenKey(token: any): string {
export function createProvider(view: ViewData, def: NodeDef, componentView: ViewData): NodeData { export function createProvider(view: ViewData, def: NodeDef, componentView: ViewData): NodeData {
const providerDef = def.provider; const providerDef = def.provider;
const provider = createInstance(view, def.parent, providerDef.ctor, providerDef.deps);
if (providerDef.outputs.length) {
for (let i = 0; i < providerDef.outputs.length; i++) {
const output = providerDef.outputs[i];
const subscription = provider[output.propName].subscribe(
view.def.handleEvent.bind(null, view, def.parent, output.eventName));
view.disposables[def.disposableIndex + i] = subscription.unsubscribe.bind(subscription);
}
}
return { return {
renderNode: undefined, renderNode: undefined,
provider: createInstance(view, def.parent, providerDef.ctor, providerDef.deps), provider,
embeddedViews: undefined, componentView embeddedViews: undefined, componentView,
}; };
} }

View File

@ -30,10 +30,12 @@ export function textDef(constants: string[]): NodeDef {
parent: undefined, parent: undefined,
childFlags: undefined, childFlags: undefined,
bindingIndex: undefined, bindingIndex: undefined,
disposableIndex: undefined,
providerIndices: undefined, providerIndices: undefined,
// regular values // regular values
flags: 0, flags: 0,
childCount: 0, bindings, childCount: 0, bindings,
disposableCount: 0,
element: undefined, element: undefined,
provider: undefined, provider: undefined,
text: {prefix: constants[0]}, text: {prefix: constants[0]},

View File

@ -19,6 +19,7 @@ export interface ViewDefinition {
flags: ViewFlags; flags: ViewFlags;
componentType: RenderComponentType; componentType: RenderComponentType;
update: ViewUpdateFn; update: ViewUpdateFn;
handleEvent: ViewHandleEventFn;
/** /**
* Order: Depth first. * Order: Depth first.
* Especially providers are before elements / anchros. * Especially providers are before elements / anchros.
@ -33,10 +34,10 @@ export interface ViewDefinition {
reverseChildNodes: NodeDef[]; reverseChildNodes: NodeDef[];
lastRootNode: number; lastRootNode: number;
bindingCount: number; bindingCount: number;
disposableCount: number;
} }
export type ViewUpdateFn = (updater: NodeUpdater, view: ViewData, component: any, context: any) => export type ViewUpdateFn = (updater: NodeUpdater, view: ViewData) => void;
void;
export interface NodeUpdater { export interface NodeUpdater {
checkInline( checkInline(
@ -45,6 +46,9 @@ export interface NodeUpdater {
checkDynamic(view: ViewData, nodeIndex: number, values: any[]): void; checkDynamic(view: ViewData, nodeIndex: number, values: any[]): void;
} }
export type ViewHandleEventFn =
(view: ViewData, nodeIndex: number, eventName: string, event: any) => boolean;
/** /**
* Bitmask for ViewDefintion.flags. * Bitmask for ViewDefintion.flags.
*/ */
@ -66,6 +70,8 @@ export interface NodeDef {
childFlags: NodeFlags; childFlags: NodeFlags;
bindingIndex: number; bindingIndex: number;
bindings: BindingDef[]; bindings: BindingDef[];
disposableIndex: number;
disposableCount: number;
element: ElementDef; element: ElementDef;
providerIndices: {[tokenKey: string]: number}; providerIndices: {[tokenKey: string]: number};
provider: ProviderDef; provider: ProviderDef;
@ -102,6 +108,12 @@ export enum NodeFlags {
export interface ElementDef { export interface ElementDef {
name: string; name: string;
attrs: {[name: string]: string}; attrs: {[name: string]: string};
outputs: ElementOutputDef[];
}
export interface ElementOutputDef {
target: string;
eventName: string;
} }
/** /**
@ -118,10 +130,16 @@ export interface DepDef {
tokenKey: string; tokenKey: string;
} }
export interface ProviderOutputDef {
propName: string;
eventName: string;
}
export interface ProviderDef { export interface ProviderDef {
tokenKey: string; tokenKey: string;
ctor: any; ctor: any;
deps: DepDef[]; deps: DepDef[];
outputs: ProviderOutputDef[];
} }
export interface TextDef { prefix: string; } export interface TextDef { prefix: string; }
@ -164,8 +182,11 @@ export interface ViewData {
nodes: NodeData[]; nodes: NodeData[];
firstChange: boolean; firstChange: boolean;
oldValues: any[]; oldValues: any[];
disposables: DisposableFn[];
} }
export type DisposableFn = () => void;
/** /**
* Node instance data. * Node instance data.
* Attention: Adding fields to this is performance sensitive! * Attention: Adding fields to this is performance sensitive!

View File

@ -13,14 +13,14 @@ import {createAnchor} from './anchor';
import {checkAndUpdateElementDynamic, checkAndUpdateElementInline, createElement} from './element'; import {checkAndUpdateElementDynamic, checkAndUpdateElementInline, createElement} from './element';
import {callLifecycleHooksChildrenFirst, checkAndUpdateProviderDynamic, checkAndUpdateProviderInline, createProvider} from './provider'; import {callLifecycleHooksChildrenFirst, checkAndUpdateProviderDynamic, checkAndUpdateProviderInline, createProvider} from './provider';
import {checkAndUpdateTextDynamic, checkAndUpdateTextInline, createText} from './text'; import {checkAndUpdateTextDynamic, checkAndUpdateTextInline, createText} from './text';
import {ElementDef, NodeData, NodeDef, NodeFlags, NodeType, NodeUpdater, ProviderDef, Services, TextDef, ViewData, ViewDefinition, ViewFlags, ViewUpdateFn} from './types'; import {ElementDef, NodeData, NodeDef, NodeFlags, NodeType, NodeUpdater, ProviderDef, Services, TextDef, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn} from './types';
import {checkBindingNoChanges} from './util'; import {checkBindingNoChanges} from './util';
const NOOP_UPDATE = (): any => undefined; const NOOP = (): any => undefined;
export function viewDef( export function viewDef(
flags: ViewFlags, nodesWithoutIndices: NodeDef[], update?: ViewUpdateFn, flags: ViewFlags, nodesWithoutIndices: NodeDef[], update?: ViewUpdateFn,
componentType?: RenderComponentType): ViewDefinition { handleEvent?: ViewHandleEventFn, componentType?: RenderComponentType): ViewDefinition {
// clone nodes and set auto calculated values // clone nodes and set auto calculated values
if (nodesWithoutIndices.length === 0) { if (nodesWithoutIndices.length === 0) {
throw new Error(`Illegal State: Views without nodes are not allowed!`); throw new Error(`Illegal State: Views without nodes are not allowed!`);
@ -28,6 +28,7 @@ export function viewDef(
const nodes: NodeDef[] = new Array(nodesWithoutIndices.length); const nodes: NodeDef[] = new Array(nodesWithoutIndices.length);
const reverseChildNodes: NodeDef[] = new Array(nodesWithoutIndices.length); const reverseChildNodes: NodeDef[] = new Array(nodesWithoutIndices.length);
let viewBindingCount = 0; let viewBindingCount = 0;
let viewDisposableCount = 0;
let viewFlags = 0; let viewFlags = 0;
let currentParent: NodeDef = null; let currentParent: NodeDef = null;
let lastRootNode: NodeDef = null; let lastRootNode: NodeDef = null;
@ -44,7 +45,8 @@ export function viewDef(
const node = cloneAndModifyNode(nodesWithoutIndices[i], { const node = cloneAndModifyNode(nodesWithoutIndices[i], {
index: i, index: i,
parent: currentParent ? currentParent.index : undefined, parent: currentParent ? currentParent.index : undefined,
bindingIndex: viewBindingCount, reverseChildIndex, bindingIndex: viewBindingCount,
disposableIndex: viewDisposableCount, reverseChildIndex,
providerIndices: Object.create(currentParent ? currentParent.providerIndices : null) providerIndices: Object.create(currentParent ? currentParent.providerIndices : null)
}); });
nodes[i] = node; nodes[i] = node;
@ -53,6 +55,7 @@ export function viewDef(
viewFlags |= node.flags; viewFlags |= node.flags;
viewBindingCount += node.bindings.length; viewBindingCount += node.bindings.length;
viewDisposableCount += node.disposableCount;
if (currentParent) { if (currentParent) {
currentParent.childFlags |= node.flags; currentParent.childFlags |= node.flags;
} }
@ -72,8 +75,10 @@ export function viewDef(
nodeFlags: viewFlags, nodeFlags: viewFlags,
flags, flags,
nodes: nodes, reverseChildNodes, nodes: nodes, reverseChildNodes,
update: update || NOOP_UPDATE, componentType, update: update || NOOP,
handleEvent: handleEvent || NOOP, componentType,
bindingCount: viewBindingCount, bindingCount: viewBindingCount,
disposableCount: viewDisposableCount,
lastRootNode: lastRootNode.index lastRootNode: lastRootNode.index
}; };
} }
@ -148,6 +153,7 @@ function cloneAndModifyNode(nodeDef: NodeDef, values: {
reverseChildIndex: number, reverseChildIndex: number,
parent: number, parent: number,
bindingIndex: number, bindingIndex: number,
disposableIndex: number,
providerIndices: {[tokenKey: string]: number} providerIndices: {[tokenKey: string]: number}
}): NodeDef { }): NodeDef {
const clonedNode: NodeDef = <any>{}; const clonedNode: NodeDef = <any>{};
@ -157,6 +163,7 @@ function cloneAndModifyNode(nodeDef: NodeDef, values: {
clonedNode.index = values.index; clonedNode.index = values.index;
clonedNode.bindingIndex = values.bindingIndex; clonedNode.bindingIndex = values.bindingIndex;
clonedNode.disposableIndex = values.disposableIndex;
clonedNode.parent = values.parent; clonedNode.parent = values.parent;
clonedNode.reverseChildIndex = values.reverseChildIndex; clonedNode.reverseChildIndex = values.reverseChildIndex;
clonedNode.providerIndices = values.providerIndices; clonedNode.providerIndices = values.providerIndices;
@ -188,6 +195,7 @@ function createView(
} else { } else {
renderer = def.componentType ? services.renderComponent(def.componentType) : parent.renderer; renderer = def.componentType ? services.renderComponent(def.componentType) : parent.renderer;
} }
const disposables = def.disposableCount ? new Array(def.disposableCount) : undefined;
const view: ViewData = { const view: ViewData = {
def, def,
parent, parent,
@ -195,7 +203,7 @@ function createView(
context: undefined, context: undefined,
component: undefined, nodes, component: undefined, nodes,
firstChange: true, renderer, services, firstChange: true, renderer, services,
oldValues: new Array(def.bindingCount) oldValues: new Array(def.bindingCount), disposables
}; };
return view; return view;
} }
@ -232,7 +240,7 @@ function initView(view: ViewData, renderHost: any, component: any, context: any)
} }
export function checkNoChangesView(view: ViewData) { export function checkNoChangesView(view: ViewData) {
view.def.update(CheckNoChanges, view, view.component, view.context); view.def.update(CheckNoChanges, view);
execEmbeddedViewsAction(view, ViewAction.CheckNoChanges); execEmbeddedViewsAction(view, ViewAction.CheckNoChanges);
execComponentViewsAction(view, ViewAction.CheckNoChanges); execComponentViewsAction(view, ViewAction.CheckNoChanges);
} }
@ -274,7 +282,7 @@ const CheckNoChanges: NodeUpdater = {
}; };
export function checkAndUpdateView(view: ViewData) { export function checkAndUpdateView(view: ViewData) {
view.def.update(CheckAndUpdate, view, view.component, view.context); view.def.update(CheckAndUpdate, view);
execEmbeddedViewsAction(view, ViewAction.CheckAndUpdate); execEmbeddedViewsAction(view, ViewAction.CheckAndUpdate);
callLifecycleHooksChildrenFirst( callLifecycleHooksChildrenFirst(
@ -320,6 +328,11 @@ const CheckAndUpdate: NodeUpdater = {
export function destroyView(view: ViewData) { export function destroyView(view: ViewData) {
callLifecycleHooksChildrenFirst(view, NodeFlags.OnDestroy); callLifecycleHooksChildrenFirst(view, NodeFlags.OnDestroy);
if (view.disposables) {
for (let i = 0; i < view.disposables.length; i++) {
view.disposables[i]();
}
}
execComponentViewsAction(view, ViewAction.Destroy); execComponentViewsAction(view, ViewAction.Destroy);
execEmbeddedViewsAction(view, ViewAction.Destroy); execEmbeddedViewsAction(view, ViewAction.Destroy);
} }

View File

@ -7,7 +7,7 @@
*/ */
import {RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation} from '@angular/core'; 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 {DefaultServices, NodeDef, NodeFlags, NodeUpdater, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, checkAndUpdateView, checkNoChangesView, createRootView, elementDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index';
import {inject} from '@angular/core/testing'; import {inject} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
@ -34,8 +34,9 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
new RenderComponentType('1', 'someUrl', 0, ViewEncapsulation.None, [], {}); new RenderComponentType('1', 'someUrl', 0, ViewEncapsulation.None, [], {});
})); }));
function compViewDef(nodes: NodeDef[], updater?: ViewUpdateFn): ViewDefinition { function compViewDef(
return viewDef(config.viewFlags, nodes, updater, renderComponentType); nodes: NodeDef[], update?: ViewUpdateFn, handleEvent?: ViewHandleEventFn): ViewDefinition {
return viewDef(config.viewFlags, nodes, update, handleEvent, renderComponentType);
} }
function createAndGetRootNodes(viewDef: ViewDefinition): {rootNodes: any[], view: ViewData} { function createAndGetRootNodes(viewDef: ViewDefinition): {rootNodes: any[], view: ViewData} {

View File

@ -7,7 +7,7 @@
*/ */
import {RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation} from '@angular/core'; 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 {BindingType, DefaultServices, NodeDef, NodeFlags, NodeUpdater, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, checkAndUpdateView, checkNoChangesView, createRootView, destroyView, elementDef, providerDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index';
import {inject} from '@angular/core/testing'; import {inject} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
@ -34,8 +34,9 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
new RenderComponentType('1', 'someUrl', 0, ViewEncapsulation.None, [], {}); new RenderComponentType('1', 'someUrl', 0, ViewEncapsulation.None, [], {});
})); }));
function compViewDef(nodes: NodeDef[], updater?: ViewUpdateFn): ViewDefinition { function compViewDef(
return viewDef(config.viewFlags, nodes, updater, renderComponentType); nodes: NodeDef[], update?: ViewUpdateFn, handleEvent?: ViewHandleEventFn): ViewDefinition {
return viewDef(config.viewFlags, nodes, update, handleEvent, renderComponentType);
} }
function createAndGetRootNodes(viewDef: ViewDefinition): {rootNodes: any[], view: ViewData} { function createAndGetRootNodes(viewDef: ViewDefinition): {rootNodes: any[], view: ViewData} {
@ -45,56 +46,57 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
} }
it('should create and attach component views', () => { it('should create and attach component views', () => {
class AComp {} let instance: AComp;
class AComp {
constructor() { instance = this; }
}
const {view, rootNodes} = createAndGetRootNodes(compViewDef([ const {view, rootNodes} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, 1, 'div'), elementDef(NodeFlags.None, 1, 'div'),
providerDef(NodeFlags.None, AComp, [], null, () => compViewDef([ providerDef(NodeFlags.None, AComp, [], null, null, () => compViewDef([
elementDef(NodeFlags.None, 0, 'span'), elementDef(NodeFlags.None, 0, 'span'),
])), ])),
])); ]));
const compView = view.nodes[1].componentView;
expect(compView.context).toBe(instance);
expect(compView.component).toBe(instance);
const compRootEl = getDOM().childNodes(rootNodes[0])[0]; const compRootEl = getDOM().childNodes(rootNodes[0])[0];
expect(getDOM().nodeName(compRootEl).toLowerCase()).toBe('span'); expect(getDOM().nodeName(compRootEl).toLowerCase()).toBe('span');
}); });
it('should dirty check component views', () => { it('should dirty check component views', () => {
let value = 'v1'; let value = 'v1';
let instance: AComp;
class AComp { class AComp {
a: any; a: any;
constructor() { instance = this; }
} }
const updater = jasmine.createSpy('updater').and.callFake( const update = jasmine.createSpy('updater').and.callFake(
(updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, value)); (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, value));
const {view, rootNodes} = createAndGetRootNodes( const {view, rootNodes} = createAndGetRootNodes(
compViewDef([ compViewDef([
elementDef(NodeFlags.None, 1, 'div'), elementDef(NodeFlags.None, 1, 'div'),
providerDef(NodeFlags.None, AComp, [], null, () => compViewDef( providerDef(NodeFlags.None, AComp, [], null, null, () => compViewDef(
[ [
elementDef(NodeFlags.None, 0, 'span', null, [[BindingType.ElementAttribute, 'a', SecurityContext.NONE]]), elementDef(NodeFlags.None, 0, 'span', null, [[BindingType.ElementAttribute, 'a', SecurityContext.NONE]]),
], updater ], update
)), )),
], jasmine.createSpy('parentUpdater'))); ], jasmine.createSpy('parentUpdater')));
const compView = view.nodes[1].componentView;
checkAndUpdateView(view); checkAndUpdateView(view);
expect(updater).toHaveBeenCalled(); expect(update).toHaveBeenCalled();
// component expect(update.calls.mostRecent().args[1]).toBe(compView);
expect(updater.calls.mostRecent().args[2]).toBe(instance);
// view context
expect(updater.calls.mostRecent().args[3]).toBe(instance);
updater.calls.reset(); update.calls.reset();
checkNoChangesView(view); checkNoChangesView(view);
expect(updater).toHaveBeenCalled(); expect(update).toHaveBeenCalled();
// component expect(update.calls.mostRecent().args[1]).toBe(compView);
expect(updater.calls.mostRecent().args[2]).toBe(instance);
// view context
expect(updater.calls.mostRecent().args[3]).toBe(instance);
value = 'v2'; value = 'v2';
expect(() => checkNoChangesView(view)) expect(() => checkNoChangesView(view))
@ -114,10 +116,11 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
const {view, rootNodes} = createAndGetRootNodes(compViewDef([ const {view, rootNodes} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, 1, 'div'), elementDef(NodeFlags.None, 1, 'div'),
providerDef( providerDef(
NodeFlags.None, AComp, [], null, () => compViewDef([ NodeFlags.None, AComp, [], null, null,
elementDef(NodeFlags.None, 1, 'span'), () => compViewDef([
providerDef(NodeFlags.OnDestroy, ChildProvider, []) elementDef(NodeFlags.None, 1, 'span'),
])), providerDef(NodeFlags.OnDestroy, ChildProvider, [])
])),
])); ]));
destroyView(view); destroyView(view);

View File

@ -7,7 +7,7 @@
*/ */
import {RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation} from '@angular/core'; 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 {BindingType, DefaultServices, NodeDef, NodeFlags, NodeUpdater, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, checkAndUpdateView, checkNoChangesView, createRootView, destroyView, elementDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index';
import {inject} from '@angular/core/testing'; import {inject} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
@ -34,8 +34,9 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
new RenderComponentType('1', 'someUrl', 0, ViewEncapsulation.None, [], {}); new RenderComponentType('1', 'someUrl', 0, ViewEncapsulation.None, [], {});
})); }));
function compViewDef(nodes: NodeDef[], updater?: ViewUpdateFn): ViewDefinition { function compViewDef(
return viewDef(config.viewFlags, nodes, updater, renderComponentType); nodes: NodeDef[], update?: ViewUpdateFn, handleEvent?: ViewHandleEventFn): ViewDefinition {
return viewDef(config.viewFlags, nodes, update, handleEvent, renderComponentType);
} }
function createAndGetRootNodes(viewDef: ViewDefinition): {rootNodes: any[], view: ViewData} { function createAndGetRootNodes(viewDef: ViewDefinition): {rootNodes: any[], view: ViewData} {
@ -101,12 +102,12 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
describe('change properties', () => { describe('change properties', () => {
[{ [{
name: 'inline', name: 'inline',
updater: (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, 'v1', 'v2') update: (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, 'v1', 'v2')
}, },
{ {
name: 'dynamic', name: 'dynamic',
updater: (updater: NodeUpdater, view: ViewData) => update: (updater: NodeUpdater, view: ViewData) =>
updater.checkDynamic(view, 0, ['v1', 'v2']) updater.checkDynamic(view, 0, ['v1', 'v2'])
}].forEach((config) => { }].forEach((config) => {
it(`should update ${config.name}`, () => { it(`should update ${config.name}`, () => {
@ -119,7 +120,7 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
[BindingType.ElementProperty, 'value', SecurityContext.NONE] [BindingType.ElementProperty, 'value', SecurityContext.NONE]
]), ]),
], ],
config.updater)); config.update));
checkAndUpdateView(view); checkAndUpdateView(view);
@ -133,12 +134,12 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
describe('change attributes', () => { describe('change attributes', () => {
[{ [{
name: 'inline', name: 'inline',
updater: (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, 'v1', 'v2') update: (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, 'v1', 'v2')
}, },
{ {
name: 'dynamic', name: 'dynamic',
updater: (updater: NodeUpdater, view: ViewData) => update: (updater: NodeUpdater, view: ViewData) =>
updater.checkDynamic(view, 0, ['v1', 'v2']) updater.checkDynamic(view, 0, ['v1', 'v2'])
}].forEach((config) => { }].forEach((config) => {
it(`should update ${config.name}`, () => { it(`should update ${config.name}`, () => {
const {view, rootNodes} = createAndGetRootNodes(compViewDef( const {view, rootNodes} = createAndGetRootNodes(compViewDef(
@ -150,7 +151,7 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
[BindingType.ElementAttribute, 'a2', SecurityContext.NONE] [BindingType.ElementAttribute, 'a2', SecurityContext.NONE]
]), ]),
], ],
config.updater)); config.update));
checkAndUpdateView(view); checkAndUpdateView(view);
@ -192,12 +193,12 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
describe('change styles', () => { describe('change styles', () => {
[{ [{
name: 'inline', name: 'inline',
updater: (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, 10, 'red') update: (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, 10, 'red')
}, },
{ {
name: 'dynamic', name: 'dynamic',
updater: (updater: NodeUpdater, view: ViewData) => update: (updater: NodeUpdater, view: ViewData) =>
updater.checkDynamic(view, 0, [10, 'red']) updater.checkDynamic(view, 0, [10, 'red'])
}].forEach((config) => { }].forEach((config) => {
it(`should update ${config.name}`, () => { it(`should update ${config.name}`, () => {
const {view, rootNodes} = createAndGetRootNodes(compViewDef( const {view, rootNodes} = createAndGetRootNodes(compViewDef(
@ -209,7 +210,7 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
[BindingType.ElementStyle, 'color', null] [BindingType.ElementStyle, 'color', null]
]), ]),
], ],
config.updater)); config.update));
checkAndUpdateView(view); checkAndUpdateView(view);
@ -219,5 +220,131 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
}); });
}); });
}); });
if (getDOM().supportsDOMEvents()) {
describe('listen to DOM events', () => {
let removeNodes: Node[];
beforeEach(() => { removeNodes = []; });
afterEach(() => {
removeNodes.forEach((node) => {
if (node.parentNode) {
node.parentNode.removeChild(node);
}
});
});
function createAndAttachAndGetRootNodes(viewDef: ViewDefinition):
{rootNodes: any[], view: ViewData} {
const result = createAndGetRootNodes(viewDef);
// Note: We need to append the node to the document.body, otherwise `click` events
// won't work in IE.
result.rootNodes.forEach((node) => {
document.body.appendChild(node);
removeNodes.push(node);
});
return result;
}
it('should listen to DOM events', () => {
const handleEventSpy = jasmine.createSpy('handleEvent');
const removeListenerSpy =
spyOn(HTMLElement.prototype, 'removeEventListener').and.callThrough();
const {view, rootNodes} = createAndAttachAndGetRootNodes(compViewDef(
[elementDef(NodeFlags.None, 0, 'button', null, null, ['click'])], null,
handleEventSpy));
rootNodes[0].click();
expect(handleEventSpy).toHaveBeenCalled();
let handleEventArgs = handleEventSpy.calls.mostRecent().args;
expect(handleEventArgs[0]).toBe(view);
expect(handleEventArgs[1]).toBe(0);
expect(handleEventArgs[2]).toBe('click');
expect(handleEventArgs[3]).toBeTruthy();
destroyView(view);
expect(removeListenerSpy).toHaveBeenCalled();
});
it('should listen to window events', () => {
const handleEventSpy = jasmine.createSpy('handleEvent');
const addListenerSpy = spyOn(window, 'addEventListener');
const removeListenerSpy = spyOn(window, 'removeEventListener');
const {view, rootNodes} = createAndAttachAndGetRootNodes(compViewDef(
[elementDef(NodeFlags.None, 0, 'button', null, null, [['window', 'windowClick']])],
null, handleEventSpy));
expect(addListenerSpy).toHaveBeenCalled();
expect(addListenerSpy.calls.mostRecent().args[0]).toBe('windowClick');
addListenerSpy.calls.mostRecent().args[1]({name: 'windowClick'});
expect(handleEventSpy).toHaveBeenCalled();
const handleEventArgs = handleEventSpy.calls.mostRecent().args;
expect(handleEventArgs[0]).toBe(view);
expect(handleEventArgs[1]).toBe(0);
expect(handleEventArgs[2]).toBe('windowClick');
expect(handleEventArgs[3]).toBeTruthy();
destroyView(view);
expect(removeListenerSpy).toHaveBeenCalled();
});
it('should listen to document events', () => {
const handleEventSpy = jasmine.createSpy('handleEvent');
const addListenerSpy = spyOn(document, 'addEventListener');
const removeListenerSpy = spyOn(document, 'removeEventListener');
const {view, rootNodes} = createAndAttachAndGetRootNodes(compViewDef(
[elementDef(
NodeFlags.None, 0, 'button', null, null, [['document', 'documentClick']])],
null, handleEventSpy));
expect(addListenerSpy).toHaveBeenCalled();
expect(addListenerSpy.calls.mostRecent().args[0]).toBe('documentClick');
addListenerSpy.calls.mostRecent().args[1]({name: 'documentClick'});
expect(handleEventSpy).toHaveBeenCalled();
const handleEventArgs = handleEventSpy.calls.mostRecent().args;
expect(handleEventArgs[0]).toBe(view);
expect(handleEventArgs[1]).toBe(0);
expect(handleEventArgs[2]).toBe('documentClick');
expect(handleEventArgs[3]).toBeTruthy();
destroyView(view);
expect(removeListenerSpy).toHaveBeenCalled();
});
it('should preventDefault only if the handler returns false', () => {
let eventHandlerResult: any;
let preventDefaultSpy: jasmine.Spy;
const {view, rootNodes} = createAndAttachAndGetRootNodes(compViewDef(
[elementDef(NodeFlags.None, 0, 'button', null, null, ['click'])], null,
(view, index, eventName, event) => {
preventDefaultSpy = spyOn(event, 'preventDefault').and.callThrough();
return eventHandlerResult;
}));
eventHandlerResult = undefined;
rootNodes[0].click();
expect(preventDefaultSpy).not.toHaveBeenCalled();
eventHandlerResult = true;
rootNodes[0].click();
expect(preventDefaultSpy).not.toHaveBeenCalled();
eventHandlerResult = 'someString';
rootNodes[0].click();
expect(preventDefaultSpy).not.toHaveBeenCalled();
eventHandlerResult = false;
rootNodes[0].click();
expect(preventDefaultSpy).toHaveBeenCalled();
});
});
}
}); });
} }

View File

@ -7,7 +7,7 @@
*/ */
import {RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation} from '@angular/core'; 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 {BindingType, DefaultServices, NodeDef, NodeFlags, NodeUpdater, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, 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 {inject} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
@ -34,12 +34,13 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
new RenderComponentType('1', 'someUrl', 0, ViewEncapsulation.None, [], {}); new RenderComponentType('1', 'someUrl', 0, ViewEncapsulation.None, [], {});
})); }));
function compViewDef(nodes: NodeDef[], updater?: ViewUpdateFn): ViewDefinition { function compViewDef(
return viewDef(config.viewFlags, nodes, updater, renderComponentType); nodes: NodeDef[], update?: ViewUpdateFn, handleEvent?: ViewHandleEventFn): ViewDefinition {
return viewDef(config.viewFlags, nodes, update, handleEvent, renderComponentType);
} }
function embeddedViewDef(nodes: NodeDef[], updater?: ViewUpdateFn): ViewDefinition { function embeddedViewDef(nodes: NodeDef[], update?: ViewUpdateFn): ViewDefinition {
return viewDef(config.viewFlags, nodes, updater); return viewDef(config.viewFlags, nodes, update);
} }
function createAndGetRootNodes( function createAndGetRootNodes(
@ -49,6 +50,23 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
return {rootNodes, view}; return {rootNodes, view};
} }
it('should create embedded views with the right context', () => {
const parentContext = new Object();
const childContext = new Object();
const {view: parentView, rootNodes} = createAndGetRootNodes(
compViewDef([
elementDef(NodeFlags.None, 2, 'div'),
anchorDef(NodeFlags.HasEmbeddedViews, 0, embeddedViewDef([elementDef(
NodeFlags.None, 0, 'span')])),
]),
parentContext);
const childView = createEmbeddedView(parentView, parentView.def.nodes[1], childContext);
expect(childView.component).toBe(parentContext);
expect(childView.context).toBe(childContext);
});
it('should attach and detach embedded views', () => { it('should attach and detach embedded views', () => {
const {view: parentView, rootNodes} = createAndGetRootNodes(compViewDef([ const {view: parentView, rootNodes} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, 2, 'div'), elementDef(NodeFlags.None, 2, 'div'),
@ -97,45 +115,35 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
it('should dirty check embedded views', () => { it('should dirty check embedded views', () => {
let childValue = 'v1'; let childValue = 'v1';
const parentContext = new Object(); const update = jasmine.createSpy('updater').and.callFake(
const childContext = new Object();
const updater = jasmine.createSpy('updater').and.callFake(
(updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, childValue)); (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, childValue));
const {view: parentView, rootNodes} = createAndGetRootNodes( const {view: parentView, rootNodes} = createAndGetRootNodes(compViewDef([
compViewDef([ elementDef(NodeFlags.None, 1, 'div'),
elementDef(NodeFlags.None, 1, 'div'), anchorDef(
anchorDef( NodeFlags.HasEmbeddedViews, 0,
NodeFlags.HasEmbeddedViews, 0, embeddedViewDef(
embeddedViewDef( [elementDef(
[elementDef( NodeFlags.None, 0, 'span', null,
NodeFlags.None, 0, 'span', null, [[BindingType.ElementAttribute, 'name', SecurityContext.NONE]])],
[[BindingType.ElementAttribute, 'name', SecurityContext.NONE]])], update))
updater)) ]));
]),
parentContext);
const childView0 = createEmbeddedView(parentView, parentView.def.nodes[1], childContext); const childView0 = createEmbeddedView(parentView, parentView.def.nodes[1]);
const rootEl = rootNodes[0]; const rootEl = rootNodes[0];
attachEmbeddedView(parentView.nodes[1], 0, childView0); attachEmbeddedView(parentView.nodes[1], 0, childView0);
checkAndUpdateView(parentView); checkAndUpdateView(parentView);
expect(updater).toHaveBeenCalled(); expect(update).toHaveBeenCalled();
// component expect(update.calls.mostRecent().args[1]).toBe(childView0);
expect(updater.calls.mostRecent().args[2]).toBe(parentContext);
// view context
expect(updater.calls.mostRecent().args[3]).toBe(childContext);
updater.calls.reset(); update.calls.reset();
checkNoChangesView(parentView); checkNoChangesView(parentView);
expect(updater).toHaveBeenCalled(); expect(update).toHaveBeenCalled();
// component expect(update.calls.mostRecent().args[1]).toBe(childView0);
expect(updater.calls.mostRecent().args[2]).toBe(parentContext);
// view context
expect(updater.calls.mostRecent().args[3]).toBe(childContext);
childValue = 'v2'; childValue = 'v2';
expect(() => checkNoChangesView(parentView)) expect(() => checkNoChangesView(parentView))

View File

@ -6,8 +6,8 @@
* found in the LICENSE file at https://angular.io/license * 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 {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, DoCheck, ElementRef, EventEmitter, 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 {BindingType, DefaultServices, NodeDef, NodeFlags, NodeUpdater, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, checkAndUpdateView, checkNoChangesView, createRootView, destroyView, elementDef, providerDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index';
import {inject} from '@angular/core/testing'; import {inject} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
@ -34,12 +34,13 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
new RenderComponentType('1', 'someUrl', 0, ViewEncapsulation.None, [], {}); new RenderComponentType('1', 'someUrl', 0, ViewEncapsulation.None, [], {});
})); }));
function compViewDef(nodes: NodeDef[], updater?: ViewUpdateFn): ViewDefinition { function compViewDef(
return viewDef(config.viewFlags, nodes, updater, renderComponentType); nodes: NodeDef[], update?: ViewUpdateFn, handleEvent?: ViewHandleEventFn): ViewDefinition {
return viewDef(config.viewFlags, nodes, update, handleEvent, renderComponentType);
} }
function embeddedViewDef(nodes: NodeDef[], updater?: ViewUpdateFn): ViewDefinition { function embeddedViewDef(nodes: NodeDef[], update?: ViewUpdateFn): ViewDefinition {
return viewDef(config.viewFlags, nodes, updater); return viewDef(config.viewFlags, nodes, update);
} }
function createAndGetRootNodes(viewDef: ViewDefinition): {rootNodes: any[], view: ViewData} { function createAndGetRootNodes(viewDef: ViewDefinition): {rootNodes: any[], view: ViewData} {
@ -110,10 +111,11 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
createAndGetRootNodes(compViewDef([ createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, 1, 'div'), elementDef(NodeFlags.None, 1, 'div'),
providerDef( providerDef(
NodeFlags.None, Dep, [], null, () => compViewDef([ NodeFlags.None, Dep, [], null, null,
elementDef(NodeFlags.None, 1, 'span'), () => compViewDef([
providerDef(NodeFlags.None, SomeService, [Dep]) elementDef(NodeFlags.None, 1, 'span'),
])), providerDef(NodeFlags.None, SomeService, [Dep])
])),
])); ]));
expect(instance.dep instanceof Dep).toBeTruthy(); expect(instance.dep instanceof Dep).toBeTruthy();
@ -173,12 +175,12 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
describe('data binding', () => { describe('data binding', () => {
[{ [{
name: 'inline', name: 'inline',
updater: (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 1, 'v1', 'v2') update: (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 1, 'v1', 'v2')
}, },
{ {
name: 'dynamic', name: 'dynamic',
updater: (updater: NodeUpdater, view: ViewData) => update: (updater: NodeUpdater, view: ViewData) =>
updater.checkDynamic(view, 1, ['v1', 'v2']) updater.checkDynamic(view, 1, ['v1', 'v2'])
}].forEach((config) => { }].forEach((config) => {
it(`should update ${config.name}`, () => { it(`should update ${config.name}`, () => {
let instance: SomeService; let instance: SomeService;
@ -194,7 +196,7 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
elementDef(NodeFlags.None, 1, 'span'), elementDef(NodeFlags.None, 1, 'span'),
providerDef(NodeFlags.None, SomeService, [], {a: [0, 'a'], b: [1, 'b']}) providerDef(NodeFlags.None, SomeService, [], {a: [0, 'a'], b: [1, 'b']})
], ],
config.updater)); config.update));
checkAndUpdateView(view); checkAndUpdateView(view);
@ -226,6 +228,39 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
}); });
}); });
describe('outputs', () => {
it('should listen to provider events', () => {
let emitter = new EventEmitter<any>();
let unsubscribeSpy: any;
class SomeService {
emitter = {
subscribe: (callback: any) => {
const subscription = emitter.subscribe(callback);
unsubscribeSpy = spyOn(subscription, 'unsubscribe').and.callThrough();
return subscription;
}
};
}
const handleEvent = jasmine.createSpy('handleEvent');
const subscribe = spyOn(emitter, 'subscribe').and.callThrough();
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(NodeFlags.None, 1, 'span'),
providerDef(NodeFlags.None, SomeService, [], null, {emitter: 'someEventName'})
],
null, handleEvent));
emitter.emit('someEventInstance');
expect(handleEvent).toHaveBeenCalledWith(view, 0, 'someEventName', 'someEventInstance');
destroyView(view);
expect(unsubscribeSpy).toHaveBeenCalled();
});
});
describe('lifecycle hooks', () => { describe('lifecycle hooks', () => {
it('should call the lifecycle hooks in the right order', () => { it('should call the lifecycle hooks in the right order', () => {
let instanceCount = 0; let instanceCount = 0;

View File

@ -7,7 +7,7 @@
*/ */
import {RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation} from '@angular/core'; 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 {DefaultServices, NodeDef, NodeFlags, NodeUpdater, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, checkAndUpdateView, checkNoChangesView, createRootView, elementDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index';
import {inject} from '@angular/core/testing'; import {inject} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
@ -34,8 +34,9 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
new RenderComponentType('1', 'someUrl', 0, ViewEncapsulation.None, [], {}); new RenderComponentType('1', 'someUrl', 0, ViewEncapsulation.None, [], {});
})); }));
function compViewDef(nodes: NodeDef[], updater?: ViewUpdateFn): ViewDefinition { function compViewDef(
return viewDef(config.viewFlags, nodes, updater, renderComponentType); nodes: NodeDef[], update?: ViewUpdateFn, handleEvent?: ViewHandleEventFn): ViewDefinition {
return viewDef(config.viewFlags, nodes, update, handleEvent, renderComponentType);
} }
function createAndGetRootNodes(viewDef: ViewDefinition): {rootNodes: any[], view: ViewData} { function createAndGetRootNodes(viewDef: ViewDefinition): {rootNodes: any[], view: ViewData} {
@ -88,19 +89,19 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
describe('change text', () => { describe('change text', () => {
[{ [{
name: 'inline', name: 'inline',
updater: (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, 'a', 'b') update: (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, 'a', 'b')
}, },
{ {
name: 'dynamic', name: 'dynamic',
updater: (updater: NodeUpdater, view: ViewData) => update: (updater: NodeUpdater, view: ViewData) =>
updater.checkDynamic(view, 0, ['a', 'b']) updater.checkDynamic(view, 0, ['a', 'b'])
}].forEach((config) => { }].forEach((config) => {
it(`should update ${config.name}`, () => { it(`should update ${config.name}`, () => {
const {view, rootNodes} = createAndGetRootNodes(compViewDef( const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[ [
textDef(['0', '1', '2']), textDef(['0', '1', '2']),
], ],
config.updater)); config.update));
checkAndUpdateView(view); checkAndUpdateView(view);

View File

@ -25,16 +25,18 @@ let viewFlags = ViewFlags.DirectDom;
const TreeComponent_Host: ViewDefinition = viewDef(viewFlags, [ const TreeComponent_Host: ViewDefinition = viewDef(viewFlags, [
elementDef(NodeFlags.None, 1, 'tree'), elementDef(NodeFlags.None, 1, 'tree'),
providerDef(NodeFlags.None, TreeComponent, [], null, () => TreeComponent_0), providerDef(NodeFlags.None, TreeComponent, [], null, null, () => TreeComponent_0),
]); ]);
const TreeComponent_1: ViewDefinition = viewDef( const TreeComponent_1: ViewDefinition = viewDef(
viewFlags, viewFlags,
[ [
elementDef(NodeFlags.None, 1, 'tree'), elementDef(NodeFlags.None, 1, 'tree'),
providerDef(NodeFlags.None, TreeComponent, [], {data: [0, 'data']}, () => TreeComponent_0), providerDef(
NodeFlags.None, TreeComponent, [], {data: [0, 'data']}, null, () => TreeComponent_0),
], ],
(updater: NodeUpdater, view: ViewData, cmp: TreeComponent) => { (updater: NodeUpdater, view: ViewData) => {
const cmp = view.component;
updater.checkInline(view, 1, cmp.data.left); updater.checkInline(view, 1, cmp.data.left);
}); });
@ -42,9 +44,11 @@ const TreeComponent_2: ViewDefinition = viewDef(
viewFlags, viewFlags,
[ [
elementDef(NodeFlags.None, 1, 'tree'), elementDef(NodeFlags.None, 1, 'tree'),
providerDef(NodeFlags.None, TreeComponent, [], {data: [0, 'data']}, () => TreeComponent_0), providerDef(
NodeFlags.None, TreeComponent, [], {data: [0, 'data']}, null, () => TreeComponent_0),
], ],
(updater: NodeUpdater, view: ViewData, cmp: TreeComponent) => { (updater: NodeUpdater, view: ViewData) => {
const cmp = view.component;
updater.checkInline(view, 1, cmp.data.right); updater.checkInline(view, 1, cmp.data.right);
}); });
@ -59,7 +63,8 @@ const TreeComponent_0: ViewDefinition = viewDef(
anchorDef(NodeFlags.HasEmbeddedViews, 1, TreeComponent_2), anchorDef(NodeFlags.HasEmbeddedViews, 1, TreeComponent_2),
providerDef(NodeFlags.None, NgIf, [ViewContainerRef, TemplateRef], {ngIf: [0, 'ngIf']}), providerDef(NodeFlags.None, NgIf, [ViewContainerRef, TemplateRef], {ngIf: [0, 'ngIf']}),
], ],
(updater: NodeUpdater, view: ViewData, cmp: TreeComponent) => { (updater: NodeUpdater, view: ViewData) => {
const cmp = view.component;
updater.checkInline(view, 0, cmp.bgColor); updater.checkInline(view, 0, cmp.bgColor);
updater.checkInline(view, 1, cmp.data.value); updater.checkInline(view, 1, cmp.data.value);
updater.checkInline(view, 3, cmp.data.left != null); updater.checkInline(view, 3, cmp.data.left != null);