feat(compiler): Add support for setting attributes to Component host element

Fixes #1008
Fixes #1009
Closes #1052
This commit is contained in:
Marc Laval 2015-03-23 14:13:32 +01:00
parent f995b07876
commit 58dd75a1c8
5 changed files with 256 additions and 116 deletions

View File

@ -10,7 +10,7 @@ import {NgElement} from 'angular2/src/core/dom/element';
import {Directive, onChange, onDestroy} from 'angular2/src/core/annotations/annotations';
import {BindingPropagationConfig} from 'angular2/src/core/compiler/binding_propagation_config';
import * as pclModule from 'angular2/src/core/compiler/private_component_location';
import {reflector} from 'angular2/src/reflection/reflection';
import {setterFactory} from './property_setter_factory';
var _MAX_DIRECTIVE_CONSTRUCTION_COUNTER = 10;
@ -527,7 +527,7 @@ export class ElementInjector extends TreeNode {
_buildPropSetter(dep) {
var ngElement = this._getPreBuiltObjectByKeyId(StaticKeys.instance().ngElementId);
var domElement = ngElement.domElement;
var setter = reflector.setter(dep.propSetterName);
var setter = setterFactory(dep.propSetterName);
return function(v) { setter(domElement, v) };
}

View File

@ -1,5 +1,4 @@
import {int, isPresent, isBlank, Type, BaseException, StringWrapper, RegExpWrapper, isString, stringify} from 'angular2/src/facade/lang';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {int, isPresent, isBlank} from 'angular2/src/facade/lang';
import {ListWrapper, List, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
import {reflector} from 'angular2/src/reflection/reflection';
@ -11,88 +10,8 @@ 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 ATTRIBUTE_PREFIX = 'attr.';
var attributeSettersCache = StringMapWrapper.create();
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)) {
dashCasedAttributeName = camelCaseToDashCase(attrName);
setterFn = function(element, value) {
if (_isValidAttributeValue(dashCasedAttributeName, value)) {
DOM.setAttribute(element, dashCasedAttributeName, stringify(value));
} else {
DOM.removeAttribute(element, dashCasedAttributeName);
if (isPresent(value)) {
throw new BaseException("Invalid " + dashCasedAttributeName + " attribute, only string values are allowed, got '" + stringify(value) + "'");
}
}
};
StringMapWrapper.set(attributeSettersCache, attrName, setterFn);
}
return setterFn;
}
const CLASS_PREFIX = 'class.';
var classSettersCache = StringMapWrapper.create();
function classSetterFactory(className:string) {
var setterFn = StringMapWrapper.get(classSettersCache, className);
if (isBlank(setterFn)) {
setterFn = function(element, value) {
if (value) {
DOM.addClass(element, className);
} else {
DOM.removeClass(element, className);
}
};
StringMapWrapper.set(classSettersCache, className, setterFn);
}
return setterFn;
}
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, dashCasedStyleName, valAsStr + stylesuffix);
} else {
DOM.removeStyle(element, dashCasedStyleName);
}
};
StringMapWrapper.set(classSettersCache, cacheKey, setterFn);
}
return setterFn;
}
import {dashCaseToCamelCase} from './util';
import {setterFactory} from '../property_setter_factory'
/**
* Creates the ElementBinders and adds watches to the
@ -178,28 +97,7 @@ export class ElementBinderBuilder extends CompileStep {
_bindElementProperties(protoView, compileElement) {
MapWrapper.forEach(compileElement.propertyBindings, (expression, property) => {
var setterFn, styleParts, styleSuffix;
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)) {
styleParts = StringWrapper.split(property, DOT_REGEXP);
styleSuffix = styleParts.length > 2 ? ListWrapper.get(styleParts, 2) : '';
setterFn = styleSetterFactory(ListWrapper.get(styleParts, 1), styleSuffix);
} else if (StringWrapper.equals(property, 'innerHtml')) {
setterFn = (element, value) => DOM.setInnerHTML(element, value);
} else {
property = this._resolvePropertyName(property);
var propertySetterFn = reflector.setter(property);
setterFn = function(receiver, value) {
if (DOM.hasProperty(receiver, property)) {
return propertySetterFn(receiver, value);
}
}
}
var setterFn = setterFactory(property);
protoView.bindElementProperty(expression.ast, property, setterFn);
});
}
@ -263,9 +161,4 @@ export class ElementBinderBuilder extends CompileStep {
_splitBindConfig(bindConfig:string) {
return ListWrapper.map(bindConfig.split('|'), (s) => s.trim());
}
_resolvePropertyName(attrName:string) {
var mappedPropName = StringMapWrapper.get(DOM.attrToPropMap, attrName);
return isPresent(mappedPropName) ? mappedPropName : attrName;
}
}

View File

@ -0,0 +1,138 @@
import {StringWrapper, RegExpWrapper, BaseException, isPresent, isBlank, isString, stringify} from 'angular2/src/facade/lang';
import {ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {reflector} from 'angular2/src/reflection/reflection';
var DASH_CASE_REGEXP = RegExpWrapper.create('-([a-z])');
var CAMEL_CASE_REGEXP = RegExpWrapper.create('([A-Z])');
export function dashCaseToCamelCase(input:string): string {
return StringWrapper.replaceAllMapped(input, DASH_CASE_REGEXP, (m) => {
return m[1].toUpperCase();
});
}
export function camelCaseToDashCase(input:string): string {
return StringWrapper.replaceAllMapped(input, CAMEL_CASE_REGEXP, (m) => {
return '-' + m[1].toLowerCase();
});
}
const STYLE_SEPARATOR = '.';
var propertySettersCache = StringMapWrapper.create();
var innerHTMLSetterCache;
export function setterFactory(property: string): Function {
var setterFn, styleParts, styleSuffix;
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)) {
styleParts = property.split(STYLE_SEPARATOR);
styleSuffix = styleParts.length > 2 ? ListWrapper.get(styleParts, 2) : '';
setterFn = styleSetterFactory(ListWrapper.get(styleParts, 1), styleSuffix);
} else if (StringWrapper.equals(property, 'innerHtml')) {
if (isBlank(innerHTMLSetterCache)) {
innerHTMLSetterCache = (el, value) => DOM.setInnerHTML(el, value);
}
setterFn = innerHTMLSetterCache;
} else {
property = resolvePropertyName(property);
setterFn = StringMapWrapper.get(propertySettersCache, property);
if (isBlank(setterFn)) {
var propertySetterFn = reflector.setter(property);
setterFn = function(receiver, value) {
if (DOM.hasProperty(receiver, property)) {
return propertySetterFn(receiver, value);
}
}
StringMapWrapper.set(propertySettersCache, property, setterFn);
}
}
return setterFn;
}
const ATTRIBUTE_PREFIX = 'attr.';
var attributeSettersCache = StringMapWrapper.create();
function _isValidAttributeValue(attrName:string, value: any): boolean {
if (attrName == "role") {
return isString(value);
} else {
return isPresent(value);
}
}
function attributeSetterFactory(attrName:string): Function {
var setterFn = StringMapWrapper.get(attributeSettersCache, attrName);
var dashCasedAttributeName;
if (isBlank(setterFn)) {
dashCasedAttributeName = camelCaseToDashCase(attrName);
setterFn = function(element, value) {
if (_isValidAttributeValue(dashCasedAttributeName, value)) {
DOM.setAttribute(element, dashCasedAttributeName, stringify(value));
} else {
if (isPresent(value)) {
throw new BaseException("Invalid " + dashCasedAttributeName +
" attribute, only string values are allowed, got '" + stringify(value) + "'");
}
DOM.removeAttribute(element, dashCasedAttributeName);
}
};
StringMapWrapper.set(attributeSettersCache, attrName, setterFn);
}
return setterFn;
}
const CLASS_PREFIX = 'class.';
var classSettersCache = StringMapWrapper.create();
function classSetterFactory(className:string): Function {
var setterFn = StringMapWrapper.get(classSettersCache, className);
if (isBlank(setterFn)) {
setterFn = function(element, value) {
if (value) {
DOM.addClass(element, className);
} else {
DOM.removeClass(element, className);
}
};
StringMapWrapper.set(classSettersCache, className, setterFn);
}
return setterFn;
}
const STYLE_PREFIX = 'style.';
var styleSettersCache = StringMapWrapper.create();
function styleSetterFactory(styleName:string, styleSuffix:string): Function {
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, dashCasedStyleName, valAsStr + styleSuffix);
} else {
DOM.removeStyle(element, dashCasedStyleName);
}
};
StringMapWrapper.set(styleSettersCache, cacheKey, setterFn);
}
return setterFn;
}
function resolvePropertyName(attrName:string): string {
var mappedPropName = StringMapWrapper.get(DOM.attrToPropMap, attrName);
return isPresent(mappedPropName) ? mappedPropName : attrName;
}

View File

@ -1,6 +1,7 @@
import {describe, ddescribe, it, iit, xit, xdescribe, expect, beforeEach, SpyObject, proxy, el} from 'angular2/test_lib';
import {isBlank, isPresent, IMPLEMENTS} from 'angular2/src/facade/lang';
import {ListWrapper, MapWrapper, List, StringMapWrapper} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {ProtoElementInjector, PreBuiltObjects, DirectiveBinding} from 'angular2/src/core/compiler/element_injector';
import {Parent, Ancestor} from 'angular2/src/core/annotations/visibility';
import {EventEmitter, PropertySetter} from 'angular2/src/core/annotations/di';
@ -76,13 +77,34 @@ class NeedsEventEmitter {
class NeedsPropertySetter {
propSetter;
constructor(@PropertySetter('title') propSetter: Function) {
roleSetter;
classSetter;
styleSetter;
unitSetter;
constructor(@PropertySetter('title') propSetter: Function, @PropertySetter('attr.role') roleSetter: Function,
@PropertySetter('class.active') classSetter: Function, @PropertySetter('style.width') styleSetter: Function,
@PropertySetter('style.height.px') unitSetter: Function) {
this.propSetter = propSetter;
this.roleSetter = roleSetter;
this.classSetter = classSetter;
this.styleSetter = styleSetter;
this.unitSetter = unitSetter;
}
setProp(value) {
this.propSetter(value);
}
setRole(value) {
this.roleSetter(value);
}
setClass(value) {
this.classSetter(value);
}
setStyle(value) {
this.styleSetter(value);
}
setStyleWithUnit(value) {
this.unitSetter(value);
}
}
class A_Needs_B {
@ -529,9 +551,18 @@ export function main() {
var preBuildObject = new PreBuiltObjects(null, ngElement, null, null);
var inj = injector([NeedsPropertySetter], null, null, preBuildObject);
inj.get(NeedsPropertySetter).setProp('foobar');
var component = inj.get(NeedsPropertySetter);
component.setProp('foobar');
component.setRole('button');
component.setClass(true);
component.setStyle('40px')
component.setStyleWithUnit(50);
expect(div.title).toEqual('foobar');
expect(DOM.getAttribute(div, 'role')).toEqual('button');
expect(DOM.hasClass(div, 'active')).toEqual(true);
expect(DOM.getStyle(div, 'width')).toEqual('40px');
expect(DOM.getStyle(div, 'height')).toEqual('50px');
});
});

View File

@ -0,0 +1,78 @@
import {describe, ddescribe, it, iit, xit, xdescribe, expect, beforeEach, el} from 'angular2/test_lib';
import {setterFactory} from 'angular2/src/core/compiler/property_setter_factory';
import {DOM} from 'angular2/src/dom/dom_adapter';
export function main() {
var div;
beforeEach( () => {
div = el('<div></div>');
});
describe('property setter factory', () => {
it('should return a setter for a property', () => {
var setterFn = setterFactory('title');
setterFn(div, 'Hello');
expect(div.title).toEqual('Hello');
var otherSetterFn = setterFactory('title');
expect(setterFn).toBe(otherSetterFn);
});
it('should return a setter for an attribute', () => {
var setterFn = setterFactory('attr.role');
setterFn(div, 'button');
expect(DOM.getAttribute(div, 'role')).toEqual('button');
setterFn(div, null);
expect(DOM.getAttribute(div, 'role')).toEqual(null);
expect(() => {
setterFn(div, 4);
}).toThrowError("Invalid role attribute, only string values are allowed, got '4'");
var otherSetterFn = setterFactory('attr.role');
expect(setterFn).toBe(otherSetterFn);
});
it('should return a setter for a class', () => {
var setterFn = setterFactory('class.active');
setterFn(div, true);
expect(DOM.hasClass(div, 'active')).toEqual(true);
setterFn(div, false);
expect(DOM.hasClass(div, 'active')).toEqual(false);
var otherSetterFn = setterFactory('class.active');
expect(setterFn).toBe(otherSetterFn);
});
it('should return a setter for a style', () => {
var setterFn = setterFactory('style.width');
setterFn(div, '40px');
expect(DOM.getStyle(div, 'width')).toEqual('40px');
setterFn(div, null);
expect(DOM.getStyle(div, 'width')).toEqual('');
var otherSetterFn = setterFactory('style.width');
expect(setterFn).toBe(otherSetterFn);
});
it('should return a setter for a style with a unit', () => {
var setterFn = setterFactory('style.height.px');
setterFn(div, 40);
expect(DOM.getStyle(div, 'height')).toEqual('40px');
setterFn(div, null);
expect(DOM.getStyle(div, 'height')).toEqual('');
var otherSetterFn = setterFactory('style.height.px');
expect(setterFn).toBe(otherSetterFn);
});
it('should return a setter for innerHtml', () => {
var setterFn = setterFactory('innerHtml');
setterFn(div, '<span></span>');
expect(DOM.getInnerHTML(div)).toEqual('<span></span>');
var otherSetterFn = setterFactory('innerHtml');
expect(setterFn).toBe(otherSetterFn);
});
});
}