Skip to content

Commit 7e6f536

Browse files
fix(compiler): properly bind to properties that don't have matching attr name
Fixes angular#619 Closes angular#783
1 parent e490861 commit 7e6f536

File tree

5 files changed

+91
-8
lines changed

5 files changed

+91
-8
lines changed

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

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {int, isPresent, isBlank, Type, BaseException, StringWrapper, RegExpWrapper, isString, stringify} from 'angular2/src/facade/lang';
2-
import {Element, DOM} from 'angular2/src/facade/dom';
2+
import {Element, DOM, attrToPropMap} from 'angular2/src/facade/dom';
33
import {ListWrapper, List, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
44

55
import {reflector} from 'angular2/src/reflection/reflection';
@@ -91,11 +91,19 @@ function roleSetter(element:Element, value) {
9191
}
9292
}
9393

94+
// special mapping for cases where attribute name doesn't match property name
95+
var attrToProp = StringMapWrapper.merge({
96+
"inner-html": "innerHTML",
97+
"readonly": "readOnly",
98+
"tabindex": "tabIndex",
99+
}, attrToPropMap);
100+
94101
// tells if an attribute is handled by the ElementBinderBuilder step
95102
export function isSpecialProperty(propName:string) {
96-
return StringWrapper.startsWith(propName, ARIA_PREFIX)
97-
|| StringWrapper.startsWith(propName, CLASS_PREFIX)
98-
|| StringWrapper.startsWith(propName, STYLE_PREFIX);
103+
return StringWrapper.startsWith(propName, ARIA_PREFIX)
104+
|| StringWrapper.startsWith(propName, CLASS_PREFIX)
105+
|| StringWrapper.startsWith(propName, STYLE_PREFIX)
106+
|| StringMapWrapper.contains(attrToProp, propName);
99107
}
100108

101109
/**
@@ -176,10 +184,14 @@ export class ElementBinderBuilder extends CompileStep {
176184
setterFn = classSetterFactory(StringWrapper.substring(property, CLASS_PREFIX.length));
177185
} else if (StringWrapper.startsWith(property, STYLE_PREFIX)) {
178186
styleParts = StringWrapper.split(property, DOT_REGEXP);
179-
styleSuffix = styleParts.length > 2 ? ListWrapper.get(styleParts, 2) : '';
187+
styleSuffix = styleParts.length > 2 ? ListWrapper.get(styleParts, 2) : '';
180188
setterFn = styleSetterFactory(ListWrapper.get(styleParts, 1), styleSuffix);
181-
} else if (DOM.hasProperty(compileElement.element, property)) {
182-
setterFn = reflector.setter(property);
189+
} else {
190+
property = this._resolvePropertyName(property);
191+
//TODO(pk): special casing innerHtml, see: https://github.com/angular/angular/issues/789
192+
if (DOM.hasProperty(compileElement.element, property) || StringWrapper.equals(property, 'innerHtml')) {
193+
setterFn = reflector.setter(property);
194+
}
183195
}
184196

185197
if (isPresent(setterFn)) {
@@ -236,4 +248,9 @@ export class ElementBinderBuilder extends CompileStep {
236248
var parts = StringWrapper.split(bindConfig, RegExpWrapper.create("\\|"));
237249
return ListWrapper.map(parts, (s) => s.trim());
238250
}
251+
252+
_resolvePropertyName(attrName:string) {
253+
var mappedPropName = StringMapWrapper.get(attrToProp, attrName);
254+
return isPresent(mappedPropName) ? mappedPropName : attrName;
255+
}
239256
}

modules/angular2/src/facade/dom.dart

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ export 'dart:html' show
1717
InputElement,
1818
AnchorElement,
1919
Text,
20-
window;
20+
window,
21+
attrToPropMap;
2122

2223
// TODO(tbosch): Is there a builtin one? Why is Dart
2324
// removing unknown elements by default?
@@ -36,6 +37,11 @@ void gc() {
3637

3738
final identitySanitizer = new IdentitySanitizer();
3839

40+
// override JS logic of attribute to property mapping
41+
var attrToPropMap = {
42+
"inner-html": "innerHtml"
43+
};
44+
3945
class DOM {
4046
static query(String selector) => document.querySelector(selector);
4147

modules/angular2/src/facade/dom.es6

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ export var gc = window.gc ? () => window.gc() : () => null;
1616
export var CssRule = window.CSSRule;
1717
export var CssKeyframesRule = window.CSSKeyframesRule;
1818

19+
export var attrToPropMap = {};
20+
1921
export class DOM {
2022
static query(selector) {
2123
return document.querySelector(selector);

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,41 @@ export function main() {
102102
});
103103
});
104104

105+
it('should consume binding to propery names where attr name and property name do not match', (done) => {
106+
tplResolver.setTemplate(MyComp, new Template({inline: '<div [tabindex]="ctxNumProp"></div>'}));
107+
108+
compiler.compile(MyComp).then((pv) => {
109+
createView(pv);
110+
111+
cd.detectChanges();
112+
expect(view.nodes[0].tabIndex).toEqual(0);
113+
114+
ctx.ctxNumProp = 5;
115+
cd.detectChanges();
116+
expect(view.nodes[0].tabIndex).toEqual(5);
117+
118+
done();
119+
});
120+
});
121+
122+
it('should consume binding to inner-html', (done) => {
123+
tplResolver.setTemplate(MyComp, new Template({inline: '<div inner-html="{{ctxProp}}"></div>'}));
124+
125+
compiler.compile(MyComp).then((pv) => {
126+
createView(pv);
127+
128+
ctx.ctxProp = 'Some <span>HTML</span>';
129+
cd.detectChanges();
130+
expect(DOM.getInnerHTML(view.nodes[0])).toEqual('Some <span>HTML</span>');
131+
132+
ctx.ctxProp = 'Some other <div>HTML</div>';
133+
cd.detectChanges();
134+
expect(DOM.getInnerHTML(view.nodes[0])).toEqual('Some other <div>HTML</div>');
135+
136+
done();
137+
});
138+
});
139+
105140
it('should consume directive watch expression change.', (done) => {
106141
var tpl =
107142
'<div>' +
@@ -490,8 +525,10 @@ class PushBasedComp {
490525
@Component()
491526
class MyComp {
492527
ctxProp:string;
528+
ctxNumProp;
493529
constructor() {
494530
this.ctxProp = 'initial value';
531+
this.ctxNumProp = 0;
495532
}
496533
}
497534

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,27 @@ export function main() {
196196
expect(view.nodes[0].hidden).toEqual(false);
197197
});
198198

199+
it('should bind element properties where attr name and prop name do not match', () => {
200+
var propertyBindings = MapWrapper.createFromStringMap({
201+
'tabindex': 'prop1'
202+
});
203+
var pipeline = createPipeline({propertyBindings: propertyBindings});
204+
var results = pipeline.process(el('<div viewroot prop-binding></div>'));
205+
var pv = results[0].inheritedProtoView;
206+
207+
expect(pv.elementBinders[0].hasElementPropertyBindings).toBe(true);
208+
209+
instantiateView(pv);
210+
211+
evalContext.prop1 = 1;
212+
changeDetector.detectChanges();
213+
expect(view.nodes[0].tabIndex).toEqual(1);
214+
215+
evalContext.prop1 = 0;
216+
changeDetector.detectChanges();
217+
expect(view.nodes[0].tabIndex).toEqual(0);
218+
});
219+
199220
it('should bind to aria-* attributes when exp evaluates to strings', () => {
200221
var propertyBindings = MapWrapper.createFromStringMap({
201222
'aria-label': 'prop1'

0 commit comments

Comments
 (0)