feat(compiler): allow setting attributes on a host element
Closes #1402
This commit is contained in:
parent
7225416661
commit
51839ca677
|
@ -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
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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';
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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({
|
||||||
|
|
Loading…
Reference in New Issue