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
/**
* 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.
*
@ -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
});

View File

@ -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
});

View File

@ -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);

View File

@ -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);
}

View File

@ -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();
}

View File

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

View File

@ -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;
}

View File

@ -116,15 +116,17 @@ export class DirectiveMetadata {
compileChildren:boolean;
hostListeners:Map<string, string>;
hostProperties:Map<string, string>;
hostAttributes:Map<string, string>;
properties:Map<string, string>;
readAttributes:List<string>;
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;

View File

@ -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);

View File

@ -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')

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

View File

@ -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('<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', () => {
var element = el('<input some-decor-props some-attr="someValue">');
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({