refactor(render): user render compiler

This commit is contained in:
Tobias Bosch
2015-04-02 14:40:49 -07:00
parent 069bbf3ed0
commit 1d4d18d9db
72 changed files with 1052 additions and 4739 deletions

View File

@ -1,4 +1,5 @@
import {AST} from 'angular2/change_detection';
import {SetterFn} from 'angular2/src/reflection/types';
import {isPresent, isBlank, BaseException} from 'angular2/src/facade/lang';
import {List, ListWrapper} from 'angular2/src/facade/collection';
import * as protoViewModule from './proto_view';
@ -15,6 +16,7 @@ export class ElementBinder {
componentId: string;
parentIndex:number;
distanceToParent:number;
propertySetters: Map<string, SetterFn>;
constructor({
textNodeIndices,
@ -24,7 +26,8 @@ export class ElementBinder {
eventLocals,
eventNames,
parentIndex,
distanceToParent
distanceToParent,
propertySetters
}) {
this.textNodeIndices = textNodeIndices;
this.contentTagSelector = contentTagSelector;
@ -34,6 +37,7 @@ export class ElementBinder {
this.eventNames = eventNames;
this.parentIndex = parentIndex;
this.distanceToParent = distanceToParent;
this.propertySetters = propertySetters;
}
mergeChildComponentProtoViews(protoViews:List<protoViewModule.ProtoView>, target:List<protoViewModule.ProtoView>):ElementBinder {
@ -45,15 +49,14 @@ export class ElementBinder {
}
return new ElementBinder({
parentIndex: this.parentIndex,
// Don't clone as we assume immutability!
textNodeIndices: this.textNodeIndices,
contentTagSelector: this.contentTagSelector,
nestedProtoView: nestedProtoView,
componentId: this.componentId,
// Don't clone as we assume immutability!
eventLocals: this.eventLocals,
eventNames: this.eventNames,
distanceToParent: this.distanceToParent
distanceToParent: this.distanceToParent,
propertySetters: this.propertySetters
});
}
}

View File

@ -0,0 +1,125 @@
import {StringWrapper, RegExpWrapper, BaseException, isPresent, isBlank, isString, stringify} from 'angular2/src/facade/lang';
import {ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {camelCaseToDashCase, dashCaseToCamelCase} from '../util';
import {reflector} from 'angular2/src/reflection/reflection';
const STYLE_SEPARATOR = '.';
var propertySettersCache = StringMapWrapper.create();
var innerHTMLSetterCache;
export function setterFactory(property: string): Function {
var setterFn, styleParts, styleSuffix;
if (StringWrapper.startsWith(property, ATTRIBUTE_PREFIX)) {
setterFn = attributeSetterFactory(StringWrapper.substring(property, ATTRIBUTE_PREFIX.length));
} else if (StringWrapper.startsWith(property, CLASS_PREFIX)) {
setterFn = classSetterFactory(StringWrapper.substring(property, CLASS_PREFIX.length));
} else if (StringWrapper.startsWith(property, STYLE_PREFIX)) {
styleParts = property.split(STYLE_SEPARATOR);
styleSuffix = styleParts.length > 2 ? ListWrapper.get(styleParts, 2) : '';
setterFn = styleSetterFactory(ListWrapper.get(styleParts, 1), styleSuffix);
} else if (StringWrapper.equals(property, 'innerHtml')) {
if (isBlank(innerHTMLSetterCache)) {
innerHTMLSetterCache = (el, value) => DOM.setInnerHTML(el, value);
}
setterFn = innerHTMLSetterCache;
} else {
property = resolvePropertyName(property);
setterFn = StringMapWrapper.get(propertySettersCache, property);
if (isBlank(setterFn)) {
var propertySetterFn = reflector.setter(property);
setterFn = function(receiver, value) {
if (DOM.hasProperty(receiver, property)) {
return propertySetterFn(receiver, value);
}
};
StringMapWrapper.set(propertySettersCache, property, setterFn);
}
}
return setterFn;
}
const ATTRIBUTE_PREFIX = 'attr.';
var attributeSettersCache = StringMapWrapper.create();
function _isValidAttributeValue(attrName:string, value: any): boolean {
if (attrName == "role") {
return isString(value);
} else {
return isPresent(value);
}
}
function attributeSetterFactory(attrName:string): Function {
var setterFn = StringMapWrapper.get(attributeSettersCache, attrName);
var dashCasedAttributeName;
if (isBlank(setterFn)) {
dashCasedAttributeName = camelCaseToDashCase(attrName);
setterFn = function(element, value) {
if (_isValidAttributeValue(dashCasedAttributeName, value)) {
DOM.setAttribute(element, dashCasedAttributeName, stringify(value));
} else {
if (isPresent(value)) {
throw new BaseException("Invalid " + dashCasedAttributeName +
" attribute, only string values are allowed, got '" + stringify(value) + "'");
}
DOM.removeAttribute(element, dashCasedAttributeName);
}
};
StringMapWrapper.set(attributeSettersCache, attrName, setterFn);
}
return setterFn;
}
const CLASS_PREFIX = 'class.';
var classSettersCache = StringMapWrapper.create();
function classSetterFactory(className:string): Function {
var setterFn = StringMapWrapper.get(classSettersCache, className);
var dashCasedClassName;
if (isBlank(setterFn)) {
dashCasedClassName = camelCaseToDashCase(className);
setterFn = function(element, value) {
if (value) {
DOM.addClass(element, dashCasedClassName);
} else {
DOM.removeClass(element, dashCasedClassName);
}
};
StringMapWrapper.set(classSettersCache, className, setterFn);
}
return setterFn;
}
const STYLE_PREFIX = 'style.';
var styleSettersCache = StringMapWrapper.create();
function styleSetterFactory(styleName:string, styleSuffix:string): Function {
var cacheKey = styleName + styleSuffix;
var setterFn = StringMapWrapper.get(styleSettersCache, cacheKey);
var dashCasedStyleName;
if (isBlank(setterFn)) {
dashCasedStyleName = camelCaseToDashCase(styleName);
setterFn = function(element, value) {
var valAsStr;
if (isPresent(value)) {
valAsStr = stringify(value);
DOM.setStyle(element, dashCasedStyleName, valAsStr + styleSuffix);
} else {
DOM.removeStyle(element, dashCasedStyleName);
}
};
StringMapWrapper.set(styleSettersCache, cacheKey, setterFn);
}
return setterFn;
}
function resolvePropertyName(attrName:string): string {
var mappedPropName = StringMapWrapper.get(DOM.attrToPropMap, attrName);
return isPresent(mappedPropName) ? mappedPropName : attrName;
}

View File

@ -1,6 +1,5 @@
import {isPresent} from 'angular2/src/facade/lang';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {SetterFn} from 'angular2/src/reflection/types';
import {List, Map, ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
@ -16,20 +15,17 @@ export class ProtoView {
isTemplateElement:boolean;
isRootView:boolean;
rootBindingOffset:int;
propertySetters: Map<string, SetterFn>;
constructor({
elementBinders,
element,
isRootView,
propertySetters
isRootView
}) {
this.element = element;
this.elementBinders = elementBinders;
this.isTemplateElement = DOM.isTemplateElement(this.element);
this.isRootView = isRootView;
this.rootBindingOffset = (isPresent(this.element) && DOM.hasClass(this.element, NG_BINDING_CLASS)) ? 1 : 0;
this.propertySetters = propertySetters;
}
mergeChildComponentProtoViews(protoViews:List<ProtoView>, target:List<ProtoView>):ProtoView {
@ -45,9 +41,7 @@ export class ProtoView {
var result = new ProtoView({
elementBinders: elementBinders,
element: this.element,
isRootView: this.isRootView,
// Don't clone as we assume immutability!
propertySetters: this.propertySetters
isRootView: this.isRootView
});
ListWrapper.insert(target, 0, result);
return result

View File

@ -1,4 +1,4 @@
import {isPresent, BaseException} from 'angular2/src/facade/lang';
import {isPresent, isBlank, BaseException} from 'angular2/src/facade/lang';
import {ListWrapper, MapWrapper, Set, SetWrapper} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/dom/dom_adapter';
@ -9,6 +9,7 @@ import {SetterFn} from 'angular2/src/reflection/types';
import {ProtoView} from './proto_view';
import {ElementBinder} from './element_binder';
import {setterFactory} from './property_setter_factory';
import * as api from '../../api';
import * as directDomRenderer from '../direct_dom_renderer';
@ -20,14 +21,12 @@ export class ProtoViewBuilder {
variableBindings: Map<string, string>;
elements:List<ElementBinderBuilder>;
isRootView:boolean;
propertySetters:Set<string>;
constructor(rootElement) {
this.rootElement = rootElement;
this.elements = [];
this.isRootView = false;
this.variableBindings = MapWrapper.create();
this.propertySetters = new Set();
}
bindElement(element, description = null):ElementBinderBuilder {
@ -55,13 +54,10 @@ export class ProtoViewBuilder {
var renderElementBinders = [];
var apiElementBinders = [];
var propertySetters = MapWrapper.create();
ListWrapper.forEach(this.elements, (ebb) => {
var propertySetters = MapWrapper.create();
var eventLocalsAstSplitter = new EventLocalsAstSplitter();
var apiDirectiveBinders = ListWrapper.map(ebb.directives, (db) => {
MapWrapper.forEach(db.propertySetters, (setter, propertyName) => {
MapWrapper.set(propertySetters, propertyName, setter);
});
return new api.DirectiveBinder({
directiveIndex: db.directiveIndex,
propertyBindings: db.propertyBindings,
@ -74,15 +70,14 @@ export class ProtoViewBuilder {
var nestedProtoView =
isPresent(ebb.nestedProtoView) ? ebb.nestedProtoView.build() : null;
var parentIndex = isPresent(ebb.parent) ? ebb.parent.index : -1;
var parentWithDirectivesIndex = isPresent(ebb.parentWithDirectives) ? ebb.parentWithDirectives.index : -1;
ListWrapper.push(apiElementBinders, new api.ElementBinder({
index: ebb.index, parentIndex:parentIndex, distanceToParent:ebb.distanceToParent,
parentWithDirectivesIndex: parentWithDirectivesIndex, distanceToParentWithDirectives: ebb.distanceToParentWithDirectives,
directives: apiDirectiveBinders,
nestedProtoView: nestedProtoView,
propertyBindings: ebb.propertyBindings, variableBindings: ebb.variableBindings,
eventBindings: eventLocalsAstSplitter.splitEventAstIntoLocals(ebb.eventBindings),
textBindings: ebb.textBindings
textBindings: ebb.textBindings,
readAttributes: ebb.readAttributes
}));
ListWrapper.push(renderElementBinders, new ElementBinder({
textNodeIndices: ebb.textBindingIndices,
@ -92,15 +87,15 @@ export class ProtoViewBuilder {
nestedProtoView: isPresent(nestedProtoView) ? nestedProtoView.render.delegate : null,
componentId: ebb.componentId,
eventLocals: eventLocalsAstSplitter.buildEventLocals(),
eventNames: eventLocalsAstSplitter.buildEventNames()
eventNames: eventLocalsAstSplitter.buildEventNames(),
propertySetters: propertySetters
}));
});
return new api.ProtoView({
render: new directDomRenderer.DirectDomProtoViewRef(new ProtoView({
element: this.rootElement,
elementBinders: renderElementBinders,
isRootView: this.isRootView,
propertySetters: propertySetters
isRootView: this.isRootView
})),
elementBinders: apiElementBinders,
variableBindings: this.variableBindings
@ -113,8 +108,6 @@ export class ElementBinderBuilder {
index:number;
parent:ElementBinderBuilder;
distanceToParent:number;
parentWithDirectives:ElementBinderBuilder;
distanceToParentWithDirectives:number;
directives:List<DirectiveBuilder>;
nestedProtoView:ProtoViewBuilder;
propertyBindings: Map<string, ASTWithSource>;
@ -124,6 +117,7 @@ export class ElementBinderBuilder {
textBindings: List<ASTWithSource>;
contentTagSelector:string;
propertySetters: Map<string, SetterFn>;
readAttributes: Map<string, string>;
componentId: string;
constructor(index, element, description) {
@ -131,8 +125,6 @@ export class ElementBinderBuilder {
this.index = index;
this.parent = null;
this.distanceToParent = 0;
this.parentWithDirectives = null;
this.distanceToParentWithDirectives = 0;
this.directives = [];
this.nestedProtoView = null;
this.propertyBindings = MapWrapper.create();
@ -143,25 +135,23 @@ export class ElementBinderBuilder {
this.contentTagSelector = null;
this.propertySetters = MapWrapper.create();
this.componentId = null;
this.readAttributes = MapWrapper.create();
}
setParent(parent:ElementBinderBuilder, distanceToParent):ElementBinderBuilder {
this.parent = parent;
if (isPresent(parent)) {
this.distanceToParent = distanceToParent;
if (parent.directives.length > 0) {
this.parentWithDirectives = parent;
this.distanceToParentWithDirectives = distanceToParent;
} else {
this.parentWithDirectives = parent.parentWithDirectives;
if (isPresent(this.parentWithDirectives)) {
this.distanceToParentWithDirectives = distanceToParent + parent.distanceToParentWithDirectives;
}
}
}
return this;
}
readAttribute(attrName:string) {
if (isBlank(MapWrapper.get(this.readAttributes, attrName))) {
MapWrapper.set(this.readAttributes, attrName, DOM.getAttribute(this.element, attrName));
}
}
bindDirective(directiveIndex:number):DirectiveBuilder {
var directive = new DirectiveBuilder(directiveIndex);
ListWrapper.push(this.directives, directive);
@ -178,6 +168,11 @@ export class ElementBinderBuilder {
bindProperty(name, expression) {
MapWrapper.set(this.propertyBindings, name, expression);
this.bindPropertySetter(name);
}
bindPropertySetter(name) {
MapWrapper.set(this.propertySetters, name, setterFactory(name));
}
bindVariable(name, value) {
@ -209,10 +204,6 @@ export class ElementBinderBuilder {
this.contentTagSelector = value;
}
bindPropertySetter(propertyName, setter) {
MapWrapper.set(this.propertySetters, propertyName, setter);
}
setComponentId(componentId:string) {
this.componentId = componentId;
}
@ -222,13 +213,11 @@ export class DirectiveBuilder {
directiveIndex:number;
propertyBindings: Map<string, ASTWithSource>;
eventBindings: Map<string, ASTWithSource>;
propertySetters: Map<string, SetterFn>;
constructor(directiveIndex) {
this.directiveIndex = directiveIndex;
this.propertyBindings = MapWrapper.create();
this.eventBindings = MapWrapper.create();
this.propertySetters = MapWrapper.create();
}
bindProperty(name, expression) {
@ -238,10 +227,6 @@ export class DirectiveBuilder {
bindEvent(name, expression) {
MapWrapper.set(this.eventBindings, name, expression);
}
bindPropertySetter(propertyName, setter) {
MapWrapper.set(this.propertySetters, propertyName, setter);
}
}
export class EventLocalsAstSplitter extends AstTransformer {
@ -257,15 +242,19 @@ export class EventLocalsAstSplitter extends AstTransformer {
}
splitEventAstIntoLocals(eventBindings:Map<string, ASTWithSource>):Map<string, ASTWithSource> {
if (isPresent(eventBindings)) {
var result = MapWrapper.create();
MapWrapper.forEach(eventBindings, (astWithSource, eventName) => {
MapWrapper.set(result, eventName, astWithSource.ast.visit(this));
ListWrapper.push(this.eventNames, eventName);
});
return result;
}
return null;
// TODO(tbosch): reenable this when we are using
// the render views
return eventBindings;
// if (isPresent(eventBindings)) {
// var result = MapWrapper.create();
// MapWrapper.forEach(eventBindings, (astWithSource, eventName) => {
// var adjustedAst = astWithSource.ast.visit(this);
// MapWrapper.set(result, eventName, new ASTWithSource(adjustedAst, astWithSource.source, ''));
// ListWrapper.push(this.eventNames, eventName);
// });
// return result;
// }
// return null;
}
visitAccessMember(ast:AccessMember) {

View File

@ -52,7 +52,7 @@ export class View {
}
setElementProperty(elementIndex:number, propertyName:string, value:any) {
var setter = MapWrapper.get(this.proto.propertySetters, propertyName);
var setter = MapWrapper.get(this.proto.elementBinders[elementIndex].propertySetters, propertyName);
setter(this.boundElements[elementIndex], value);
}