Skip to content

Commit 02aa8e7

Browse files
marclavalmhevery
authored andcommitted
feat(compiler): support bindings for any attribute
Closes angular#1029
1 parent ee523ef commit 02aa8e7

File tree

3 files changed

+54
-32
lines changed

3 files changed

+54
-32
lines changed

modules/angular2/src/core/compiler/pipeline/element_binder_builder.js

Lines changed: 24 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,34 @@ import {dashCaseToCamelCase, camelCaseToDashCase} from './util';
1515

1616
var DOT_REGEXP = RegExpWrapper.create('\\.');
1717

18-
const ARIA_PREFIX = 'aria';
19-
var ariaSettersCache = StringMapWrapper.create();
18+
const ATTRIBUTE_PREFIX = 'attr.';
19+
var attributeSettersCache = StringMapWrapper.create();
2020

21-
function ariaSetterFactory(attrName:string) {
22-
var setterFn = StringMapWrapper.get(ariaSettersCache, attrName);
23-
var ariaAttrName;
21+
function _isValidAttributeValue(attrName:string, value: any) {
22+
if (attrName == "role") {
23+
return isString(value);
24+
} else {
25+
return isPresent(value);
26+
}
27+
}
28+
29+
function attributeSetterFactory(attrName:string) {
30+
var setterFn = StringMapWrapper.get(attributeSettersCache, attrName);
31+
var dashCasedAttributeName;
2432

2533
if (isBlank(setterFn)) {
26-
ariaAttrName = camelCaseToDashCase(attrName);
34+
dashCasedAttributeName = camelCaseToDashCase(attrName);
2735
setterFn = function(element, value) {
28-
if (isPresent(value)) {
29-
DOM.setAttribute(element, ariaAttrName, stringify(value));
36+
if (_isValidAttributeValue(dashCasedAttributeName, value)) {
37+
DOM.setAttribute(element, dashCasedAttributeName, stringify(value));
3038
} else {
31-
DOM.removeAttribute(element, ariaAttrName);
39+
DOM.removeAttribute(element, dashCasedAttributeName);
40+
if (isPresent(value)) {
41+
throw new BaseException("Invalid " + dashCasedAttributeName + " attribute, only string values are allowed, got '" + stringify(value) + "'");
42+
}
3243
}
3344
};
34-
StringMapWrapper.set(ariaSettersCache, attrName, setterFn);
45+
StringMapWrapper.set(attributeSettersCache, attrName, setterFn);
3546
}
3647

3748
return setterFn;
@@ -82,21 +93,9 @@ function styleSetterFactory(styleName:string, stylesuffix:string) {
8293
return setterFn;
8394
}
8495

85-
const ROLE_ATTR = 'role';
86-
function roleSetter(element, value) {
87-
if (isString(value)) {
88-
DOM.setAttribute(element, ROLE_ATTR, value);
89-
} else {
90-
DOM.removeAttribute(element, ROLE_ATTR);
91-
if (isPresent(value)) {
92-
throw new BaseException("Invalid role attribute, only string values are allowed, got '" + stringify(value) + "'");
93-
}
94-
}
95-
}
96-
9796
// tells if an attribute is handled by the ElementBinderBuilder step
9897
export function isSpecialProperty(propName:string) {
99-
return StringWrapper.startsWith(propName, ARIA_PREFIX)
98+
return StringWrapper.startsWith(propName, ATTRIBUTE_PREFIX)
10099
|| StringWrapper.startsWith(propName, CLASS_PREFIX)
101100
|| StringWrapper.startsWith(propName, STYLE_PREFIX)
102101
|| StringMapWrapper.contains(DOM.attrToPropMap, propName);
@@ -188,10 +187,8 @@ export class ElementBinderBuilder extends CompileStep {
188187
MapWrapper.forEach(compileElement.propertyBindings, (expression, property) => {
189188
var setterFn, styleParts, styleSuffix;
190189

191-
if (StringWrapper.startsWith(property, ARIA_PREFIX)) {
192-
setterFn = ariaSetterFactory(property);
193-
} else if (StringWrapper.equals(property, ROLE_ATTR)) {
194-
setterFn = roleSetter;
190+
if (StringWrapper.startsWith(property, ATTRIBUTE_PREFIX)) {
191+
setterFn = attributeSetterFactory(StringWrapper.substring(property, ATTRIBUTE_PREFIX.length));
195192
} else if (StringWrapper.startsWith(property, CLASS_PREFIX)) {
196193
setterFn = classSetterFactory(StringWrapper.substring(property, CLASS_PREFIX.length));
197194
} else if (StringWrapper.startsWith(property, STYLE_PREFIX)) {

modules/angular2/test/core/compiler/integration_spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export function main() {
116116
}));
117117

118118
it('should consume binding to aria-* attributes', inject([AsyncTestCompleter], (async) => {
119-
tplResolver.setTemplate(MyComp, new Template({inline: '<div [aria-label]="ctxProp"></div>'}));
119+
tplResolver.setTemplate(MyComp, new Template({inline: '<div [attr.aria-label]="ctxProp"></div>'}));
120120

121121
compiler.compile(MyComp).then((pv) => {
122122
createView(pv);

modules/angular2/test/core/compiler/pipeline/element_binder_builder_spec.js

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ export function main() {
218218

219219
it('should bind to aria-* attributes when exp evaluates to strings', () => {
220220
var propertyBindings = MapWrapper.createFromStringMap({
221-
'aria-label': 'prop1'
221+
'attr.aria-label': 'prop1'
222222
});
223223
var pipeline = createPipeline({propertyBindings: propertyBindings});
224224
var results = pipeline.process(el('<div viewroot prop-binding></div>'));
@@ -243,7 +243,7 @@ export function main() {
243243

244244
it('should bind to aria-* attributes when exp evaluates to booleans', () => {
245245
var propertyBindings = MapWrapper.createFromStringMap({
246-
'aria-busy': 'prop1'
246+
'attr.aria-busy': 'prop1'
247247
});
248248
var pipeline = createPipeline({propertyBindings: propertyBindings});
249249
var results = pipeline.process(el('<div viewroot prop-binding></div>'));
@@ -264,7 +264,7 @@ export function main() {
264264

265265
it('should bind to ARIA role attribute', () => {
266266
var propertyBindings = MapWrapper.createFromStringMap({
267-
'role': 'prop1'
267+
'attr.role': 'prop1'
268268
});
269269
var pipeline = createPipeline({propertyBindings: propertyBindings});
270270
var results = pipeline.process(el('<div viewroot prop-binding></div>'));
@@ -289,7 +289,7 @@ export function main() {
289289

290290
it('should throw for a non-string ARIA role', () => {
291291
var propertyBindings = MapWrapper.createFromStringMap({
292-
'role': 'prop1'
292+
'attr.role': 'prop1'
293293
});
294294
var pipeline = createPipeline({propertyBindings: propertyBindings});
295295
var results = pipeline.process(el('<div viewroot prop-binding></div>'));
@@ -303,6 +303,31 @@ export function main() {
303303
}).toThrowError("Invalid role attribute, only string values are allowed, got '1'");
304304
});
305305

306+
it('should bind to any attribute', () => {
307+
var propertyBindings = MapWrapper.createFromStringMap({
308+
'attr.foo-bar': 'prop1'
309+
});
310+
var pipeline = createPipeline({propertyBindings: propertyBindings});
311+
var results = pipeline.process(el('<div viewroot prop-binding></div>'));
312+
var pv = results[0].inheritedProtoView;
313+
314+
expect(pv.elementBinders[0].hasElementPropertyBindings).toBe(true);
315+
316+
instantiateView(pv);
317+
318+
evalContext.prop1 = 'baz';
319+
changeDetector.detectChanges();
320+
expect(DOM.getAttribute(view.nodes[0], 'foo-bar')).toEqual('baz');
321+
322+
evalContext.prop1 = 123;
323+
changeDetector.detectChanges();
324+
expect(DOM.getAttribute(view.nodes[0], 'foo-bar')).toEqual('123');
325+
326+
evalContext.prop1 = null;
327+
changeDetector.detectChanges();
328+
expect(DOM.getAttribute(view.nodes[0], 'foo-bar')).toBeNull();
329+
});
330+
306331
it('should bind class with a dot', () => {
307332
var propertyBindings = MapWrapper.createFromStringMap({
308333
'class.bar': 'prop1',

0 commit comments

Comments
 (0)