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,
childFlags: undefined,
bindingIndex: undefined,
disposableIndex: undefined,
providerIndices: undefined,
// regular values
flags,
childCount,
bindings: [],
disposableCount: 0,
element: undefined,
provider: undefined,
text: undefined,

View File

@ -8,14 +8,16 @@
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';
export function elementDef(
flags: NodeFlags, childCount: number, name: string, fixedAttrs: {[name: string]: string} = {},
bindings: ([BindingType.ElementClass, string] | [BindingType.ElementStyle, string, string] | [
BindingType.ElementAttribute | BindingType.ElementProperty, string, SecurityContext
])[] = []): NodeDef {
bindings?:
([BindingType.ElementClass, string] | [BindingType.ElementStyle, string, string] |
[BindingType.ElementAttribute | BindingType.ElementProperty, string, SecurityContext])[],
outputs?: (string | [string, string])[]): NodeDef {
bindings = bindings || [];
const bindingDefs = new Array(bindings.length);
for (let i = 0; i < bindings.length; i++) {
const entry = bindings[i];
@ -35,6 +37,19 @@ export function elementDef(
}
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 {
type: NodeType.Element,
// will bet set by the view definition
@ -43,12 +58,14 @@ export function elementDef(
parent: undefined,
childFlags: undefined,
bindingIndex: undefined,
disposableIndex: undefined,
providerIndices: undefined,
// regular values
flags,
childCount,
bindings: bindingDefs,
element: {name, attrs: fixedAttrs},
disposableCount: outputDefs.length,
element: {name, attrs: fixedAttrs, outputs: outputDefs},
provider: undefined,
text: undefined,
component: undefined,
@ -62,22 +79,52 @@ export function createElement(view: ViewData, renderHost: any, def: NodeDef): No
let el: any;
if (view.renderer) {
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 {
el = document.createElement(elDef.name);
if (parentNode) {
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]);
}
}
}
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 {
renderNode: el,
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(
view: ViewData, def: NodeDef, v0: any, v1: any, v2: any, v3: any, v4: any, v5: any, v6: 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 {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';
const _tokenKeyCache = new Map<any, string>();
@ -26,7 +26,8 @@ const TemplateRefTokenKey = tokenKey(TemplateRef);
export function providerDef(
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[] = [];
if (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 => {
let token: any;
let flags: DepFlags;
@ -61,16 +68,14 @@ export function providerDef(
parent: undefined,
childFlags: undefined,
bindingIndex: undefined,
disposableIndex: undefined,
providerIndices: undefined,
// regular values
flags,
childCount: 0, bindings,
disposableCount: outputDefs.length,
element: undefined,
provider: {
tokenKey: tokenKey(ctor),
ctor,
deps: depDefs,
},
provider: {tokenKey: tokenKey(ctor), ctor, deps: depDefs, outputs: outputDefs},
text: undefined, component,
template: undefined
};
@ -87,10 +92,19 @@ export function tokenKey(token: any): string {
export function createProvider(view: ViewData, def: NodeDef, componentView: ViewData): NodeData {
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 {
renderNode: undefined,
provider: createInstance(view, def.parent, providerDef.ctor, providerDef.deps),
embeddedViews: undefined, componentView
provider,
embeddedViews: undefined, componentView,
};
}

View File

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

View File

@ -19,6 +19,7 @@ export interface ViewDefinition {
flags: ViewFlags;
componentType: RenderComponentType;
update: ViewUpdateFn;
handleEvent: ViewHandleEventFn;
/**
* Order: Depth first.
* Especially providers are before elements / anchros.
@ -33,10 +34,10 @@ export interface ViewDefinition {
reverseChildNodes: NodeDef[];
lastRootNode: number;
bindingCount: number;
disposableCount: number;
}
export type ViewUpdateFn = (updater: NodeUpdater, view: ViewData, component: any, context: any) =>
void;
export type ViewUpdateFn = (updater: NodeUpdater, view: ViewData) => void;
export interface NodeUpdater {
checkInline(
@ -45,6 +46,9 @@ export interface NodeUpdater {
checkDynamic(view: ViewData, nodeIndex: number, values: any[]): void;
}
export type ViewHandleEventFn =
(view: ViewData, nodeIndex: number, eventName: string, event: any) => boolean;
/**
* Bitmask for ViewDefintion.flags.
*/
@ -66,6 +70,8 @@ export interface NodeDef {
childFlags: NodeFlags;
bindingIndex: number;
bindings: BindingDef[];
disposableIndex: number;
disposableCount: number;
element: ElementDef;
providerIndices: {[tokenKey: string]: number};
provider: ProviderDef;
@ -102,6 +108,12 @@ export enum NodeFlags {
export interface ElementDef {
name: string;
attrs: {[name: string]: string};
outputs: ElementOutputDef[];
}
export interface ElementOutputDef {
target: string;
eventName: string;
}
/**
@ -118,10 +130,16 @@ export interface DepDef {
tokenKey: string;
}
export interface ProviderOutputDef {
propName: string;
eventName: string;
}
export interface ProviderDef {
tokenKey: string;
ctor: any;
deps: DepDef[];
outputs: ProviderOutputDef[];
}
export interface TextDef { prefix: string; }
@ -164,8 +182,11 @@ export interface ViewData {
nodes: NodeData[];
firstChange: boolean;
oldValues: any[];
disposables: DisposableFn[];
}
export type DisposableFn = () => void;
/**
* Node instance data.
* 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 {callLifecycleHooksChildrenFirst, checkAndUpdateProviderDynamic, checkAndUpdateProviderInline, createProvider} from './provider';
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';
const NOOP_UPDATE = (): any => undefined;
const NOOP = (): any => undefined;
export function viewDef(
flags: ViewFlags, nodesWithoutIndices: NodeDef[], update?: ViewUpdateFn,
componentType?: RenderComponentType): ViewDefinition {
handleEvent?: ViewHandleEventFn, componentType?: RenderComponentType): ViewDefinition {
// clone nodes and set auto calculated values
if (nodesWithoutIndices.length === 0) {
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 reverseChildNodes: NodeDef[] = new Array(nodesWithoutIndices.length);
let viewBindingCount = 0;
let viewDisposableCount = 0;
let viewFlags = 0;
let currentParent: NodeDef = null;
let lastRootNode: NodeDef = null;
@ -44,7 +45,8 @@ export function viewDef(
const node = cloneAndModifyNode(nodesWithoutIndices[i], {
index: i,
parent: currentParent ? currentParent.index : undefined,
bindingIndex: viewBindingCount, reverseChildIndex,
bindingIndex: viewBindingCount,
disposableIndex: viewDisposableCount, reverseChildIndex,
providerIndices: Object.create(currentParent ? currentParent.providerIndices : null)
});
nodes[i] = node;
@ -53,6 +55,7 @@ export function viewDef(
viewFlags |= node.flags;
viewBindingCount += node.bindings.length;
viewDisposableCount += node.disposableCount;
if (currentParent) {
currentParent.childFlags |= node.flags;
}
@ -72,8 +75,10 @@ export function viewDef(
nodeFlags: viewFlags,
flags,
nodes: nodes, reverseChildNodes,
update: update || NOOP_UPDATE, componentType,
update: update || NOOP,
handleEvent: handleEvent || NOOP, componentType,
bindingCount: viewBindingCount,
disposableCount: viewDisposableCount,
lastRootNode: lastRootNode.index
};
}
@ -148,6 +153,7 @@ function cloneAndModifyNode(nodeDef: NodeDef, values: {
reverseChildIndex: number,
parent: number,
bindingIndex: number,
disposableIndex: number,
providerIndices: {[tokenKey: string]: number}
}): NodeDef {
const clonedNode: NodeDef = <any>{};
@ -157,6 +163,7 @@ function cloneAndModifyNode(nodeDef: NodeDef, values: {
clonedNode.index = values.index;
clonedNode.bindingIndex = values.bindingIndex;
clonedNode.disposableIndex = values.disposableIndex;
clonedNode.parent = values.parent;
clonedNode.reverseChildIndex = values.reverseChildIndex;
clonedNode.providerIndices = values.providerIndices;
@ -188,6 +195,7 @@ function createView(
} else {
renderer = def.componentType ? services.renderComponent(def.componentType) : parent.renderer;
}
const disposables = def.disposableCount ? new Array(def.disposableCount) : undefined;
const view: ViewData = {
def,
parent,
@ -195,7 +203,7 @@ function createView(
context: undefined,
component: undefined, nodes,
firstChange: true, renderer, services,
oldValues: new Array(def.bindingCount)
oldValues: new Array(def.bindingCount), disposables
};
return view;
}
@ -232,7 +240,7 @@ function initView(view: ViewData, renderHost: any, component: any, context: any)
}
export function checkNoChangesView(view: ViewData) {
view.def.update(CheckNoChanges, view, view.component, view.context);
view.def.update(CheckNoChanges, view);
execEmbeddedViewsAction(view, ViewAction.CheckNoChanges);
execComponentViewsAction(view, ViewAction.CheckNoChanges);
}
@ -274,7 +282,7 @@ const CheckNoChanges: NodeUpdater = {
};
export function checkAndUpdateView(view: ViewData) {
view.def.update(CheckAndUpdate, view, view.component, view.context);
view.def.update(CheckAndUpdate, view);
execEmbeddedViewsAction(view, ViewAction.CheckAndUpdate);
callLifecycleHooksChildrenFirst(
@ -320,6 +328,11 @@ const CheckAndUpdate: NodeUpdater = {
export function destroyView(view: ViewData) {
callLifecycleHooksChildrenFirst(view, NodeFlags.OnDestroy);
if (view.disposables) {
for (let i = 0; i < view.disposables.length; i++) {
view.disposables[i]();
}
}
execComponentViewsAction(view, ViewAction.Destroy);
execEmbeddedViewsAction(view, ViewAction.Destroy);
}