Skip to content

Commit 58dd75a

Browse files
committed
feat(compiler): Add support for setting attributes to Component host element
Fixes angular#1008 Fixes angular#1009 Closes angular#1052
1 parent f995b07 commit 58dd75a

File tree

5 files changed

+256
-116
lines changed

5 files changed

+256
-116
lines changed

modules/angular2/src/core/compiler/element_injector.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {NgElement} from 'angular2/src/core/dom/element';
1010
import {Directive, onChange, onDestroy} from 'angular2/src/core/annotations/annotations';
1111
import {BindingPropagationConfig} from 'angular2/src/core/compiler/binding_propagation_config';
1212
import * as pclModule from 'angular2/src/core/compiler/private_component_location';
13-
import {reflector} from 'angular2/src/reflection/reflection';
13+
import {setterFactory} from './property_setter_factory';
1414

1515
var _MAX_DIRECTIVE_CONSTRUCTION_COUNTER = 10;
1616

@@ -527,7 +527,7 @@ export class ElementInjector extends TreeNode {
527527
_buildPropSetter(dep) {
528528
var ngElement = this._getPreBuiltObjectByKeyId(StaticKeys.instance().ngElementId);
529529
var domElement = ngElement.domElement;
530-
var setter = reflector.setter(dep.propSetterName);
530+
var setter = setterFactory(dep.propSetterName);
531531
return function(v) { setter(domElement, v) };
532532
}
533533

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

Lines changed: 4 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import {int, isPresent, isBlank, Type, BaseException, StringWrapper, RegExpWrapper, isString, stringify} from 'angular2/src/facade/lang';
2-
import {DOM} from 'angular2/src/dom/dom_adapter';
1+
import {int, isPresent, isBlank} from 'angular2/src/facade/lang';
32
import {ListWrapper, List, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
43

54
import {reflector} from 'angular2/src/reflection/reflection';
@@ -11,88 +10,8 @@ import {DirectiveMetadata} from '../directive_metadata';
1110
import {CompileStep} from './compile_step';
1211
import {CompileElement} from './compile_element';
1312
import {CompileControl} from './compile_control';
14-
import {dashCaseToCamelCase, camelCaseToDashCase} from './util';
15-
16-
var DOT_REGEXP = RegExpWrapper.create('\\.');
17-
18-
const ATTRIBUTE_PREFIX = 'attr.';
19-
var attributeSettersCache = StringMapWrapper.create();
20-
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;
32-
33-
if (isBlank(setterFn)) {
34-
dashCasedAttributeName = camelCaseToDashCase(attrName);
35-
setterFn = function(element, value) {
36-
if (_isValidAttributeValue(dashCasedAttributeName, value)) {
37-
DOM.setAttribute(element, dashCasedAttributeName, stringify(value));
38-
} else {
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-
}
43-
}
44-
};
45-
StringMapWrapper.set(attributeSettersCache, attrName, setterFn);
46-
}
47-
48-
return setterFn;
49-
}
50-
51-
const CLASS_PREFIX = 'class.';
52-
var classSettersCache = StringMapWrapper.create();
53-
54-
function classSetterFactory(className:string) {
55-
var setterFn = StringMapWrapper.get(classSettersCache, className);
56-
57-
if (isBlank(setterFn)) {
58-
setterFn = function(element, value) {
59-
if (value) {
60-
DOM.addClass(element, className);
61-
} else {
62-
DOM.removeClass(element, className);
63-
}
64-
};
65-
StringMapWrapper.set(classSettersCache, className, setterFn);
66-
}
67-
68-
return setterFn;
69-
}
70-
71-
const STYLE_PREFIX = 'style.';
72-
var styleSettersCache = StringMapWrapper.create();
73-
74-
function styleSetterFactory(styleName:string, stylesuffix:string) {
75-
var cacheKey = styleName + stylesuffix;
76-
var setterFn = StringMapWrapper.get(styleSettersCache, cacheKey);
77-
var dashCasedStyleName;
78-
79-
if (isBlank(setterFn)) {
80-
dashCasedStyleName = camelCaseToDashCase(styleName);
81-
setterFn = function(element, value) {
82-
var valAsStr;
83-
if (isPresent(value)) {
84-
valAsStr = stringify(value);
85-
DOM.setStyle(element, dashCasedStyleName, valAsStr + stylesuffix);
86-
} else {
87-
DOM.removeStyle(element, dashCasedStyleName);
88-
}
89-
};
90-
StringMapWrapper.set(classSettersCache, cacheKey, setterFn);
91-
}
92-
93-
return setterFn;
94-
}
95-
13+
import {dashCaseToCamelCase} from './util';
14+
import {setterFactory} from '../property_setter_factory'
9615

9716
/**
9817
* Creates the ElementBinders and adds watches to the
@@ -178,28 +97,7 @@ export class ElementBinderBuilder extends CompileStep {
17897

17998
_bindElementProperties(protoView, compileElement) {
18099
MapWrapper.forEach(compileElement.propertyBindings, (expression, property) => {
181-
var setterFn, styleParts, styleSuffix;
182-
183-
if (StringWrapper.startsWith(property, ATTRIBUTE_PREFIX)) {
184-
setterFn = attributeSetterFactory(StringWrapper.substring(property, ATTRIBUTE_PREFIX.length));
185-
} else if (StringWrapper.startsWith(property, CLASS_PREFIX)) {
186-
setterFn = classSetterFactory(StringWrapper.substring(property, CLASS_PREFIX.length));
187-
} else if (StringWrapper.startsWith(property, STYLE_PREFIX)) {
188-
styleParts = StringWrapper.split(property, DOT_REGEXP);
189-
styleSuffix = styleParts.length > 2 ? ListWrapper.get(styleParts, 2) : '';
190-
setterFn = styleSetterFactory(ListWrapper.get(styleParts, 1), styleSuffix);
191-
} else if (StringWrapper.equals(property, 'innerHtml')) {
192-
setterFn = (element, value) => DOM.setInnerHTML(element, value);
193-
} else {
194-
property = this._resolvePropertyName(property);
195-
var propertySetterFn = reflector.setter(property);
196-
setterFn = function(receiver, value) {
197-
if (DOM.hasProperty(receiver, property)) {
198-
return propertySetterFn(receiver, value);
199-
}
200-
}
201-
}
202-
100+
var setterFn = setterFactory(property);
203101
protoView.bindElementProperty(expression.ast, property, setterFn);
204102
});
205103
}
@@ -263,9 +161,4 @@ export class ElementBinderBuilder extends CompileStep {
263161
_splitBindConfig(bindConfig:string) {
264162
return ListWrapper.map(bindConfig.split('|'), (s) => s.trim());
265163
}
266-
267-
_resolvePropertyName(attrName:string) {
268-
var mappedPropName = StringMapWrapper.get(DOM.attrToPropMap, attrName);
269-
return isPresent(mappedPropName) ? mappedPropName : attrName;
270-
}
271164
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import {StringWrapper, RegExpWrapper, BaseException, isPresent, isBlank, isString, stringify} from 'angular2/src/facade/lang';
2+
import {ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
3+
import {DOM} from 'angular2/src/dom/dom_adapter';
4+
import {reflector} from 'angular2/src/reflection/reflection';
5+
6+
var DASH_CASE_REGEXP = RegExpWrapper.create('-([a-z])');
7+
var CAMEL_CASE_REGEXP = RegExpWrapper.create('([A-Z])');
8+
9+
export function dashCaseToCamelCase(input:string): string {
10+
return StringWrapper.replaceAllMapped(input, DASH_CASE_REGEXP, (m) => {
11+
return m[1].toUpperCase();
12+
});
13+
}
14+
15+
export function camelCaseToDashCase(input:string): string {
16+
return StringWrapper.replaceAllMapped(input, CAMEL_CASE_REGEXP, (m) => {
17+
return '-' + m[1].toLowerCase();
18+
});
19+
}
20+
21+
const STYLE_SEPARATOR = '.';
22+
var propertySettersCache = StringMapWrapper.create();
23+
var innerHTMLSetterCache;
24+
25+
export function setterFactory(property: string): Function {
26+
var setterFn, styleParts, styleSuffix;
27+
if (StringWrapper.startsWith(property, ATTRIBUTE_PREFIX)) {
28+
setterFn = attributeSetterFactory(StringWrapper.substring(property, ATTRIBUTE_PREFIX.length));
29+
} else if (StringWrapper.startsWith(property, CLASS_PREFIX)) {
30+
setterFn = classSetterFactory(StringWrapper.substring(property, CLASS_PREFIX.length));
31+
} else if (StringWrapper.startsWith(property, STYLE_PREFIX)) {
32+
styleParts = property.split(STYLE_SEPARATOR);
33+
styleSuffix = styleParts.length > 2 ? ListWrapper.get(styleParts, 2) : '';
34+
setterFn = styleSetterFactory(ListWrapper.get(styleParts, 1), styleSuffix);
35+
} else if (StringWrapper.equals(property, 'innerHtml')) {
36+
if (isBlank(innerHTMLSetterCache)) {
37+
innerHTMLSetterCache = (el, value) => DOM.setInnerHTML(el, value);
38+
}
39+
setterFn = innerHTMLSetterCache;
40+
} else {
41+
property = resolvePropertyName(property);
42+
setterFn = StringMapWrapper.get(propertySettersCache, property);
43+
if (isBlank(setterFn)) {
44+
var propertySetterFn = reflector.setter(property);
45+
setterFn = function(receiver, value) {
46+
if (DOM.hasProperty(receiver, property)) {
47+
return propertySetterFn(receiver, value);
48+
}
49+
}
50+
StringMapWrapper.set(propertySettersCache, property, setterFn);
51+
}
52+
}
53+
return setterFn;
54+
}
55+
56+
const ATTRIBUTE_PREFIX = 'attr.';
57+
var attributeSettersCache = StringMapWrapper.create();
58+
59+
function _isValidAttributeValue(attrName:string, value: any): boolean {
60+
if (attrName == "role") {
61+
return isString(value);
62+
} else {
63+
return isPresent(value);
64+
}
65+
}
66+
67+
function attributeSetterFactory(attrName:string): Function {
68+
var setterFn = StringMapWrapper.get(attributeSettersCache, attrName);
69+
var dashCasedAttributeName;
70+
71+
if (isBlank(setterFn)) {
72+
dashCasedAttributeName = camelCaseToDashCase(attrName);
73+
setterFn = function(element, value) {
74+
if (_isValidAttributeValue(dashCasedAttributeName, value)) {
75+
DOM.setAttribute(element, dashCasedAttributeName, stringify(value));
76+
} else {
77+
if (isPresent(value)) {
78+
throw new BaseException("Invalid " + dashCasedAttributeName +
79+
" attribute, only string values are allowed, got '" + stringify(value) + "'");
80+
}
81+
DOM.removeAttribute(element, dashCasedAttributeName);
82+
}
83+
};
84+
StringMapWrapper.set(attributeSettersCache, attrName, setterFn);
85+
}
86+
87+
return setterFn;
88+
}
89+
90+
const CLASS_PREFIX = 'class.';
91+
var classSettersCache = StringMapWrapper.create();
92+
93+
function classSetterFactory(className:string): Function {
94+
var setterFn = StringMapWrapper.get(classSettersCache, className);
95+
96+
if (isBlank(setterFn)) {
97+
setterFn = function(element, value) {
98+
if (value) {
99+
DOM.addClass(element, className);
100+
} else {
101+
DOM.removeClass(element, className);
102+
}
103+
};
104+
StringMapWrapper.set(classSettersCache, className, setterFn);
105+
}
106+
107+
return setterFn;
108+
}
109+
110+
const STYLE_PREFIX = 'style.';
111+
var styleSettersCache = StringMapWrapper.create();
112+
113+
function styleSetterFactory(styleName:string, styleSuffix:string): Function {
114+
var cacheKey = styleName + styleSuffix;
115+
var setterFn = StringMapWrapper.get(styleSettersCache, cacheKey);
116+
var dashCasedStyleName;
117+
118+
if (isBlank(setterFn)) {
119+
dashCasedStyleName = camelCaseToDashCase(styleName);
120+
setterFn = function(element, value) {
121+
var valAsStr;
122+
if (isPresent(value)) {
123+
valAsStr = stringify(value);
124+
DOM.setStyle(element, dashCasedStyleName, valAsStr + styleSuffix);
125+
} else {
126+
DOM.removeStyle(element, dashCasedStyleName);
127+
}
128+
};
129+
StringMapWrapper.set(styleSettersCache, cacheKey, setterFn);
130+
}
131+
132+
return setterFn;
133+
}
134+
135+
function resolvePropertyName(attrName:string): string {
136+
var mappedPropName = StringMapWrapper.get(DOM.attrToPropMap, attrName);
137+
return isPresent(mappedPropName) ? mappedPropName : attrName;
138+
}

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

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {describe, ddescribe, it, iit, xit, xdescribe, expect, beforeEach, SpyObject, proxy, el} from 'angular2/test_lib';
22
import {isBlank, isPresent, IMPLEMENTS} from 'angular2/src/facade/lang';
33
import {ListWrapper, MapWrapper, List, StringMapWrapper} from 'angular2/src/facade/collection';
4+
import {DOM} from 'angular2/src/dom/dom_adapter';
45
import {ProtoElementInjector, PreBuiltObjects, DirectiveBinding} from 'angular2/src/core/compiler/element_injector';
56
import {Parent, Ancestor} from 'angular2/src/core/annotations/visibility';
67
import {EventEmitter, PropertySetter} from 'angular2/src/core/annotations/di';
@@ -76,13 +77,34 @@ class NeedsEventEmitter {
7677

7778
class NeedsPropertySetter {
7879
propSetter;
79-
constructor(@PropertySetter('title') propSetter: Function) {
80+
roleSetter;
81+
classSetter;
82+
styleSetter;
83+
unitSetter;
84+
constructor(@PropertySetter('title') propSetter: Function, @PropertySetter('attr.role') roleSetter: Function,
85+
@PropertySetter('class.active') classSetter: Function, @PropertySetter('style.width') styleSetter: Function,
86+
@PropertySetter('style.height.px') unitSetter: Function) {
8087
this.propSetter = propSetter;
88+
this.roleSetter = roleSetter;
89+
this.classSetter = classSetter;
90+
this.styleSetter = styleSetter;
91+
this.unitSetter = unitSetter;
8192
}
82-
8393
setProp(value) {
8494
this.propSetter(value);
8595
}
96+
setRole(value) {
97+
this.roleSetter(value);
98+
}
99+
setClass(value) {
100+
this.classSetter(value);
101+
}
102+
setStyle(value) {
103+
this.styleSetter(value);
104+
}
105+
setStyleWithUnit(value) {
106+
this.unitSetter(value);
107+
}
86108
}
87109

88110
class A_Needs_B {
@@ -529,9 +551,18 @@ export function main() {
529551

530552
var preBuildObject = new PreBuiltObjects(null, ngElement, null, null);
531553
var inj = injector([NeedsPropertySetter], null, null, preBuildObject);
532-
inj.get(NeedsPropertySetter).setProp('foobar');
554+
var component = inj.get(NeedsPropertySetter);
555+
component.setProp('foobar');
556+
component.setRole('button');
557+
component.setClass(true);
558+
component.setStyle('40px')
559+
component.setStyleWithUnit(50);
533560

534561
expect(div.title).toEqual('foobar');
562+
expect(DOM.getAttribute(div, 'role')).toEqual('button');
563+
expect(DOM.hasClass(div, 'active')).toEqual(true);
564+
expect(DOM.getStyle(div, 'width')).toEqual('40px');
565+
expect(DOM.getStyle(div, 'height')).toEqual('50px');
535566
});
536567
});
537568

0 commit comments

Comments
 (0)