feat(compiler): allow setting attributes on a host element

Closes #1402
This commit is contained in:
Pawel Kozlowski 2015-05-01 13:41:56 +02:00
parent 7225416661
commit 51839ca677
12 changed files with 107 additions and 1 deletions

View File

@ -598,6 +598,28 @@ export class Directive extends Injectable {
*/ */
hostProperties:any; // String map 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.: `<div my-button></div>`) on a host element (here: `<div>` )
* 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. * Specifies a set of lifecycle hostListeners in which the directive participates.
* *
@ -618,6 +640,7 @@ export class Directive extends Injectable {
events, events,
hostListeners, hostListeners,
hostProperties, hostProperties,
hostAttributes,
lifecycle, lifecycle,
compileChildren = true, compileChildren = true,
}:{ }:{
@ -626,6 +649,7 @@ export class Directive extends Injectable {
events:List, events:List,
hostListeners: any, hostListeners: any,
hostProperties: any, hostProperties: any,
hostAttributes: any,
lifecycle:List, lifecycle:List,
compileChildren:boolean compileChildren:boolean
}={}) }={})
@ -636,6 +660,7 @@ export class Directive extends Injectable {
this.events = events; this.events = events;
this.hostListeners = hostListeners; this.hostListeners = hostListeners;
this.hostProperties = hostProperties; this.hostProperties = hostProperties;
this.hostAttributes = hostAttributes;
this.lifecycle = lifecycle; this.lifecycle = lifecycle;
this.compileChildren = compileChildren; this.compileChildren = compileChildren;
} }
@ -797,6 +822,7 @@ export class Component extends Directive {
events, events,
hostListeners, hostListeners,
hostProperties, hostProperties,
hostAttributes,
injectables, injectables,
lifecycle, lifecycle,
changeDetection = DEFAULT, changeDetection = DEFAULT,
@ -807,6 +833,7 @@ export class Component extends Directive {
events:List, events:List,
hostListeners:any, hostListeners:any,
hostProperties:any, hostProperties:any,
hostAttributes:any,
injectables:List, injectables:List,
lifecycle:List, lifecycle:List,
changeDetection:string, changeDetection:string,
@ -819,6 +846,7 @@ export class Component extends Directive {
events: events, events: events,
hostListeners: hostListeners, hostListeners: hostListeners,
hostProperties: hostProperties, hostProperties: hostProperties,
hostAttributes: hostAttributes,
lifecycle: lifecycle, lifecycle: lifecycle,
compileChildren: compileChildren compileChildren: compileChildren
}); });

View File

@ -247,6 +247,7 @@ export class Compiler {
compileChildren: compileChildren, compileChildren: compileChildren,
hostListeners: isPresent(ann.hostListeners) ? MapWrapper.createFromStringMap(ann.hostListeners) : null, hostListeners: isPresent(ann.hostListeners) ? MapWrapper.createFromStringMap(ann.hostListeners) : null,
hostProperties: isPresent(ann.hostProperties) ? MapWrapper.createFromStringMap(ann.hostProperties) : 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, properties: isPresent(ann.properties) ? MapWrapper.createFromStringMap(ann.properties) : null,
readAttributes: readAttributes readAttributes: readAttributes
}); });

View File

@ -248,6 +248,9 @@ class BrowserDomAdapter extends GenericBrowserDomAdapter {
return new Map.from(element.attributes); return new Map.from(element.attributes);
} }
bool hasAttribute(Element element, String attribute) =>
element.attributes.containsKey(attribute);
String getAttribute(Element element, String attribute) => String getAttribute(Element element, String attribute) =>
element.getAttribute(attribute); element.getAttribute(attribute);

View File

@ -263,6 +263,9 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
} }
return res; return res;
} }
hasAttribute(element, attribute:string) {
return element.hasAttribute(attribute);
}
getAttribute(element, attribute:string) { getAttribute(element, attribute:string) {
return element.getAttribute(attribute); return element.getAttribute(attribute);
} }

View File

@ -199,6 +199,9 @@ export class DomAdapter {
attributeMap(element) { attributeMap(element) {
throw _abstract(); throw _abstract();
} }
hasAttribute(element, attribute:string):boolean {
throw _abstract();
}
getAttribute(element, attribute:string):string { getAttribute(element, attribute:string):string {
throw _abstract(); throw _abstract();
} }

View File

@ -211,6 +211,9 @@ class Html5LibDomAdapter implements DomAdapter {
}); });
return map; return map;
} }
hasAttribute(element, String attribute) {
throw 'not implemented';
}
getAttribute(element, String attribute) { getAttribute(element, String attribute) {
throw 'not implemented'; throw 'not implemented';
} }

View File

@ -384,6 +384,9 @@ export class Parse5DomAdapter extends DomAdapter {
} }
return res; return res;
} }
hasAttribute(element, attribute:string) {
return element.attribs && element.attribs.hasOwnProperty(attribute);
}
getAttribute(element, attribute:string) { getAttribute(element, attribute:string) {
return element.attribs && element.attribs.hasOwnProperty(attribute)? element.attribs[attribute]: null; return element.attribs && element.attribs.hasOwnProperty(attribute)? element.attribs[attribute]: null;
} }

View File

@ -116,15 +116,17 @@ export class DirectiveMetadata {
compileChildren:boolean; compileChildren:boolean;
hostListeners:Map<string, string>; hostListeners:Map<string, string>;
hostProperties:Map<string, string>; hostProperties:Map<string, string>;
hostAttributes:Map<string, string>;
properties:Map<string, string>; properties:Map<string, string>;
readAttributes:List<string>; readAttributes:List<string>;
type:number; 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.id = id;
this.selector = selector; this.selector = selector;
this.compileChildren = isPresent(compileChildren) ? compileChildren : true; this.compileChildren = isPresent(compileChildren) ? compileChildren : true;
this.hostListeners = hostListeners; this.hostListeners = hostListeners;
this.hostProperties = hostProperties; this.hostProperties = hostProperties;
this.hostAttributes = hostAttributes;
this.properties = properties; this.properties = properties;
this.readAttributes = readAttributes; this.readAttributes = readAttributes;
this.type = type; this.type = type;

View File

@ -78,6 +78,13 @@ export class DirectiveParser extends CompileStep {
this._bindHostProperty(hostPropertyName, directivePropertyName, current, directiveBinderBuilder); 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)) { if (isPresent(directive.readAttributes)) {
ListWrapper.forEach(directive.readAttributes, (attrName) => { ListWrapper.forEach(directive.readAttributes, (attrName) => {
elementBinder.readAttribute(attrName); elementBinder.readAttribute(attrName);

View File

@ -13,6 +13,7 @@ export function directiveMetadataToMap(meta: DirectiveMetadata): Map {
['compileChildren', meta.compileChildren], ['compileChildren', meta.compileChildren],
['hostListeners', _cloneIfPresent(meta.hostListeners)], ['hostListeners', _cloneIfPresent(meta.hostListeners)],
['hostProperties', _cloneIfPresent(meta.hostProperties)], ['hostProperties', _cloneIfPresent(meta.hostProperties)],
['hostAttributes', _cloneIfPresent(meta.hostAttributes)],
['properties', _cloneIfPresent(meta.properties)], ['properties', _cloneIfPresent(meta.properties)],
['readAttributes', _cloneIfPresent(meta.readAttributes)], ['readAttributes', _cloneIfPresent(meta.readAttributes)],
['type', meta.type], ['type', meta.type],
@ -32,6 +33,7 @@ export function directiveMetadataFromMap(map: Map): DirectiveMetadata {
compileChildren: MapWrapper.get(map, 'compileChildren'), compileChildren: MapWrapper.get(map, 'compileChildren'),
hostListeners: _cloneIfPresent(MapWrapper.get(map, 'hostListeners')), hostListeners: _cloneIfPresent(MapWrapper.get(map, 'hostListeners')),
hostProperties: _cloneIfPresent(MapWrapper.get(map, 'hostProperties')), hostProperties: _cloneIfPresent(MapWrapper.get(map, 'hostProperties')),
hostAttributes: _cloneIfPresent(MapWrapper.get(map, 'hostAttributes')),
properties: _cloneIfPresent(MapWrapper.get(map, 'properties')), properties: _cloneIfPresent(MapWrapper.get(map, 'properties')),
readAttributes: _cloneIfPresent(MapWrapper.get(map, 'readAttributes')), readAttributes: _cloneIfPresent(MapWrapper.get(map, 'readAttributes')),
type: MapWrapper.get(map, 'type') type: MapWrapper.get(map, 'type')

View File

@ -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: '<div update-host-attributes></div>',
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) => { it('should support updating host element via hostProperties', inject([TestBed, AsyncTestCompleter], (tb, async) => {
tb.overrideView(MyComp, new View({ tb.overrideView(MyComp, new View({
template: '<div update-host-properties></div>', template: '<div update-host-properties></div>',
@ -1095,6 +1110,15 @@ class DirectiveEmitingEvent {
} }
} }
@Directive({
selector: '[update-host-attributes]',
hostAttributes: {
'role' : 'button'
}
})
class DirectiveUpdatingHostAttributes {
}
@Directive({ @Directive({
selector: '[update-host-properties]', selector: '[update-host-properties]',
hostProperties: { hostProperties: {

View File

@ -1,6 +1,7 @@
import {describe, beforeEach, it, xit, expect, iit, ddescribe, el} from 'angular2/test_lib'; import {describe, beforeEach, it, xit, expect, iit, ddescribe, el} from 'angular2/test_lib';
import {isPresent, isBlank, assertionsEnabled} from 'angular2/src/facade/lang'; import {isPresent, isBlank, assertionsEnabled} from 'angular2/src/facade/lang';
import {ListWrapper, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection'; 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 {DirectiveParser} from 'angular2/src/render/dom/compiler/directive_parser';
import {CompilePipeline} from 'angular2/src/render/dom/compiler/compile_pipeline'; import {CompilePipeline} from 'angular2/src/render/dom/compiler/compile_pipeline';
import {CompileStep} from 'angular2/src/render/dom/compiler/compile_step'; import {CompileStep} from 'angular2/src/render/dom/compiler/compile_step';
@ -22,6 +23,7 @@ export function main() {
decoratorWithMultipleAttrs, decoratorWithMultipleAttrs,
someDirectiveWithProps, someDirectiveWithProps,
someDirectiveWithHostProperties, someDirectiveWithHostProperties,
someDirectiveWithHostAttributes,
someDirectiveWithEvents, someDirectiveWithEvents,
someDirectiveWithGlobalEvents someDirectiveWithGlobalEvents
]; ];
@ -123,6 +125,24 @@ export function main() {
expect(ast.source).toEqual('dirProp'); expect(ast.source).toEqual('dirProp');
}); });
it('should set host element attributes', () => {
var element = el('<input some-decor-with-host-attrs>');
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('<input attr_name="initial" some-decor-with-host-attrs>');
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', () => { it('should read attribute values', () => {
var element = el('<input some-decor-props some-attr="someValue">'); var element = el('<input some-decor-props some-attr="someValue">');
var results = process(element); 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({ var someDirectiveWithEvents = new DirectiveMetadata({
selector: '[some-decor-events]', selector: '[some-decor-events]',
hostListeners: MapWrapper.createFromStringMap({ hostListeners: MapWrapper.createFromStringMap({