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.: `
`) on a host element (here: `
` ) + * 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({