feat(render): don’t use the reflector for setting properties

BREAKING CHANGES:
- host actions don't take an expression as value any more but only a method name,
  and assumes to get an array via the EventEmitter with the method arguments.
- Renderer.setElementProperty does not take `style.`/... prefixes any more.
  Use the new methods `Renderer.setElementAttribute`, ... instead

Part of #2476
Closes #2637
This commit is contained in:
Tobias Bosch
2015-06-18 15:44:44 -07:00
parent 2932377769
commit 0a51ccbd68
32 changed files with 643 additions and 568 deletions
@@ -5,13 +5,17 @@ import {DirectiveIndex, DirectiveRecord} from './directive_record';
const DIRECTIVE = "directive";
const DIRECTIVE_LIFECYCLE = "directiveLifecycle";
const ELEMENT = "element";
const ELEMENT_PROPERTY = "elementProperty";
const ELEMENT_ATTRIBUTE = "elementAttribute";
const ELEMENT_CLASS = "elementClass";
const ELEMENT_STYLE = "elementStyle";
const TEXT_NODE = "textNode";
export class BindingRecord {
constructor(public mode: string, public implicitReceiver: any, public ast: AST,
public elementIndex: number, public propertyName: string, public setter: SetterFn,
public lifecycleEvent: string, public directiveRecord: DirectiveRecord) {}
public elementIndex: number, public propertyName: string, public propertyUnit: string,
public setter: SetterFn, public lifecycleEvent: string,
public directiveRecord: DirectiveRecord) {}
callOnChange(): boolean {
return isPresent(this.directiveRecord) && this.directiveRecord.callOnChange;
@@ -25,41 +29,85 @@ export class BindingRecord {
isDirectiveLifecycle(): boolean { return this.mode === DIRECTIVE_LIFECYCLE; }
isElement(): boolean { return this.mode === ELEMENT; }
isElementProperty(): boolean { return this.mode === ELEMENT_PROPERTY; }
isElementAttribute(): boolean { return this.mode === ELEMENT_ATTRIBUTE; }
isElementClass(): boolean { return this.mode === ELEMENT_CLASS; }
isElementStyle(): boolean { return this.mode === ELEMENT_STYLE; }
isTextNode(): boolean { return this.mode === TEXT_NODE; }
static createForDirective(ast: AST, propertyName: string, setter: SetterFn,
directiveRecord: DirectiveRecord): BindingRecord {
return new BindingRecord(DIRECTIVE, 0, ast, 0, propertyName, setter, null, directiveRecord);
return new BindingRecord(DIRECTIVE, 0, ast, 0, propertyName, null, setter, null,
directiveRecord);
}
static createDirectiveOnCheck(directiveRecord: DirectiveRecord): BindingRecord {
return new BindingRecord(DIRECTIVE_LIFECYCLE, 0, null, 0, null, null, "onCheck",
return new BindingRecord(DIRECTIVE_LIFECYCLE, 0, null, 0, null, null, null, "onCheck",
directiveRecord);
}
static createDirectiveOnInit(directiveRecord: DirectiveRecord): BindingRecord {
return new BindingRecord(DIRECTIVE_LIFECYCLE, 0, null, 0, null, null, "onInit",
return new BindingRecord(DIRECTIVE_LIFECYCLE, 0, null, 0, null, null, null, "onInit",
directiveRecord);
}
static createDirectiveOnChange(directiveRecord: DirectiveRecord): BindingRecord {
return new BindingRecord(DIRECTIVE_LIFECYCLE, 0, null, 0, null, null, "onChange",
return new BindingRecord(DIRECTIVE_LIFECYCLE, 0, null, 0, null, null, null, "onChange",
directiveRecord);
}
static createForElement(ast: AST, elementIndex: number, propertyName: string): BindingRecord {
return new BindingRecord(ELEMENT, 0, ast, elementIndex, propertyName, null, null, null);
static createForElementProperty(ast: AST, elementIndex: number,
propertyName: string): BindingRecord {
return new BindingRecord(ELEMENT_PROPERTY, 0, ast, elementIndex, propertyName, null, null, null,
null);
}
static createForElementAttribute(ast: AST, elementIndex: number,
attributeName: string): BindingRecord {
return new BindingRecord(ELEMENT_ATTRIBUTE, 0, ast, elementIndex, attributeName, null, null,
null, null);
}
static createForElementClass(ast: AST, elementIndex: number, className: string): BindingRecord {
return new BindingRecord(ELEMENT_CLASS, 0, ast, elementIndex, className, null, null, null,
null);
}
static createForElementStyle(ast: AST, elementIndex: number, styleName: string,
unit: string): BindingRecord {
return new BindingRecord(ELEMENT_STYLE, 0, ast, elementIndex, styleName, unit, null, null,
null);
}
static createForHostProperty(directiveIndex: DirectiveIndex, ast: AST,
propertyName: string): BindingRecord {
return new BindingRecord(ELEMENT, directiveIndex, ast, directiveIndex.elementIndex,
propertyName, null, null, null);
return new BindingRecord(ELEMENT_PROPERTY, directiveIndex, ast, directiveIndex.elementIndex,
propertyName, null, null, null, null);
}
static createForHostAttribute(directiveIndex: DirectiveIndex, ast: AST,
attributeName: string): BindingRecord {
return new BindingRecord(ELEMENT_ATTRIBUTE, directiveIndex, ast, directiveIndex.elementIndex,
attributeName, null, null, null, null);
}
static createForHostClass(directiveIndex: DirectiveIndex, ast: AST,
className: string): BindingRecord {
return new BindingRecord(ELEMENT_CLASS, directiveIndex, ast, directiveIndex.elementIndex,
className, null, null, null, null);
}
static createForHostStyle(directiveIndex: DirectiveIndex, ast: AST, styleName: string,
unit: string): BindingRecord {
return new BindingRecord(ELEMENT_STYLE, directiveIndex, ast, directiveIndex.elementIndex,
styleName, unit, null, null, null);
}
static createForTextNode(ast: AST, elementIndex: number): BindingRecord {
return new BindingRecord(TEXT_NODE, 0, ast, elementIndex, null, null, null, null);
return new BindingRecord(TEXT_NODE, 0, ast, elementIndex, null, null, null, null, null);
}
}
@@ -6,9 +6,9 @@ import {List, StringMap} from 'angular2/src/facade/collection';
import * as viewModule from './view';
export class ElementBinder {
// updated later when events are bound
nestedProtoView: viewModule.AppProtoView = null;
// updated later, so we are able to resolve cycles
nestedProtoView: viewModule.AppProtoView = null;
// updated later when events are bound
hostListeners: StringMap<string, Map<number, AST>> = null;
constructor(public index: int, public parent: ElementBinder, public distanceToParent: int,
@@ -315,13 +315,13 @@ export class EventEmitterAccessor {
}
export class HostActionAccessor {
constructor(public actionExpression: string, public getter: Function) {}
constructor(public methodName: string, public getter: Function) {}
subscribe(view: viewModule.AppView, boundElementIndex: number, directive: Object) {
var eventEmitter = this.getter(directive);
return ObservableWrapper.subscribe(
eventEmitter,
actionObj => view.callAction(boundElementIndex, this.actionExpression, actionObj));
actionArgs => view.invokeElementMethod(boundElementIndex, this.methodName, actionArgs));
}
}
@@ -66,9 +66,20 @@ class BindingRecordsCreator {
_createElementPropertyRecords(bindings: List<BindingRecord>, boundElementIndex: number,
renderElementBinder: renderApi.ElementBinder) {
MapWrapper.forEach(renderElementBinder.propertyBindings, (astWithSource, propertyName) => {
bindings.push(BindingRecord.createForElement(astWithSource, boundElementIndex, propertyName));
ListWrapper.forEach(renderElementBinder.propertyBindings, (binding) => {
if (binding.type === renderApi.PropertyBindingType.PROPERTY) {
bindings.push(BindingRecord.createForElementProperty(binding.astWithSource,
boundElementIndex, binding.property));
} else if (binding.type === renderApi.PropertyBindingType.ATTRIBUTE) {
bindings.push(BindingRecord.createForElementAttribute(binding.astWithSource,
boundElementIndex, binding.property));
} else if (binding.type === renderApi.PropertyBindingType.CLASS) {
bindings.push(BindingRecord.createForElementClass(binding.astWithSource, boundElementIndex,
binding.property));
} else if (binding.type === renderApi.PropertyBindingType.STYLE) {
bindings.push(BindingRecord.createForElementStyle(binding.astWithSource, boundElementIndex,
binding.property, binding.unit));
}
});
}
@@ -103,10 +114,21 @@ class BindingRecordsCreator {
for (var i = 0; i < directiveBinders.length; i++) {
var directiveBinder = directiveBinders[i];
// host properties
MapWrapper.forEach(directiveBinder.hostPropertyBindings, (astWithSource, propertyName) => {
ListWrapper.forEach(directiveBinder.hostPropertyBindings, (binding) => {
var dirIndex = new DirectiveIndex(boundElementIndex, i);
bindings.push(BindingRecord.createForHostProperty(dirIndex, astWithSource, propertyName));
if (binding.type === renderApi.PropertyBindingType.PROPERTY) {
bindings.push(BindingRecord.createForHostProperty(dirIndex, binding.astWithSource,
binding.property));
} else if (binding.type === renderApi.PropertyBindingType.ATTRIBUTE) {
bindings.push(BindingRecord.createForHostAttribute(dirIndex, binding.astWithSource,
binding.property));
} else if (binding.type === renderApi.PropertyBindingType.CLASS) {
bindings.push(
BindingRecord.createForHostClass(dirIndex, binding.astWithSource, binding.property));
} else if (binding.type === renderApi.PropertyBindingType.STYLE) {
bindings.push(BindingRecord.createForHostStyle(dirIndex, binding.astWithSource,
binding.property, binding.unit));
}
});
}
}
+14 -5
View File
@@ -99,11 +99,20 @@ export class AppView implements ChangeDispatcher, EventDispatcher {
// dispatch to element injector or text nodes based on context
notifyOnBinding(b: BindingRecord, currentValue: any): void {
if (b.isElement()) {
if (b.isElementProperty()) {
this.renderer.setElementProperty(this.render, b.elementIndex, b.propertyName, currentValue);
} else {
// we know it refers to _textNodes.
} else if (b.isElementAttribute()) {
this.renderer.setElementAttribute(this.render, b.elementIndex, b.propertyName, currentValue);
} else if (b.isElementClass()) {
this.renderer.setElementClass(this.render, b.elementIndex, b.propertyName, currentValue);
} else if (b.isElementStyle()) {
var unit = isPresent(b.propertyUnit) ? b.propertyUnit : '';
this.renderer.setElementStyle(this.render, b.elementIndex, b.propertyName,
`${currentValue}${unit}`);
} else if (b.isTextNode()) {
this.renderer.setText(this.render, b.elementIndex, currentValue);
} else {
throw new BaseException('Unsupported directive record');
}
}
@@ -124,8 +133,8 @@ export class AppView implements ChangeDispatcher, EventDispatcher {
return isPresent(childView) ? childView.changeDetector : null;
}
callAction(elementIndex: number, actionExpression: string, action: Object) {
this.renderer.callAction(this.render, elementIndex, actionExpression, action);
invokeElementMethod(elementIndex: number, methodName: string, args: List<any>) {
this.renderer.invokeElementMethod(this.render, elementIndex, methodName, args);
}
// implementation of EventDispatcher#dispatchEvent
+20 -4
View File
@@ -1,7 +1,6 @@
library angular.core.facade.dom;
import 'dart:html';
import 'dart:js' show JsObject;
import 'dom_adapter.dart' show setRootDomAdapter;
import 'generic_browser_adapter.dart' show GenericBrowserDomAdapter;
import '../facade/browser.dart';
@@ -97,9 +96,28 @@ final _keyCodeToKeyMap = const {
};
class BrowserDomAdapter extends GenericBrowserDomAdapter {
js.JsFunction _setProperty;
js.JsFunction _getProperty;
js.JsFunction _hasProperty;
BrowserDomAdapter() {
_setProperty = js.context.callMethod('eval', ['(function(el, prop, value) { el[prop] = value; })']);
_getProperty = js.context.callMethod('eval', ['(function(el, prop) { return el[prop]; })']);
_hasProperty = js.context.callMethod('eval', ['(function(el, prop) { return prop in el; })']);
}
static void makeCurrent() {
setRootDomAdapter(new BrowserDomAdapter());
}
bool hasProperty(Element element, String name) =>
_hasProperty.apply([element, name]);
void setProperty(Element element, String name, Object value) =>
_setProperty.apply([element, name, value]);
getProperty(Element element, String name) =>
_getProperty.apply([element, name]);
invoke(Element element, String methodName, List args) =>
this.getProperty(element, methodName).apply(args, thisArg: element);
// TODO(tbosch): move this into a separate environment class once we have it
logError(error) {
@@ -108,7 +126,7 @@ class BrowserDomAdapter extends GenericBrowserDomAdapter {
@override
Map<String, String> get attrToPropMap => const <String, String>{
'innerHtml': 'innerHtml',
'innerHtml': 'innerHTML',
'readonly': 'readOnly',
'tabindex': 'tabIndex',
};
@@ -221,8 +239,6 @@ class BrowserDomAdapter extends GenericBrowserDomAdapter {
ShadowRoot getShadowRoot(Element el) => el.shadowRoot;
Element getHost(Element el) => (el as ShadowRoot).host;
clone(Node node) => node.clone(true);
bool hasProperty(Element element, String name) =>
new JsObject.fromBrowserObject(element).hasProperty(name);
List<Node> getElementsByClassName(Element element, String name) =>
element.getElementsByClassName(name);
List<Node> getElementsByTagName(Element element, String name) =>
+6 -1
View File
@@ -50,6 +50,12 @@ var _chromeNumKeyPadMap = {
export class BrowserDomAdapter extends GenericBrowserDomAdapter {
static makeCurrent() { setRootDomAdapter(new BrowserDomAdapter()); }
hasProperty(element, name: string) { return name in element; }
setProperty(el: /*element*/ any, name: string, value: any) { el[name] = value; }
getProperty(el: /*element*/ any, name: string): any { return el[name]; }
invoke(el: /*element*/ any, methodName: string, args: List<any>): any {
el[methodName].apply(el, args);
}
// TODO(tbosch): move this into a separate environment class once we have it
logError(error) { window.console.error(error); }
@@ -152,7 +158,6 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
getShadowRoot(el: HTMLElement): DocumentFragment { return (<any>el).shadowRoot; }
getHost(el: HTMLElement): HTMLElement { return (<any>el).host; }
clone(node: Node) { return node.cloneNode(true); }
hasProperty(element, name: string) { return name in element; }
getElementsByClassName(element, name: string) { return element.getElementsByClassName(name); }
getElementsByTagName(element, name: string) { return element.getElementsByTagName(name); }
classList(element): List<any> {
+5 -1
View File
@@ -16,6 +16,11 @@ function _abstract() {
* Provides DOM operations in an environment-agnostic way.
*/
export class DomAdapter {
hasProperty(element, name: string): boolean { throw _abstract(); }
setProperty(el: /*element*/ any, name: string, value: any) { throw _abstract(); }
getProperty(el: /*element*/ any, name: string): any { throw _abstract(); }
invoke(el: /*element*/ any, methodName: string, args: List<any>): any { throw _abstract(); }
logError(error) { throw _abstract(); }
/**
@@ -70,7 +75,6 @@ export class DomAdapter {
getHost(el): any { throw _abstract(); }
getDistributedNodes(el): List<any> { throw _abstract(); }
clone(node): any { throw _abstract(); }
hasProperty(element, name: string): boolean { throw _abstract(); }
getElementsByClassName(element, name: string): List<any> { throw _abstract(); }
getElementsByTagName(element, name: string): List<any> { throw _abstract(); }
classList(element): List<any> { throw _abstract(); }
+12 -6
View File
@@ -10,13 +10,24 @@ class Html5LibDomAdapter implements DomAdapter {
setRootDomAdapter(new Html5LibDomAdapter());
}
hasProperty(element, String name) {
// This is needed for serverside compile to generate the right getters/setters...
return true;
}
void setProperty(Element element, String name, Object value) => throw 'not implemented';
getProperty(Element element, String name) => throw 'not implemented';
invoke(Element element, String methodName, List args) => throw 'not implemented';
logError(error) {
stderr.writeln('${error}');
}
@override
final attrToPropMap = const {
'innerHtml': 'innerHtml',
'innerHtml': 'innerHTML',
'readonly': 'readOnly',
'tabindex': 'tabIndex',
};
@@ -184,11 +195,6 @@ class Html5LibDomAdapter implements DomAdapter {
throw 'not implemented';
}
clone(node) => node.clone(true);
hasProperty(element, String name) {
// This is needed for serverside compile to generate the right getters/setters...
return true;
}
getElementsByClassName(element, String name) {
throw 'not implemented';
}
+14 -1
View File
@@ -26,6 +26,20 @@ function _notImplemented(methodName) {
export class Parse5DomAdapter extends DomAdapter {
static makeCurrent() { setRootDomAdapter(new Parse5DomAdapter()); }
hasProperty(element, name: string) { return _HTMLElementPropertyList.indexOf(name) > -1; }
// TODO(tbosch): don't even call this method when we run the tests on server side
// by not using the DomRenderer in tests. Keeping this for now to make tests happy...
setProperty(el: /*element*/ any, name: string, value: any) {
if (name === 'innerHTML') {
this.setInnerHTML(el, value);
} else {
el[name] = value;
}
}
// TODO(tbosch): don't even call this method when we run the tests on server side
// by not using the DomRenderer in tests. Keeping this for now to make tests happy...
getProperty(el: /*element*/ any, name: string): any { return el[name]; }
logError(error) { console.error(error); }
get attrToPropMap() { return _attrToPropMap; }
@@ -268,7 +282,6 @@ export class Parse5DomAdapter extends DomAdapter {
return newParser.parseFragment(serialized).childNodes[0];
}
}
hasProperty(element, name: string) { return _HTMLElementPropertyList.indexOf(name) > -1; }
getElementsByClassName(element, name: string) {
return this.querySelectorAll(element, "." + name);
}
+2 -2
View File
@@ -69,8 +69,8 @@ export class MapWrapper {
static delete<K>(m: Map<K, any>, k: K) { m.delete(k); }
static clearValues(m: Map<any, any>) { _clearValues(m); }
static iterable(m) { return m; }
static keys<K>(m: Map<K, any>): List<K> { return m.keys(); }
static values<V>(m: Map<any, V>): List<V> { return m.values(); }
static keys<K>(m: Map<K, any>): List<K> { return (<any>Array).from(m.keys()); }
static values<V>(m: Map<any, V>): List<V> { return (<any>Array).from(m.values()); }
}
/**
+38 -11
View File
@@ -19,17 +19,30 @@ import {ASTWithSource} from 'angular2/change_detection';
* - render compiler is not on the critical path as
* its output will be stored in precompiled templates.
*/
export class EventBinding {
constructor(public fullName: string, public source: ASTWithSource) {}
}
export enum PropertyBindingType {
PROPERTY,
ATTRIBUTE,
CLASS,
STYLE
}
export class ElementPropertyBinding {
constructor(public type: PropertyBindingType, public astWithSource: ASTWithSource,
public property: string, public unit: string = null) {}
}
export class ElementBinder {
index: number;
parentIndex: number;
distanceToParent: number;
directives: List<DirectiveBinder>;
nestedProtoView: ProtoViewDto;
propertyBindings: Map<string, ASTWithSource>;
propertyBindings: List<ElementPropertyBinding>;
variableBindings: Map<string, string>;
// Note: this contains a preprocessed AST
// that replaced the values that should be extracted from the element
@@ -45,7 +58,7 @@ export class ElementBinder {
distanceToParent?: number,
directives?: List<DirectiveBinder>,
nestedProtoView?: ProtoViewDto,
propertyBindings?: Map<string, ASTWithSource>,
propertyBindings?: List<ElementPropertyBinding>,
variableBindings?: Map<string, string>,
eventBindings?: List<EventBinding>,
textBindings?: List<ASTWithSource>,
@@ -72,12 +85,12 @@ export class DirectiveBinder {
// that replaced the values that should be extracted from the element
// with a local name
eventBindings: List<EventBinding>;
hostPropertyBindings: Map<string, ASTWithSource>;
hostPropertyBindings: List<ElementPropertyBinding>;
constructor({directiveIndex, propertyBindings, eventBindings, hostPropertyBindings}: {
directiveIndex?: number,
propertyBindings?: Map<string, ASTWithSource>,
eventBindings?: List<EventBinding>,
hostPropertyBindings?: Map<string, ASTWithSource>
hostPropertyBindings?: List<ElementPropertyBinding>
}) {
this.directiveIndex = directiveIndex;
this.propertyBindings = propertyBindings;
@@ -358,19 +371,33 @@ export class Renderer {
/**
* Sets a property on an element.
* Note: This will fail if the property was not mentioned previously as a host property
* in the ProtoView
*/
setElementProperty(viewRef: RenderViewRef, elementIndex: number, propertyName: string,
propertyValue: any) {}
/**
* Calls an action.
* Note: This will fail if the action was not mentioned previously as a host action
* in the ProtoView
* Sets an attribute on an element.
*/
callAction(viewRef: RenderViewRef, elementIndex: number, actionExpression: string,
actionArgs: any) {}
setElementAttribute(viewRef: RenderViewRef, elementIndex: number, attributeName: string,
attributeValue: string) {}
/**
* Sets a class on an element.
*/
setElementClass(viewRef: RenderViewRef, elementIndex: number, className: string, isAdd: boolean) {
}
/**
* Sets a style on an element.
*/
setElementStyle(viewRef: RenderViewRef, elementIndex: number, styleName: string,
styleValue: string) {}
/**
* Calls a method on an element.
*/
invokeElementMethod(viewRef: RenderViewRef, elementIndex: number, methodName: string,
args: List<any>) {}
/**
* Sets the value of a text node.
@@ -17,7 +17,6 @@ import {TemplateLoader} from 'angular2/src/render/dom/compiler/template_loader';
import {CompileStepFactory, DefaultStepFactory} from './compile_step_factory';
import {Parser} from 'angular2/change_detection';
import {ShadowDomStrategy} from '../shadow_dom/shadow_dom_strategy';
import {PropertySetterFactory} from '../view/property_setter_factory';
/**
* The compiler loads and translates the html templates of components into
@@ -25,8 +24,6 @@ import {PropertySetterFactory} from '../view/property_setter_factory';
* the CompilePipeline and the CompileSteps.
*/
export class DomCompiler extends RenderCompiler {
_propertySetterFactory: PropertySetterFactory = new PropertySetterFactory();
constructor(public _stepFactory: CompileStepFactory, public _templateLoader: TemplateLoader) {
super();
}
@@ -58,7 +55,7 @@ export class DomCompiler extends RenderCompiler {
var pipeline = new CompilePipeline(this._stepFactory.createSteps(viewDef, subTaskPromises));
var compileElements = pipeline.process(tplElement, protoViewType, viewDef.componentId);
var protoView = compileElements[0].inheritedProtoView.build(this._propertySetterFactory);
var protoView = compileElements[0].inheritedProtoView.build();
if (subTaskPromises.length > 0) {
return PromiseWrapper.all(subTaskPromises).then((_) => protoView);
@@ -91,11 +91,6 @@ export class DirectiveParser implements CompileStep {
this._bindDirectiveEvent(eventName, action, current, directiveBinderBuilder);
});
}
if (isPresent(dirMetadata.hostActions)) {
MapWrapper.forEach(dirMetadata.hostActions, (action, actionName) => {
this._bindHostAction(actionName, action, current, directiveBinderBuilder);
});
}
if (isPresent(dirMetadata.hostProperties)) {
MapWrapper.forEach(dirMetadata.hostProperties, (expression, hostPropertyName) => {
this._bindHostProperty(hostPropertyName, expression, current, directiveBinderBuilder);
@@ -134,9 +129,8 @@ export class DirectiveParser implements CompileStep {
elProp = bindConfig;
pipes = [];
}
var bindingAst = compileElement.bindElement().propertyBindings.get(dashCaseToCamelCase(elProp));
elProp = dashCaseToCamelCase(elProp);
var bindingAst = compileElement.bindElement().propertyBindings.get(elProp);
if (isBlank(bindingAst)) {
var attributeValue = compileElement.attrs().get(camelCaseToDashCase(elProp));
if (isPresent(attributeValue)) {
@@ -147,9 +141,8 @@ export class DirectiveParser implements CompileStep {
// Bindings are optional, so this binding only needs to be set up if an expression is given.
if (isPresent(bindingAst)) {
directiveBinderBuilder.bindProperty(dirProperty, bindingAst);
directiveBinderBuilder.bindProperty(dirProperty, bindingAst, elProp);
}
compileElement.bindElement().bindPropertyToDirective(dashCaseToCamelCase(elProp));
}
_bindDirectiveEvent(eventName, action, compileElement, directiveBinderBuilder) {
@@ -162,11 +155,6 @@ export class DirectiveParser implements CompileStep {
}
}
_bindHostAction(actionName, actionExpression, compileElement, directiveBinderBuilder) {
var ast = this._parser.parseAction(actionExpression, compileElement.elementDescription);
directiveBinderBuilder.bindHostAction(actionName, actionExpression, ast);
}
_bindHostProperty(hostPropertyName, expression, compileElement, directiveBinderBuilder) {
var ast = this._parser.parseSimpleBinding(
expression, `hostProperties of ${compileElement.elementDescription}`);
@@ -187,10 +187,28 @@ export class DomRenderer extends Renderer {
view.setElementProperty(elementIndex, propertyName, propertyValue);
}
callAction(viewRef: RenderViewRef, elementIndex: number, actionExpression: string,
actionArgs: any): void {
setElementAttribute(viewRef: RenderViewRef, elementIndex: number, attributeName: string,
attributeValue: string): void {
var view = resolveInternalDomView(viewRef);
view.callAction(elementIndex, actionExpression, actionArgs);
view.setElementAttribute(elementIndex, attributeName, attributeValue);
}
setElementClass(viewRef: RenderViewRef, elementIndex: number, className: string,
isAdd: boolean): void {
var view = resolveInternalDomView(viewRef);
view.setElementClass(elementIndex, className, isAdd);
}
setElementStyle(viewRef: RenderViewRef, elementIndex: number, styleName: string,
styleValue: string): void {
var view = resolveInternalDomView(viewRef);
view.setElementStyle(elementIndex, styleName, styleValue);
}
invokeElementMethod(viewRef: RenderViewRef, elementIndex: number, methodName: string,
args: List<any>): void {
var view = resolveInternalDomView(viewRef);
view.invokeElementMethod(elementIndex, methodName, args);
}
setText(viewRef: RenderViewRef, textNodeIndex: number, text: string): void {
@@ -1,5 +1,4 @@
import {AST} from 'angular2/change_detection';
import {SetterFn} from 'angular2/src/reflection/types';
import {List, ListWrapper} from 'angular2/src/facade/collection';
import * as protoViewModule from './proto_view';
@@ -13,13 +12,10 @@ export class ElementBinder {
componentId: string;
parentIndex: number;
distanceToParent: number;
propertySetters: Map<string, SetterFn>;
hostActions: Map<string, AST>;
elementIsEmpty: boolean;
constructor({textNodeIndices, contentTagSelector, nestedProtoView, componentId, eventLocals,
localEvents, globalEvents, hostActions, parentIndex, distanceToParent,
propertySetters, elementIsEmpty}: {
localEvents, globalEvents, parentIndex, distanceToParent, elementIsEmpty}: {
contentTagSelector?: string,
textNodeIndices?: List<number>,
nestedProtoView?: protoViewModule.DomProtoView,
@@ -29,8 +25,6 @@ export class ElementBinder {
componentId?: string,
parentIndex?: number,
distanceToParent?: number,
propertySetters?: Map<string, SetterFn>,
hostActions?: Map<string, AST>,
elementIsEmpty?: boolean
} = {}) {
this.textNodeIndices = textNodeIndices;
@@ -40,10 +34,8 @@ export class ElementBinder {
this.eventLocals = eventLocals;
this.localEvents = localEvents;
this.globalEvents = globalEvents;
this.hostActions = hostActions;
this.parentIndex = parentIndex;
this.distanceToParent = distanceToParent;
this.propertySetters = propertySetters;
this.elementIsEmpty = elementIsEmpty;
}
}
@@ -1,156 +0,0 @@
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 = '.';
const ATTRIBUTE_PREFIX = 'attr.';
const CLASS_PREFIX = 'class.';
const STYLE_PREFIX = 'style.';
export class PropertySetterFactory {
static noopSetter(el, value) {}
private _lazyPropertySettersCache: StringMap<string, Function> = StringMapWrapper.create();
private _eagerPropertySettersCache: StringMap<string, Function> = StringMapWrapper.create();
private _innerHTMLSetterCache: Function = (el, value) => DOM.setInnerHTML(el, value);
private _attributeSettersCache: StringMap<string, Function> = StringMapWrapper.create();
private _classSettersCache: StringMap<string, Function> = StringMapWrapper.create();
private _styleSettersCache: StringMap<string, Function> = StringMapWrapper.create();
createSetter(protoElement: /*element*/ any, isNgComponent: boolean, property: string): Function {
var setterFn, styleParts, styleSuffix;
if (StringWrapper.startsWith(property, ATTRIBUTE_PREFIX)) {
setterFn =
this._attributeSetterFactory(StringWrapper.substring(property, ATTRIBUTE_PREFIX.length));
} else if (StringWrapper.startsWith(property, CLASS_PREFIX)) {
setterFn = this._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 = this._styleSetterFactory(ListWrapper.get(styleParts, 1), styleSuffix);
} else if (StringWrapper.equals(property, 'innerHtml')) {
setterFn = this._innerHTMLSetterCache;
} else {
property = this._resolvePropertyName(property);
setterFn = this._propertySetterFactory(protoElement, isNgComponent, property);
}
return setterFn;
}
private _propertySetterFactory(protoElement, isNgComponent: boolean, property: string): Function {
var setterFn;
var tagName = DOM.tagName(protoElement);
var possibleCustomElement = tagName.indexOf('-') !== -1;
if (possibleCustomElement && !isNgComponent) {
// need to use late check to be able to set properties on custom elements
setterFn = StringMapWrapper.get(this._lazyPropertySettersCache, property);
if (isBlank(setterFn)) {
var propertySetterFn = reflector.setter(property);
setterFn = (receiver, value) => {
if (DOM.hasProperty(receiver, property)) {
return propertySetterFn(receiver, value);
}
};
StringMapWrapper.set(this._lazyPropertySettersCache, property, setterFn);
}
} else {
setterFn = StringMapWrapper.get(this._eagerPropertySettersCache, property);
if (isBlank(setterFn)) {
if (DOM.hasProperty(protoElement, property)) {
setterFn = reflector.setter(property);
} else {
setterFn = PropertySetterFactory.noopSetter;
}
StringMapWrapper.set(this._eagerPropertySettersCache, property, setterFn);
}
}
return setterFn;
}
private _isValidAttributeValue(attrName: string, value: any): boolean {
if (attrName == "role") {
return isString(value);
} else {
return isPresent(value);
}
}
private _attributeSetterFactory(attrName: string): Function {
var setterFn = StringMapWrapper.get(this._attributeSettersCache, attrName);
var dashCasedAttributeName;
if (isBlank(setterFn)) {
dashCasedAttributeName = camelCaseToDashCase(attrName);
setterFn = (element, value) => {
if (this._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(this._attributeSettersCache, attrName, setterFn);
}
return setterFn;
}
private _classSetterFactory(className: string): Function {
var setterFn = StringMapWrapper.get(this._classSettersCache, className);
var dashCasedClassName;
if (isBlank(setterFn)) {
dashCasedClassName = camelCaseToDashCase(className);
setterFn = (element, isAdd) => {
if (isAdd) {
DOM.addClass(element, dashCasedClassName);
} else {
DOM.removeClass(element, dashCasedClassName);
}
};
StringMapWrapper.set(this._classSettersCache, className, setterFn);
}
return setterFn;
}
private _styleSetterFactory(styleName: string, styleSuffix: string): Function {
var cacheKey = styleName + styleSuffix;
var setterFn = StringMapWrapper.get(this._styleSettersCache, cacheKey);
var dashCasedStyleName;
if (isBlank(setterFn)) {
dashCasedStyleName = camelCaseToDashCase(styleName);
setterFn = (element, value) => {
var valAsStr;
if (isPresent(value)) {
valAsStr = stringify(value);
DOM.setStyle(element, dashCasedStyleName, valAsStr + styleSuffix);
} else {
DOM.removeStyle(element, dashCasedStyleName);
}
};
StringMapWrapper.set(this._styleSettersCache, cacheKey, setterFn);
}
return setterFn;
}
private _resolvePropertyName(attrName: string): string {
var mappedPropName = StringMapWrapper.get(DOM.attrToPropMap, attrName);
return isPresent(mappedPropName) ? mappedPropName : attrName;
}
}
@@ -1,5 +1,12 @@
import {isPresent, isBlank, BaseException} from 'angular2/src/facade/lang';
import {ListWrapper, MapWrapper, Set, SetWrapper, List} from 'angular2/src/facade/collection';
import {isPresent, isBlank, BaseException, StringWrapper} from 'angular2/src/facade/lang';
import {
ListWrapper,
MapWrapper,
Set,
SetWrapper,
List,
StringMapWrapper
} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {
@@ -13,7 +20,6 @@ import {
import {DomProtoView, DomProtoViewRef, resolveInternalDomProtoView} from './proto_view';
import {ElementBinder, Event, HostAction} from './element_binder';
import {PropertySetterFactory} from './property_setter_factory';
import * as api from '../../api';
@@ -43,53 +49,28 @@ export class ProtoViewBuilder {
this.variableBindings.set(value, name);
}
build(setterFactory: PropertySetterFactory): api.ProtoViewDto {
build(): api.ProtoViewDto {
var renderElementBinders = [];
var apiElementBinders = [];
var transitiveContentTagCount = 0;
var boundTextNodeCount = 0;
ListWrapper.forEach(this.elements, (ebb: ElementBinderBuilder) => {
var propertySetters = new Map();
var hostActions = new Map();
var directiveTemplatePropertyNames = new Set();
var apiDirectiveBinders = ListWrapper.map(ebb.directives, (dbb: DirectiveBuilder) => {
ebb.eventBuilder.merge(dbb.eventBuilder);
MapWrapper.forEach(dbb.hostPropertyBindings, (_, hostPropertyName) => {
propertySetters.set(hostPropertyName,
setterFactory.createSetter(ebb.element, isPresent(ebb.componentId),
hostPropertyName));
});
ListWrapper.forEach(dbb.hostActions, (hostAction) => {
hostActions.set(hostAction.actionExpression, hostAction.expression);
});
ListWrapper.forEach(dbb.templatePropertyNames,
(name) => directiveTemplatePropertyNames.add(name));
return new api.DirectiveBinder({
directiveIndex: dbb.directiveIndex,
propertyBindings: dbb.propertyBindings,
eventBindings: dbb.eventBindings,
hostPropertyBindings: dbb.hostPropertyBindings
hostPropertyBindings:
buildElementPropertyBindings(ebb.element, isPresent(ebb.componentId),
dbb.hostPropertyBindings, directiveTemplatePropertyNames)
});
});
MapWrapper.forEach(ebb.propertyBindings, (_, propertyName) => {
var propSetter =
setterFactory.createSetter(ebb.element, isPresent(ebb.componentId), propertyName);
if (propSetter === PropertySetterFactory.noopSetter) {
if (!SetWrapper.has(ebb.propertyBindingsToDirectives, propertyName)) {
throw new BaseException(
`Can't bind to '${propertyName}' since it isn't a know property of the '${DOM.tagName(ebb.element).toLowerCase()}' element and there are no matching directives with a corresponding property`);
}
}
propertySetters.set(propertyName, propSetter);
});
var nestedProtoView =
isPresent(ebb.nestedProtoView) ? ebb.nestedProtoView.build(setterFactory) : null;
var nestedProtoView = isPresent(ebb.nestedProtoView) ? ebb.nestedProtoView.build() : null;
var nestedRenderProtoView =
isPresent(nestedProtoView) ? resolveInternalDomProtoView(nestedProtoView.render) : null;
if (isPresent(nestedRenderProtoView)) {
@@ -105,7 +86,9 @@ export class ProtoViewBuilder {
distanceToParent: ebb.distanceToParent,
directives: apiDirectiveBinders,
nestedProtoView: nestedProtoView,
propertyBindings: ebb.propertyBindings,
propertyBindings:
buildElementPropertyBindings(ebb.element, isPresent(ebb.componentId),
ebb.propertyBindings, directiveTemplatePropertyNames),
variableBindings: ebb.variableBindings,
eventBindings: ebb.eventBindings,
textBindings: ebb.textBindings,
@@ -124,8 +107,6 @@ export class ProtoViewBuilder {
eventLocals: new LiteralArray(ebb.eventBuilder.buildEventLocals()),
localEvents: ebb.eventBuilder.buildLocalEvents(),
globalEvents: ebb.eventBuilder.buildGlobalEvents(),
hostActions: hostActions,
propertySetters: propertySetters,
elementIsEmpty: childNodeInfo.elementIsEmpty
}));
});
@@ -216,7 +197,9 @@ export class ElementBinderBuilder {
return this.nestedProtoView;
}
bindProperty(name, expression) { this.propertyBindings.set(name, expression); }
bindProperty(name: string, expression: ASTWithSource) {
this.propertyBindings.set(name, expression);
}
bindPropertyToDirective(name: string) {
// we are filling in a set of property names that are bound to a property
@@ -257,20 +240,27 @@ export class ElementBinderBuilder {
}
export class DirectiveBuilder {
// mapping from directive property name to AST for that directive
propertyBindings: Map<string, ASTWithSource> = new Map();
// property names used in the template
templatePropertyNames: List<string> = [];
hostPropertyBindings: Map<string, ASTWithSource> = new Map();
hostActions: List<HostAction> = [];
eventBindings: List<api.EventBinding> = [];
eventBuilder: EventBuilder = new EventBuilder();
constructor(public directiveIndex: number) {}
bindProperty(name, expression) { this.propertyBindings.set(name, expression); }
bindProperty(name: string, expression: ASTWithSource, elProp: string) {
this.propertyBindings.set(name, expression);
if (isPresent(elProp)) {
// we are filling in a set of property names that are bound to a property
// of at least one directive. This allows us to report "dangling" bindings.
this.templatePropertyNames.push(elProp);
}
}
bindHostProperty(name, expression) { this.hostPropertyBindings.set(name, expression); }
bindHostAction(actionName: string, actionExpression: string, expression: ASTWithSource) {
this.hostActions.push(new HostAction(actionName, actionExpression, expression));
bindHostProperty(name: string, expression: ASTWithSource) {
this.hostPropertyBindings.set(name, expression);
}
bindEvent(name, expression, target = null) {
@@ -347,3 +337,60 @@ export class EventBuilder extends AstTransformer {
}
}
}
var PROPERTY_PARTS_SEPARATOR = new RegExp('\\.');
const ATTRIBUTE_PREFIX = 'attr';
const CLASS_PREFIX = 'class';
const STYLE_PREFIX = 'style';
function buildElementPropertyBindings(protoElement: /*element*/ any, isNgComponent: boolean,
bindingsInTemplate: Map<string, ASTWithSource>,
directiveTempaltePropertyNames: Set<string>) {
var propertyBindings = [];
MapWrapper.forEach(bindingsInTemplate, (ast, propertyNameInTemplate) => {
var propertyBinding = createElementPropertyBinding(ast, propertyNameInTemplate);
if (isValidElementPropertyBinding(protoElement, isNgComponent, propertyBinding)) {
propertyBindings.push(propertyBinding);
} else if (!SetWrapper.has(directiveTempaltePropertyNames, propertyNameInTemplate)) {
throw new BaseException(
`Can't bind to '${propertyNameInTemplate}' since it isn't a know property of the '${DOM.tagName(protoElement).toLowerCase()}' element and there are no matching directives with a corresponding property`);
}
});
return propertyBindings;
}
function isValidElementPropertyBinding(protoElement: /*element*/ any, isNgComponent: boolean,
binding: api.ElementPropertyBinding): boolean {
if (binding.type === api.PropertyBindingType.PROPERTY) {
var tagName = DOM.tagName(protoElement);
var possibleCustomElement = tagName.indexOf('-') !== -1;
if (possibleCustomElement && !isNgComponent) {
// can't tell now as we don't know which properties a custom element will get
// once it is instantiated
return true;
} else {
return DOM.hasProperty(protoElement, binding.property);
}
}
return true;
}
function createElementPropertyBinding(ast: ASTWithSource,
propertyNameInTemplate: string): api.ElementPropertyBinding {
var parts = StringWrapper.split(propertyNameInTemplate, PROPERTY_PARTS_SEPARATOR);
if (parts.length === 1) {
var propName = parts[0];
var mappedPropName = StringMapWrapper.get(DOM.attrToPropMap, propName);
propName = isPresent(mappedPropName) ? mappedPropName : propName;
return new api.ElementPropertyBinding(api.PropertyBindingType.PROPERTY, ast, propName);
} else if (parts[0] == ATTRIBUTE_PREFIX) {
return new api.ElementPropertyBinding(api.PropertyBindingType.ATTRIBUTE, ast, parts[1]);
} else if (parts[0] == CLASS_PREFIX) {
return new api.ElementPropertyBinding(api.PropertyBindingType.CLASS, ast, parts[1]);
} else if (parts[0] == STYLE_PREFIX) {
var unit = parts.length > 2 ? parts[2] : null;
return new api.ElementPropertyBinding(api.PropertyBindingType.STYLE, ast, parts[1], unit);
} else {
throw new BaseException(`Invalid property name ${propertyNameInTemplate}`);
}
}
+34 -12
View File
@@ -1,13 +1,13 @@
import {DOM} from 'angular2/src/dom/dom_adapter';
import {ListWrapper, MapWrapper, Map, StringMapWrapper, List} from 'angular2/src/facade/collection';
import {Locals} from 'angular2/change_detection';
import {isPresent, isBlank, BaseException} from 'angular2/src/facade/lang';
import {isPresent, isBlank, BaseException, stringify} from 'angular2/src/facade/lang';
import {DomProtoView} from './proto_view';
import {LightDom} from '../shadow_dom/light_dom';
import {DomElement} from './element';
import {RenderViewRef, EventDispatcher} from '../../api';
import {camelCaseToDashCase} from '../util';
export function resolveInternalDomView(viewRef: RenderViewRef) {
return (<DomViewRef>viewRef)._view;
@@ -40,20 +40,42 @@ export class DomView {
}
setElementProperty(elementIndex: number, propertyName: string, value: any) {
var setter = this.proto.elementBinders[elementIndex].propertySetters.get(propertyName);
setter(this.boundElements[elementIndex].element, value);
DOM.setProperty(this.boundElements[elementIndex].element, propertyName, value);
}
callAction(elementIndex: number, actionExpression: string, actionArgs: any) {
var binder = this.proto.elementBinders[elementIndex];
var hostAction = binder.hostActions.get(actionExpression);
hostAction.eval(this.boundElements[elementIndex].element, this._localsWithAction(actionArgs));
setElementAttribute(elementIndex: number, attributeName: string, value: string) {
var element = this.boundElements[elementIndex].element;
var dashCasedAttributeName = camelCaseToDashCase(attributeName);
if (isPresent(value)) {
DOM.setAttribute(element, dashCasedAttributeName, stringify(value));
} else {
DOM.removeAttribute(element, dashCasedAttributeName);
}
}
_localsWithAction(action: Object): Locals {
var map = new Map();
map.set('$action', action);
return new Locals(null, map);
setElementClass(elementIndex: number, className: string, isAdd: boolean) {
var element = this.boundElements[elementIndex].element;
var dashCasedClassName = camelCaseToDashCase(className);
if (isAdd) {
DOM.addClass(element, dashCasedClassName);
} else {
DOM.removeClass(element, dashCasedClassName);
}
}
setElementStyle(elementIndex: number, styleName: string, value: string) {
var element = this.boundElements[elementIndex].element;
var dashCasedStyleName = camelCaseToDashCase(styleName);
if (isPresent(value)) {
DOM.setStyle(element, dashCasedStyleName, stringify(value));
} else {
DOM.removeStyle(element, dashCasedStyleName);
}
}
invokeElementMethod(elementIndex: number, methodName: string, args: List<any>) {
var element = this.boundElements[elementIndex].element;
DOM.invoke(element, methodName, args);
}
setText(textIndex: number, value: string) { DOM.setText(this.boundTextNodes[textIndex], value); }
@@ -8,7 +8,6 @@ import 'package:angular2/src/core/compiler/proto_view_factory.dart';
import 'package:angular2/src/render/api.dart';
import 'package:angular2/src/render/dom/compiler/compile_pipeline.dart';
import 'package:angular2/src/render/dom/compiler/template_loader.dart';
import 'package:angular2/src/render/dom/view/property_setter_factory.dart';
import 'package:angular2/src/render/xhr.dart' show XHR;
import 'package:angular2/src/reflection/reflection.dart';
import 'package:angular2/src/services/url_resolver.dart';
@@ -106,7 +105,7 @@ class _TemplateExtractor {
var compileElements =
pipeline.process(templateEl, ViewType.COMPONENT, viewDef.componentId);
var protoViewDto = compileElements[0].inheritedProtoView
.build(new PropertySetterFactory());
.build();
reflector.reflectionCapabilities = savedReflectionCapabilities;