From 58dd75a1c8abfa8396ead9b2b1c95dc6c6558668 Mon Sep 17 00:00:00 2001 From: Marc Laval Date: Mon, 23 Mar 2015 14:13:32 +0100 Subject: [PATCH] feat(compiler): Add support for setting attributes to Component host element Fixes #1008 Fixes #1009 Closes #1052 --- .../src/core/compiler/element_injector.js | 4 +- .../pipeline/element_binder_builder.js | 115 +-------------- .../core/compiler/property_setter_factory.js | 138 ++++++++++++++++++ .../core/compiler/element_injector_spec.js | 37 ++++- .../compiler/property_setter_factory_spec.js | 78 ++++++++++ 5 files changed, 256 insertions(+), 116 deletions(-) create mode 100644 modules/angular2/src/core/compiler/property_setter_factory.js create mode 100644 modules/angular2/test/core/compiler/property_setter_factory_spec.js diff --git a/modules/angular2/src/core/compiler/element_injector.js b/modules/angular2/src/core/compiler/element_injector.js index 97547971cf..b815d42768 100644 --- a/modules/angular2/src/core/compiler/element_injector.js +++ b/modules/angular2/src/core/compiler/element_injector.js @@ -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) }; } 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 7271097ceb..3d3aef5ee8 100644 --- a/modules/angular2/src/core/compiler/pipeline/element_binder_builder.js +++ b/modules/angular2/src/core/compiler/pipeline/element_binder_builder.js @@ -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; - } } diff --git a/modules/angular2/src/core/compiler/property_setter_factory.js b/modules/angular2/src/core/compiler/property_setter_factory.js new file mode 100644 index 0000000000..3696635f7b --- /dev/null +++ b/modules/angular2/src/core/compiler/property_setter_factory.js @@ -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; +} diff --git a/modules/angular2/test/core/compiler/element_injector_spec.js b/modules/angular2/test/core/compiler/element_injector_spec.js index a229ee796a..7e4837232c 100644 --- a/modules/angular2/test/core/compiler/element_injector_spec.js +++ b/modules/angular2/test/core/compiler/element_injector_spec.js @@ -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'); }); }); diff --git a/modules/angular2/test/core/compiler/property_setter_factory_spec.js b/modules/angular2/test/core/compiler/property_setter_factory_spec.js new file mode 100644 index 0000000000..b021100eb7 --- /dev/null +++ b/modules/angular2/test/core/compiler/property_setter_factory_spec.js @@ -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('
'); + }); + 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, ''); + expect(DOM.getInnerHTML(div)).toEqual(''); + + var otherSetterFn = setterFactory('innerHtml'); + expect(setterFn).toBe(otherSetterFn); + }); + + }); +}