fix: properly bind to camelCased properties

Fixes #866
Closes #941
This commit is contained in:
Pawel Kozlowski 2015-03-12 17:28:35 +01:00
parent afda43dc02
commit b39d2c0101
8 changed files with 61 additions and 21 deletions

View File

@ -6,6 +6,7 @@ import {Decorator, Component, Viewport} from '../../annotations/annotations';
import {ElementBinder} from '../element_binder';
import {ProtoElementInjector} from '../element_injector';
import {ProtoView} from '../view';
import {dashCaseToCamelCase} from './util';
import {AST} from 'angular2/change_detection';
@ -114,7 +115,7 @@ export class CompileElement {
if (isBlank(this.propertyBindings)) {
this.propertyBindings = MapWrapper.create();
}
MapWrapper.set(this.propertyBindings, property, expression);
MapWrapper.set(this.propertyBindings, dashCaseToCamelCase(property), expression);
}
addVariableBinding(variableName:string, variableValue:string) {

View File

@ -10,7 +10,8 @@ import {CompileStep} from './compile_step';
import {CompileElement} from './compile_element';
import {CompileControl} from './compile_control';
import {isSpecialProperty} from './element_binder_builder';;
import {isSpecialProperty} from './element_binder_builder';
import {dashCaseToCamelCase, camelCaseToDashCase} from './util';
var PROPERTY_BINDING_REGEXP = RegExpWrapper.create('^ *([^\\s\\|]+)');
@ -84,7 +85,7 @@ function updateMatchedProperties(matchedProperties, selector, directive) {
if (isPresent(attrs)) {
for (var idx = 0; idx<attrs.length; idx+=2) {
// attribute name is stored on even indexes
StringMapWrapper.set(matchedProperties, attrs[idx], true);
StringMapWrapper.set(matchedProperties, dashCaseToCamelCase(attrs[idx]), true);
}
}
// some properties can be used by the directive, so we need to register them
@ -97,7 +98,7 @@ function updateMatchedProperties(matchedProperties, selector, directive) {
// keep the property name and remove the pipe
var bindProp = RegExpWrapper.firstMatch(PROPERTY_BINDING_REGEXP, value);
if (isPresent(bindProp) && isPresent(bindProp[1])) {
StringMapWrapper.set(matchedProperties, bindProp[1], true);
StringMapWrapper.set(matchedProperties, dashCaseToCamelCase(bindProp[1]), true);
}
});
}
@ -130,7 +131,7 @@ function checkMissingDirectives(current, matchedProperties, isTemplateElement) {
MapWrapper.forEach(ppBindings, (expression, prop) => {
if (!DOM.hasProperty(current.element, prop) && !isSpecialProperty(prop)) {
if (!isPresent(matchedProperties) || !isPresent(StringMapWrapper.get(matchedProperties, prop))) {
throw new BaseException(`Missing directive to handle '${prop}' in ${current.elementDescription}`);
throw new BaseException(`Missing directive to handle '${camelCaseToDashCase(prop)}' in ${current.elementDescription}`);
}
}
});

View File

@ -11,21 +11,24 @@ import {DirectiveMetadata} from '../directive_metadata';
import {CompileStep} from './compile_step';
import {CompileElement} from './compile_element';
import {CompileControl} from './compile_control';
import {dashCaseToCamelCase, camelCaseToDashCase} from './util';
var DOT_REGEXP = RegExpWrapper.create('\\.');
const ARIA_PREFIX = 'aria-';
const ARIA_PREFIX = 'aria';
var ariaSettersCache = StringMapWrapper.create();
function ariaSetterFactory(attrName:string) {
var setterFn = StringMapWrapper.get(ariaSettersCache, attrName);
var ariaAttrName;
if (isBlank(setterFn)) {
ariaAttrName = camelCaseToDashCase(attrName);
setterFn = function(element, value) {
if (isPresent(value)) {
DOM.setAttribute(element, attrName, stringify(value));
DOM.setAttribute(element, ariaAttrName, stringify(value));
} else {
DOM.removeAttribute(element, attrName);
DOM.removeAttribute(element, ariaAttrName);
}
};
StringMapWrapper.set(ariaSettersCache, attrName, setterFn);
@ -34,7 +37,6 @@ function ariaSetterFactory(attrName:string) {
return setterFn;
}
const CLASS_ATTR = 'class';
const CLASS_PREFIX = 'class.';
var classSettersCache = StringMapWrapper.create();
@ -55,22 +57,23 @@ function classSetterFactory(className:string) {
return setterFn;
}
const STYLE_ATTR = 'style';
const STYLE_PREFIX = 'style.';
var styleSettersCache = StringMapWrapper.create();
function styleSetterFactory(styleName:string, stylesuffix:string) {
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, styleName, valAsStr + stylesuffix);
DOM.setStyle(element, dashCasedStyleName, valAsStr + stylesuffix);
} else {
DOM.removeStyle(element, styleName);
DOM.removeStyle(element, dashCasedStyleName);
}
};
StringMapWrapper.set(classSettersCache, cacheKey, setterFn);
@ -229,7 +232,7 @@ export class ElementBinderBuilder extends CompileStep {
var elProp = ListWrapper.removeAt(pipes, 0);
var bindingAst = isPresent(compileElement.propertyBindings) ?
MapWrapper.get(compileElement.propertyBindings, elProp) :
MapWrapper.get(compileElement.propertyBindings, dashCaseToCamelCase(elProp)) :
null;
if (isBlank(bindingAst)) {
@ -246,7 +249,7 @@ export class ElementBinderBuilder extends CompileStep {
directiveIndex,
fullExpAstWithBindPipes,
dirProp,
reflector.setter(dirProp)
reflector.setter(dashCaseToCamelCase(dirProp))
);
}
});

View File

@ -0,0 +1,16 @@
import {StringWrapper, RegExpWrapper} from 'angular2/src/facade/lang';
var DASH_CASE_REGEXP = RegExpWrapper.create('-([a-z])');
var CAMEL_CASE_REGEXP = RegExpWrapper.create('([A-Z])');
export function dashCaseToCamelCase(input:string) {
return StringWrapper.replaceAllMapped(input, DASH_CASE_REGEXP, (m) => {
return m[1].toUpperCase();
});
}
export function camelCaseToDashCase(input:string) {
return StringWrapper.replaceAllMapped(input, CAMEL_CASE_REGEXP, (m) => {
return '-' + m[1].toLowerCase();
});
}

View File

@ -21,7 +21,7 @@ class BrowserDomAdapter extends GenericBrowserDomAdapter {
@override
final attrToPropMap = const {
'inner-html': 'innerHtml',
'innerHtml': 'innerHtml',
'readonly': 'readOnly',
'tabindex': 'tabIndex',
};

View File

@ -4,9 +4,9 @@ import {setRootDomAdapter} from './dom_adapter';
import {GenericBrowserDomAdapter} from './generic_browser_adapter';
var _attrToPropMap = {
'inner-html': 'innerHTML',
'innerHtml': 'innerHTML',
'readonly': 'readOnly',
'tabindex': 'tabIndex',
'tabindex': 'tabIndex'
};
export class BrowserDomAdapter extends GenericBrowserDomAdapter {

View File

@ -13,7 +13,7 @@ import {BaseException, isPresent, isBlank} from 'angular2/src/facade/lang';
import {SelectorMatcher, CssSelector} from 'angular2/src/core/compiler/selector';
var _attrToPropMap = {
'inner-html': 'innerHTML',
'innerHtml': 'innerHTML',
'readonly': 'readOnly',
'tabindex': 'tabIndex',
};
@ -206,7 +206,7 @@ export class Parse5DomAdapter extends DomAdapter {
}
setText(el, value:string) {
if (this.isTextNode(el)) {
el.data = value;
el.data = value;
} else {
this.clearNodes(el);
treeAdapter.insertText(el, value);
@ -315,7 +315,7 @@ export class Parse5DomAdapter extends DomAdapter {
for (var key in styleMap) {
var newValue = styleMap[key];
if (newValue && newValue.length > 0) {
styleAttrValue += key + ":" + styleMap[key] + ";";
styleAttrValue += key + ":" + styleMap[key] + ";";
}
}
element.attribs["style"] = styleAttrValue;
@ -427,7 +427,7 @@ export class Parse5DomAdapter extends DomAdapter {
var declaration = parsedRule.declarations[j];
rule.style[declaration.property] = declaration.value;
rule.style.cssText += declaration.property + ": " + declaration.value + ";";
}
}
} else if (parsedRule.type == "media") {
rule.type = 4;
rule.media = {mediaText: parsedRule.media};

View File

@ -133,6 +133,23 @@ export function main() {
});
}));
it('should consume binding to camel-cased properties using dash-cased syntax in templates', inject([AsyncTestCompleter], (async) => {
tplResolver.setTemplate(MyComp, new Template({inline: '<input [read-only]="ctxBoolProp">'}));
compiler.compile(MyComp).then((pv) => {
createView(pv);
cd.detectChanges();
expect(view.nodes[0].readOnly).toBeFalsy();
ctx.ctxBoolProp = true;
cd.detectChanges();
expect(view.nodes[0].readOnly).toBeTruthy();
async.done();
});
}));
it('should consume binding to inner-html', inject([AsyncTestCompleter], (async) => {
tplResolver.setTemplate(MyComp, new Template({inline: '<div inner-html="{{ctxProp}}"></div>'}));
@ -592,9 +609,11 @@ class PushBasedComp {
class MyComp {
ctxProp:string;
ctxNumProp;
ctxBoolProp;
constructor() {
this.ctxProp = 'initial value';
this.ctxNumProp = 0;
this.ctxBoolProp = false;
}
}