diff --git a/modules/angular2/src/core/annotations_impl/annotations.js b/modules/angular2/src/core/annotations_impl/annotations.js
index c944a682bd..ccde9f28bf 100644
--- a/modules/angular2/src/core/annotations_impl/annotations.js
+++ b/modules/angular2/src/core/annotations_impl/annotations.js
@@ -598,6 +598,28 @@ export class Directive extends Injectable {
*/
hostProperties:any; // String map
+ /**
+ * Specifies static attributes that should be propagated to a host element. Attributes specified in `hostAttributes`
+ * are propagated only if a given attribute is not present on a host element.
+ *
+ * ## Syntax
+ *
+ * ```
+ * @Directive({
+ * selector: '[my-button]',
+ * hostAttributes: {
+ * 'role': 'button'
+ * }
+ * })
+ * class MyButton {
+ * }
+ *
+ * In this example using `my-button` directive (ex.: `
` )
+ * will ensure that this element will get the "button" role.
+ * ```
+ */
+ hostAttributes:any; // String map
+
/**
* Specifies a set of lifecycle hostListeners in which the directive participates.
*
@@ -618,6 +640,7 @@ export class Directive extends Injectable {
events,
hostListeners,
hostProperties,
+ hostAttributes,
lifecycle,
compileChildren = true,
}:{
@@ -626,6 +649,7 @@ export class Directive extends Injectable {
events:List,
hostListeners: any,
hostProperties: any,
+ hostAttributes: any,
lifecycle:List,
compileChildren:boolean
}={})
@@ -636,6 +660,7 @@ export class Directive extends Injectable {
this.events = events;
this.hostListeners = hostListeners;
this.hostProperties = hostProperties;
+ this.hostAttributes = hostAttributes;
this.lifecycle = lifecycle;
this.compileChildren = compileChildren;
}
@@ -797,6 +822,7 @@ export class Component extends Directive {
events,
hostListeners,
hostProperties,
+ hostAttributes,
injectables,
lifecycle,
changeDetection = DEFAULT,
@@ -807,6 +833,7 @@ export class Component extends Directive {
events:List,
hostListeners:any,
hostProperties:any,
+ hostAttributes:any,
injectables:List,
lifecycle:List,
changeDetection:string,
@@ -819,6 +846,7 @@ export class Component extends Directive {
events: events,
hostListeners: hostListeners,
hostProperties: hostProperties,
+ hostAttributes: hostAttributes,
lifecycle: lifecycle,
compileChildren: compileChildren
});
diff --git a/modules/angular2/src/core/compiler/compiler.js b/modules/angular2/src/core/compiler/compiler.js
index 8f8b316568..16bc783d3b 100644
--- a/modules/angular2/src/core/compiler/compiler.js
+++ b/modules/angular2/src/core/compiler/compiler.js
@@ -247,6 +247,7 @@ export class Compiler {
compileChildren: compileChildren,
hostListeners: isPresent(ann.hostListeners) ? MapWrapper.createFromStringMap(ann.hostListeners) : null,
hostProperties: isPresent(ann.hostProperties) ? MapWrapper.createFromStringMap(ann.hostProperties) : null,
+ hostAttributes: isPresent(ann.hostAttributes) ? MapWrapper.createFromStringMap(ann.hostAttributes) : null,
properties: isPresent(ann.properties) ? MapWrapper.createFromStringMap(ann.properties) : null,
readAttributes: readAttributes
});
diff --git a/modules/angular2/src/dom/browser_adapter.dart b/modules/angular2/src/dom/browser_adapter.dart
index 1588c1772d..f85414530b 100644
--- a/modules/angular2/src/dom/browser_adapter.dart
+++ b/modules/angular2/src/dom/browser_adapter.dart
@@ -248,6 +248,9 @@ class BrowserDomAdapter extends GenericBrowserDomAdapter {
return new Map.from(element.attributes);
}
+ bool hasAttribute(Element element, String attribute) =>
+ element.attributes.containsKey(attribute);
+
String getAttribute(Element element, String attribute) =>
element.getAttribute(attribute);
diff --git a/modules/angular2/src/dom/browser_adapter.es6 b/modules/angular2/src/dom/browser_adapter.es6
index dfeefd5f3e..4990b95cc2 100644
--- a/modules/angular2/src/dom/browser_adapter.es6
+++ b/modules/angular2/src/dom/browser_adapter.es6
@@ -263,6 +263,9 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
}
return res;
}
+ hasAttribute(element, attribute:string) {
+ return element.hasAttribute(attribute);
+ }
getAttribute(element, attribute:string) {
return element.getAttribute(attribute);
}
diff --git a/modules/angular2/src/dom/dom_adapter.js b/modules/angular2/src/dom/dom_adapter.js
index ae75257f3f..cff55c4b6b 100644
--- a/modules/angular2/src/dom/dom_adapter.js
+++ b/modules/angular2/src/dom/dom_adapter.js
@@ -199,6 +199,9 @@ export class DomAdapter {
attributeMap(element) {
throw _abstract();
}
+ hasAttribute(element, attribute:string):boolean {
+ throw _abstract();
+ }
getAttribute(element, attribute:string):string {
throw _abstract();
}
diff --git a/modules/angular2/src/dom/html_adapter.dart b/modules/angular2/src/dom/html_adapter.dart
index 5b7173a65b..297f8680be 100644
--- a/modules/angular2/src/dom/html_adapter.dart
+++ b/modules/angular2/src/dom/html_adapter.dart
@@ -211,6 +211,9 @@ class Html5LibDomAdapter implements DomAdapter {
});
return map;
}
+ hasAttribute(element, String attribute) {
+ throw 'not implemented';
+ }
getAttribute(element, String attribute) {
throw 'not implemented';
}
diff --git a/modules/angular2/src/dom/parse5_adapter.cjs b/modules/angular2/src/dom/parse5_adapter.cjs
index d126d185fa..28f9642237 100644
--- a/modules/angular2/src/dom/parse5_adapter.cjs
+++ b/modules/angular2/src/dom/parse5_adapter.cjs
@@ -384,6 +384,9 @@ export class Parse5DomAdapter extends DomAdapter {
}
return res;
}
+ hasAttribute(element, attribute:string) {
+ return element.attribs && element.attribs.hasOwnProperty(attribute);
+ }
getAttribute(element, attribute:string) {
return element.attribs && element.attribs.hasOwnProperty(attribute)? element.attribs[attribute]: null;
}
diff --git a/modules/angular2/src/render/api.js b/modules/angular2/src/render/api.js
index 307248e7a3..92e9404124 100644
--- a/modules/angular2/src/render/api.js
+++ b/modules/angular2/src/render/api.js
@@ -116,15 +116,17 @@ export class DirectiveMetadata {
compileChildren:boolean;
hostListeners:Map
;
hostProperties:Map;
+ hostAttributes:Map;
properties:Map;
readAttributes:List;
type:number;
- constructor({id, selector, compileChildren, hostListeners, hostProperties, properties, readAttributes, type}) {
+ constructor({id, selector, compileChildren, hostListeners, hostProperties, hostAttributes, properties, readAttributes, type}) {
this.id = id;
this.selector = selector;
this.compileChildren = isPresent(compileChildren) ? compileChildren : true;
this.hostListeners = hostListeners;
this.hostProperties = hostProperties;
+ this.hostAttributes = hostAttributes;
this.properties = properties;
this.readAttributes = readAttributes;
this.type = type;
diff --git a/modules/angular2/src/render/dom/compiler/directive_parser.js b/modules/angular2/src/render/dom/compiler/directive_parser.js
index 91cec5eff5..9b0b32208b 100644
--- a/modules/angular2/src/render/dom/compiler/directive_parser.js
+++ b/modules/angular2/src/render/dom/compiler/directive_parser.js
@@ -78,6 +78,13 @@ export class DirectiveParser extends CompileStep {
this._bindHostProperty(hostPropertyName, directivePropertyName, current, directiveBinderBuilder);
});
}
+ if (isPresent(directive.hostAttributes)) {
+ MapWrapper.forEach(directive.hostAttributes, (hostAttrValue, hostAttrName) => {
+ if (!DOM.hasAttribute(current.element, hostAttrName)) {
+ DOM.setAttribute(current.element, hostAttrName, hostAttrValue);
+ }
+ });
+ }
if (isPresent(directive.readAttributes)) {
ListWrapper.forEach(directive.readAttributes, (attrName) => {
elementBinder.readAttribute(attrName);
diff --git a/modules/angular2/src/render/dom/convert.js b/modules/angular2/src/render/dom/convert.js
index 556ffa5574..9b57c08554 100644
--- a/modules/angular2/src/render/dom/convert.js
+++ b/modules/angular2/src/render/dom/convert.js
@@ -13,6 +13,7 @@ export function directiveMetadataToMap(meta: DirectiveMetadata): Map {
['compileChildren', meta.compileChildren],
['hostListeners', _cloneIfPresent(meta.hostListeners)],
['hostProperties', _cloneIfPresent(meta.hostProperties)],
+ ['hostAttributes', _cloneIfPresent(meta.hostAttributes)],
['properties', _cloneIfPresent(meta.properties)],
['readAttributes', _cloneIfPresent(meta.readAttributes)],
['type', meta.type],
@@ -32,6 +33,7 @@ export function directiveMetadataFromMap(map: Map): DirectiveMetadata {
compileChildren: MapWrapper.get(map, 'compileChildren'),
hostListeners: _cloneIfPresent(MapWrapper.get(map, 'hostListeners')),
hostProperties: _cloneIfPresent(MapWrapper.get(map, 'hostProperties')),
+ hostAttributes: _cloneIfPresent(MapWrapper.get(map, 'hostAttributes')),
properties: _cloneIfPresent(MapWrapper.get(map, 'properties')),
readAttributes: _cloneIfPresent(MapWrapper.get(map, 'readAttributes')),
type: MapWrapper.get(map, 'type')
diff --git a/modules/angular2/test/core/compiler/integration_spec.js b/modules/angular2/test/core/compiler/integration_spec.js
index 7312b77b92..27a5ba1ce6 100644
--- a/modules/angular2/test/core/compiler/integration_spec.js
+++ b/modules/angular2/test/core/compiler/integration_spec.js
@@ -617,6 +617,21 @@ export function main() {
});
}));
+ it('should support updating host element via hostAttributes', inject([TestBed, AsyncTestCompleter], (tb, async) => {
+ tb.overrideView(MyComp, new View({
+ template: '',
+ directives: [DirectiveUpdatingHostAttributes]
+ }));
+
+ tb.createView(MyComp, {context: ctx}).then((view) => {
+ view.detectChanges();
+
+ expect(DOM.getAttribute(view.rootNodes[0], "role")).toEqual("button");
+
+ async.done();
+ });
+ }));
+
it('should support updating host element via hostProperties', inject([TestBed, AsyncTestCompleter], (tb, async) => {
tb.overrideView(MyComp, new View({
template: '',
@@ -1095,6 +1110,15 @@ class DirectiveEmitingEvent {
}
}
+@Directive({
+ selector: '[update-host-attributes]',
+ hostAttributes: {
+ 'role' : 'button'
+ }
+})
+class DirectiveUpdatingHostAttributes {
+}
+
@Directive({
selector: '[update-host-properties]',
hostProperties: {
diff --git a/modules/angular2/test/render/dom/compiler/directive_parser_spec.js b/modules/angular2/test/render/dom/compiler/directive_parser_spec.js
index a219693ca9..2c40cb342b 100644
--- a/modules/angular2/test/render/dom/compiler/directive_parser_spec.js
+++ b/modules/angular2/test/render/dom/compiler/directive_parser_spec.js
@@ -1,6 +1,7 @@
import {describe, beforeEach, it, xit, expect, iit, ddescribe, el} from 'angular2/test_lib';
import {isPresent, isBlank, assertionsEnabled} from 'angular2/src/facade/lang';
import {ListWrapper, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
+import {DOM} from 'angular2/src/dom/dom_adapter';
import {DirectiveParser} from 'angular2/src/render/dom/compiler/directive_parser';
import {CompilePipeline} from 'angular2/src/render/dom/compiler/compile_pipeline';
import {CompileStep} from 'angular2/src/render/dom/compiler/compile_step';
@@ -22,6 +23,7 @@ export function main() {
decoratorWithMultipleAttrs,
someDirectiveWithProps,
someDirectiveWithHostProperties,
+ someDirectiveWithHostAttributes,
someDirectiveWithEvents,
someDirectiveWithGlobalEvents
];
@@ -123,6 +125,24 @@ export function main() {
expect(ast.source).toEqual('dirProp');
});
+ it('should set host element attributes', () => {
+ var element = el('');
+ var results = process(element);
+
+ expect(DOM.getAttribute(results[0].element, 'attr_name')).toEqual('attr_val');
+ });
+
+ it('should not set host element attribute if an attribute already exists', () => {
+ var element = el('');
+ var results = process(element);
+
+ expect(DOM.getAttribute(results[0].element, 'attr_name')).toEqual('initial');
+
+ DOM.removeAttribute(element, 'attr_name');
+ results = process(element);
+ expect(DOM.getAttribute(results[0].element, 'attr_name')).toEqual('attr_val');
+ });
+
it('should read attribute values', () => {
var element = el('');
var results = process(element);
@@ -242,6 +262,13 @@ var someDirectiveWithHostProperties = new DirectiveMetadata({
})
});
+var someDirectiveWithHostAttributes = new DirectiveMetadata({
+ selector: '[some-decor-with-host-attrs]',
+ hostAttributes: MapWrapper.createFromStringMap({
+ 'attr_name': 'attr_val'
+ })
+});
+
var someDirectiveWithEvents = new DirectiveMetadata({
selector: '[some-decor-events]',
hostListeners: MapWrapper.createFromStringMap({