Skip to content

Commit df8e15c

Browse files
committed
feat(core): add support for @HostBinding and @HostListener
Example: @directive({selector: 'my-directive'}) class MyDirective { @HostBinding("attr.my-attr") myAttr: string; @HostListener("click", ["$event.target"]) onClick(target) { this.target = target; } } Closes #3996
1 parent 855cb16 commit df8e15c

File tree

7 files changed

+264
-30
lines changed

7 files changed

+264
-30
lines changed

modules/angular2/metadata.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,13 @@ export {
4040
PropertyMetadata,
4141
Event,
4242
EventFactory,
43-
EventMetadata
43+
EventMetadata,
44+
HostBinding,
45+
HostBindingFactory,
46+
HostBindingMetadata,
47+
HostListener,
48+
HostListenerFactory,
49+
HostListenerMetadata
4450
} from './src/core/metadata';
4551

4652
export {

modules/angular2/src/core/compiler/directive_resolver.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import {resolveForwardRef, Injectable} from 'angular2/di';
22
import {Type, isPresent, BaseException, stringify} from 'angular2/src/core/facade/lang';
3-
import {ListWrapper, StringMapWrapper} from 'angular2/src/core/facade/collection';
3+
import {ListWrapper, StringMap, StringMapWrapper} from 'angular2/src/core/facade/collection';
44
import {
55
DirectiveMetadata,
66
ComponentMetadata,
77
PropertyMetadata,
8-
EventMetadata
8+
EventMetadata,
9+
HostBindingMetadata,
10+
HostListenerMetadata
911
} from 'angular2/metadata';
1012
import {reflector} from 'angular2/src/core/reflection/reflection';
1113

@@ -40,6 +42,7 @@ export class DirectiveResolver {
4042
StringMap<string, any[]>): DirectiveMetadata {
4143
var properties = [];
4244
var events = [];
45+
var host = {};
4346

4447
StringMapWrapper.forEach(propertyMetadata, (metadata: any[], propName: string) => {
4548
metadata.forEach(a => {
@@ -58,23 +61,37 @@ export class DirectiveResolver {
5861
events.push(propName);
5962
}
6063
}
64+
65+
if (a instanceof HostBindingMetadata) {
66+
if (isPresent(a.hostPropertyName)) {
67+
host[`[${a.hostPropertyName}]`] = propName;
68+
} else {
69+
host[`[${propName}]`] = propName;
70+
}
71+
}
72+
73+
if (a instanceof HostListenerMetadata) {
74+
var args = isPresent(a.args) ? a.args.join(', ') : '';
75+
host[`(${a.eventName})`] = `${propName}(${args})`;
76+
}
6177
});
6278
});
63-
64-
return this._merge(dm, properties, events);
79+
return this._merge(dm, properties, events, host);
6580
}
6681

67-
private _merge(dm: DirectiveMetadata, properties: string[], events: string[]): DirectiveMetadata {
82+
private _merge(dm: DirectiveMetadata, properties: string[], events: string[],
83+
host: StringMap<string, string>): DirectiveMetadata {
6884
var mergedProperties =
6985
isPresent(dm.properties) ? ListWrapper.concat(dm.properties, properties) : properties;
7086
var mergedEvents = isPresent(dm.events) ? ListWrapper.concat(dm.events, events) : events;
87+
var mergedHost = isPresent(dm.host) ? StringMapWrapper.merge(dm.host, host) : host;
7188

7289
if (dm instanceof ComponentMetadata) {
7390
return new ComponentMetadata({
7491
selector: dm.selector,
7592
properties: mergedProperties,
7693
events: mergedEvents,
77-
host: dm.host,
94+
host: mergedHost,
7895
lifecycle: dm.lifecycle,
7996
bindings: dm.bindings,
8097
exportAs: dm.exportAs,
@@ -88,7 +105,7 @@ export class DirectiveResolver {
88105
selector: dm.selector,
89106
properties: mergedProperties,
90107
events: mergedEvents,
91-
host: dm.host,
108+
host: mergedHost,
92109
lifecycle: dm.lifecycle,
93110
bindings: dm.bindings,
94111
exportAs: dm.exportAs,

modules/angular2/src/core/metadata.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,19 @@ class Event extends EventMetadata {
112112
const Event([String bindingPropertyName])
113113
: super(bindingPropertyName);
114114
}
115+
116+
/**
117+
* See: [HostBindingMetadata] for docs.
118+
*/
119+
class HostBinding extends HostBindingMetadata {
120+
const HostBinding([String hostPropertyName])
121+
: super(hostPropertyName);
122+
}
123+
124+
/**
125+
* See: [HostListenerMetadata] for docs.
126+
*/
127+
class HostListener extends HostListenerMetadata {
128+
const HostListener(String eventName, [List<String> args])
129+
: super(eventName, args);
130+
}

modules/angular2/src/core/metadata.ts

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ export {
1515
PipeMetadata,
1616
LifecycleEvent,
1717
PropertyMetadata,
18-
EventMetadata
18+
EventMetadata,
19+
HostBindingMetadata,
20+
HostListenerMetadata
1921
} from './metadata/directives';
2022

2123
export {ViewMetadata, ViewEncapsulation} from './metadata/view';
@@ -33,7 +35,9 @@ import {
3335
PipeMetadata,
3436
LifecycleEvent,
3537
PropertyMetadata,
36-
EventMetadata
38+
EventMetadata,
39+
HostBindingMetadata,
40+
HostListenerMetadata
3741
} from './metadata/directives';
3842

3943
import {ViewMetadata, ViewEncapsulation} from './metadata/view';
@@ -447,6 +451,45 @@ export interface EventFactory {
447451
new (bindingPropertyName?: string): any;
448452
}
449453

454+
/**
455+
* {@link HostBindingMetadata} factory for creating decorators.
456+
*
457+
* ## Example as TypeScript Decorator
458+
*
459+
* ```
460+
* @Directive({
461+
* selector: 'sample-dir'
462+
* })
463+
* class SampleDir {
464+
* @HostBinding() prop1; // Same as @HostBinding('prop1') prop1;
465+
* @HostBinding("el-prop") prop1;
466+
* }
467+
* ```
468+
*/
469+
export interface HostBindingFactory {
470+
(hostPropertyName?: string): any;
471+
new (hostPropertyName?: string): any;
472+
}
473+
474+
/**
475+
* {@link HostListenerMetadata} factory for creating decorators.
476+
*
477+
* ## Example as TypeScript Decorator
478+
*
479+
* ```
480+
* @Directive({
481+
* selector: 'sample-dir'
482+
* })
483+
* class SampleDir {
484+
* @HostListener("change", ['$event.target.value']) onChange(value){}
485+
* }
486+
* ```
487+
*/
488+
export interface HostListenerFactory {
489+
(eventName: string, args?: string[]): any;
490+
new (eventName: string, args?: string[]): any;
491+
}
492+
450493
/**
451494
* {@link ComponentMetadata} factory function.
452495
*/
@@ -492,4 +535,14 @@ export var Property: PropertyFactory = makePropDecorator(PropertyMetadata);
492535
/**
493536
* {@link EventMetadata} factory function.
494537
*/
495-
export var Event: EventFactory = makePropDecorator(EventMetadata);
538+
export var Event: EventFactory = makePropDecorator(EventMetadata);
539+
540+
/**
541+
* {@link HostBindingMetadata} factory function.
542+
*/
543+
export var HostBinding: HostBindingFactory = makePropDecorator(HostBindingMetadata);
544+
545+
/**
546+
* {@link HostListenerMetadata} factory function.
547+
*/
548+
export var HostListener: HostListenerFactory = makePropDecorator(HostListenerMetadata);

modules/angular2/src/core/metadata/directives.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,4 +1109,68 @@ export class PropertyMetadata {
11091109
@CONST()
11101110
export class EventMetadata {
11111111
constructor(public bindingPropertyName?: string) {}
1112+
}
1113+
1114+
/**
1115+
* Declare a host property binding.
1116+
*
1117+
* ## Example
1118+
*
1119+
* ```
1120+
* @Directive({
1121+
* selector: 'sample-dir'
1122+
* })
1123+
* class SampleDir {
1124+
* @HostBinding() prop1; // Same as @HostBinding('prop1') prop1;
1125+
* @HostBinding("el-prop") prop2;
1126+
* }
1127+
* ```
1128+
*
1129+
* This is equivalent to
1130+
*
1131+
* ```
1132+
* @Directive({
1133+
* selector: 'sample-dir',
1134+
* host: {'[prop1]': 'prop1', '[el-prop]': 'prop2'}
1135+
* })
1136+
* class SampleDir {
1137+
* prop1;
1138+
* prop2;
1139+
* }
1140+
* ```
1141+
*/
1142+
@CONST()
1143+
export class HostBindingMetadata {
1144+
constructor(public hostPropertyName?: string) {}
1145+
}
1146+
1147+
/**
1148+
* Declare a host listener.
1149+
*
1150+
* ## Example
1151+
*
1152+
* ```
1153+
* @Directive({
1154+
* selector: 'sample-dir'
1155+
* })
1156+
* class SampleDir {
1157+
* @HostListener("change", ['$event.target.value']) onChange(value){}
1158+
* }
1159+
* ```
1160+
*
1161+
* This is equivalent to
1162+
*
1163+
* ```
1164+
* @Directive({
1165+
* selector: 'sample-dir',
1166+
* host: {'(change)': 'onChange($event.target.value)'}
1167+
* })
1168+
* class SampleDir {
1169+
* onChange(value){}
1170+
* }
1171+
* ```
1172+
*/
1173+
@CONST()
1174+
export class HostListenerMetadata {
1175+
constructor(public eventName: string, public args?: string[]) {}
11121176
}

modules/angular2/test/core/compiler/directive_resolver_spec.ts

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import {ddescribe, describe, it, iit, expect, beforeEach} from 'angular2/test_lib';
22
import {DirectiveResolver} from 'angular2/src/core/compiler/directive_resolver';
3-
import {DirectiveMetadata, Directive, Property, Event} from 'angular2/metadata';
3+
import {
4+
DirectiveMetadata,
5+
Directive,
6+
Property,
7+
Event,
8+
HostBinding,
9+
HostListener
10+
} from 'angular2/metadata';
411

512
@Directive({selector: 'someDirective'})
613
class SomeDirective {
@@ -11,7 +18,7 @@ class SomeChildDirective extends SomeDirective {
1118
}
1219

1320
@Directive({selector: 'someDirective', properties: ['c']})
14-
class SomeDirectiveWithProps {
21+
class SomeDirectiveWithProperties {
1522
@Property() a;
1623
@Property("renamed") b;
1724
c;
@@ -40,6 +47,23 @@ class SomeDirectiveWithGetterEvents {
4047
}
4148
}
4249

50+
@Directive({selector: 'someDirective', host: {'[c]': 'c'}})
51+
class SomeDirectiveWithHostBindings {
52+
@HostBinding() a;
53+
@HostBinding("renamed") b;
54+
c;
55+
}
56+
57+
@Directive({selector: 'someDirective', host: {'(c)': 'onC()'}})
58+
class SomeDirectiveWithHostListeners {
59+
@HostListener('a')
60+
onA() {
61+
}
62+
@HostListener('b', ['$event.value'])
63+
onB(value) {
64+
}
65+
}
66+
4367

4468
class SomeDirectiveWithoutMetadata {}
4569

@@ -52,7 +76,8 @@ export function main() {
5276
it('should read out the Directive metadata', () => {
5377
var directiveMetadata = resolver.resolve(SomeDirective);
5478
expect(directiveMetadata)
55-
.toEqual(new DirectiveMetadata({selector: 'someDirective', properties: [], events: []}));
79+
.toEqual(new DirectiveMetadata(
80+
{selector: 'someDirective', properties: [], events: [], host: {}}));
5681
});
5782

5883
it('should throw if not matching metadata is found', () => {
@@ -63,39 +88,44 @@ export function main() {
6388
it('should not read parent class Directive metadata', function() {
6489
var directiveMetadata = resolver.resolve(SomeChildDirective);
6590
expect(directiveMetadata)
66-
.toEqual(
67-
new DirectiveMetadata({selector: 'someChildDirective', properties: [], events: []}));
91+
.toEqual(new DirectiveMetadata(
92+
{selector: 'someChildDirective', properties: [], events: [], host: {}}));
6893
});
6994

7095
describe('properties', () => {
7196
it('should append directive properties', () => {
72-
var directiveMetadata = resolver.resolve(SomeDirectiveWithProps);
73-
expect(directiveMetadata)
74-
.toEqual(new DirectiveMetadata(
75-
{selector: 'someDirective', properties: ['c', 'a', 'b: renamed'], events: []}));
97+
var directiveMetadata = resolver.resolve(SomeDirectiveWithProperties);
98+
expect(directiveMetadata.properties).toEqual(['c', 'a', 'b: renamed']);
7699
});
77100

78101
it('should work with getters and setters', () => {
79102
var directiveMetadata = resolver.resolve(SomeDirectiveWithSetterProps);
80-
expect(directiveMetadata)
81-
.toEqual(new DirectiveMetadata(
82-
{selector: 'someDirective', properties: ['a: renamed'], events: []}));
103+
expect(directiveMetadata.properties).toEqual(['a: renamed']);
83104
});
84105
});
85106

86107
describe('events', () => {
87108
it('should append directive events', () => {
88109
var directiveMetadata = resolver.resolve(SomeDirectiveWithEvents);
89-
expect(directiveMetadata)
90-
.toEqual(new DirectiveMetadata(
91-
{selector: 'someDirective', properties: [], events: ['c', 'a', 'b: renamed']}));
110+
expect(directiveMetadata.events).toEqual(['c', 'a', 'b: renamed']);
92111
});
93112

94113
it('should work with getters and setters', () => {
95114
var directiveMetadata = resolver.resolve(SomeDirectiveWithGetterEvents);
96-
expect(directiveMetadata)
97-
.toEqual(new DirectiveMetadata(
98-
{selector: 'someDirective', properties: [], events: ['a: renamed']}));
115+
expect(directiveMetadata.events).toEqual(['a: renamed']);
116+
});
117+
});
118+
119+
describe('host', () => {
120+
it('should append host bindings', () => {
121+
var directiveMetadata = resolver.resolve(SomeDirectiveWithHostBindings);
122+
expect(directiveMetadata.host).toEqual({'[c]': 'c', '[a]': 'a', '[renamed]': 'b'});
123+
});
124+
125+
it('should append host listeners', () => {
126+
var directiveMetadata = resolver.resolve(SomeDirectiveWithHostListeners);
127+
expect(directiveMetadata.host)
128+
.toEqual({'(c)': 'onC()', '(a)': 'onA()', '(b)': 'onB($event.value)'});
99129
});
100130
});
101131
});

0 commit comments

Comments
 (0)