diff --git a/modules/angular2/src/core/compiler/pipeline/element_binder_builder.js b/modules/angular2/src/core/compiler/pipeline/element_binder_builder.js index 790105261b..d252295d50 100644 --- a/modules/angular2/src/core/compiler/pipeline/element_binder_builder.js +++ b/modules/angular2/src/core/compiler/pipeline/element_binder_builder.js @@ -15,23 +15,34 @@ import {dashCaseToCamelCase, camelCaseToDashCase} from './util'; var DOT_REGEXP = RegExpWrapper.create('\\.'); -const ARIA_PREFIX = 'aria'; -var ariaSettersCache = StringMapWrapper.create(); +const ATTRIBUTE_PREFIX = 'attr.'; +var attributeSettersCache = StringMapWrapper.create(); -function ariaSetterFactory(attrName:string) { - var setterFn = StringMapWrapper.get(ariaSettersCache, attrName); - var ariaAttrName; +function _isValidAttributeValue(attrName:string, value: any) { + if (attrName == "role") { + return isString(value); + } else { + return isPresent(value); + } +} + +function attributeSetterFactory(attrName:string) { + var setterFn = StringMapWrapper.get(attributeSettersCache, attrName); + var dashCasedAttributeName; if (isBlank(setterFn)) { - ariaAttrName = camelCaseToDashCase(attrName); + dashCasedAttributeName = camelCaseToDashCase(attrName); setterFn = function(element, value) { - if (isPresent(value)) { - DOM.setAttribute(element, ariaAttrName, stringify(value)); + if (_isValidAttributeValue(dashCasedAttributeName, value)) { + DOM.setAttribute(element, dashCasedAttributeName, stringify(value)); } else { - DOM.removeAttribute(element, ariaAttrName); + DOM.removeAttribute(element, dashCasedAttributeName); + if (isPresent(value)) { + throw new BaseException("Invalid " + dashCasedAttributeName + " attribute, only string values are allowed, got '" + stringify(value) + "'"); + } } }; - StringMapWrapper.set(ariaSettersCache, attrName, setterFn); + StringMapWrapper.set(attributeSettersCache, attrName, setterFn); } return setterFn; @@ -82,21 +93,9 @@ function styleSetterFactory(styleName:string, stylesuffix:string) { return setterFn; } -const ROLE_ATTR = 'role'; -function roleSetter(element, value) { - if (isString(value)) { - DOM.setAttribute(element, ROLE_ATTR, value); - } else { - DOM.removeAttribute(element, ROLE_ATTR); - if (isPresent(value)) { - throw new BaseException("Invalid role attribute, only string values are allowed, got '" + stringify(value) + "'"); - } - } -} - // tells if an attribute is handled by the ElementBinderBuilder step export function isSpecialProperty(propName:string) { - return StringWrapper.startsWith(propName, ARIA_PREFIX) + return StringWrapper.startsWith(propName, ATTRIBUTE_PREFIX) || StringWrapper.startsWith(propName, CLASS_PREFIX) || StringWrapper.startsWith(propName, STYLE_PREFIX) || StringMapWrapper.contains(DOM.attrToPropMap, propName); @@ -188,10 +187,8 @@ export class ElementBinderBuilder extends CompileStep { MapWrapper.forEach(compileElement.propertyBindings, (expression, property) => { var setterFn, styleParts, styleSuffix; - if (StringWrapper.startsWith(property, ARIA_PREFIX)) { - setterFn = ariaSetterFactory(property); - } else if (StringWrapper.equals(property, ROLE_ATTR)) { - setterFn = roleSetter; + 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)) { diff --git a/modules/angular2/test/core/compiler/integration_spec.js b/modules/angular2/test/core/compiler/integration_spec.js index 96cca00458..a8f3c61faa 100644 --- a/modules/angular2/test/core/compiler/integration_spec.js +++ b/modules/angular2/test/core/compiler/integration_spec.js @@ -116,7 +116,7 @@ export function main() { })); it('should consume binding to aria-* attributes', inject([AsyncTestCompleter], (async) => { - tplResolver.setTemplate(MyComp, new Template({inline: '
'})); + tplResolver.setTemplate(MyComp, new Template({inline: '
'})); compiler.compile(MyComp).then((pv) => { createView(pv); diff --git a/modules/angular2/test/core/compiler/pipeline/element_binder_builder_spec.js b/modules/angular2/test/core/compiler/pipeline/element_binder_builder_spec.js index 02b1524a50..dba449e5d5 100644 --- a/modules/angular2/test/core/compiler/pipeline/element_binder_builder_spec.js +++ b/modules/angular2/test/core/compiler/pipeline/element_binder_builder_spec.js @@ -218,7 +218,7 @@ export function main() { it('should bind to aria-* attributes when exp evaluates to strings', () => { var propertyBindings = MapWrapper.createFromStringMap({ - 'aria-label': 'prop1' + 'attr.aria-label': 'prop1' }); var pipeline = createPipeline({propertyBindings: propertyBindings}); var results = pipeline.process(el('
')); @@ -243,7 +243,7 @@ export function main() { it('should bind to aria-* attributes when exp evaluates to booleans', () => { var propertyBindings = MapWrapper.createFromStringMap({ - 'aria-busy': 'prop1' + 'attr.aria-busy': 'prop1' }); var pipeline = createPipeline({propertyBindings: propertyBindings}); var results = pipeline.process(el('
')); @@ -264,7 +264,7 @@ export function main() { it('should bind to ARIA role attribute', () => { var propertyBindings = MapWrapper.createFromStringMap({ - 'role': 'prop1' + 'attr.role': 'prop1' }); var pipeline = createPipeline({propertyBindings: propertyBindings}); var results = pipeline.process(el('
')); @@ -289,7 +289,7 @@ export function main() { it('should throw for a non-string ARIA role', () => { var propertyBindings = MapWrapper.createFromStringMap({ - 'role': 'prop1' + 'attr.role': 'prop1' }); var pipeline = createPipeline({propertyBindings: propertyBindings}); var results = pipeline.process(el('
')); @@ -303,6 +303,31 @@ export function main() { }).toThrowError("Invalid role attribute, only string values are allowed, got '1'"); }); + it('should bind to any attribute', () => { + var propertyBindings = MapWrapper.createFromStringMap({ + 'attr.foo-bar': 'prop1' + }); + var pipeline = createPipeline({propertyBindings: propertyBindings}); + var results = pipeline.process(el('
')); + var pv = results[0].inheritedProtoView; + + expect(pv.elementBinders[0].hasElementPropertyBindings).toBe(true); + + instantiateView(pv); + + evalContext.prop1 = 'baz'; + changeDetector.detectChanges(); + expect(DOM.getAttribute(view.nodes[0], 'foo-bar')).toEqual('baz'); + + evalContext.prop1 = 123; + changeDetector.detectChanges(); + expect(DOM.getAttribute(view.nodes[0], 'foo-bar')).toEqual('123'); + + evalContext.prop1 = null; + changeDetector.detectChanges(); + expect(DOM.getAttribute(view.nodes[0], 'foo-bar')).toBeNull(); + }); + it('should bind class with a dot', () => { var propertyBindings = MapWrapper.createFromStringMap({ 'class.bar': 'prop1',