From df8e15cab771d001eaf0b26796f39072c012bb53 Mon Sep 17 00:00:00 2001 From: vsavkin Date: Fri, 4 Sep 2015 14:07:16 -0700 Subject: [PATCH] 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 --- modules/angular2/metadata.ts | 8 ++- .../src/core/compiler/directive_resolver.ts | 31 +++++++-- modules/angular2/src/core/metadata.dart | 16 +++++ modules/angular2/src/core/metadata.ts | 59 ++++++++++++++++- .../angular2/src/core/metadata/directives.ts | 64 ++++++++++++++++++ .../core/compiler/directive_resolver_spec.ts | 66 ++++++++++++++----- .../test/core/compiler/integration_spec.ts | 50 +++++++++++++- 7 files changed, 264 insertions(+), 30 deletions(-) diff --git a/modules/angular2/metadata.ts b/modules/angular2/metadata.ts index 472d95f3aa..eb751c8413 100644 --- a/modules/angular2/metadata.ts +++ b/modules/angular2/metadata.ts @@ -40,7 +40,13 @@ export { PropertyMetadata, Event, EventFactory, - EventMetadata + EventMetadata, + HostBinding, + HostBindingFactory, + HostBindingMetadata, + HostListener, + HostListenerFactory, + HostListenerMetadata } from './src/core/metadata'; export { diff --git a/modules/angular2/src/core/compiler/directive_resolver.ts b/modules/angular2/src/core/compiler/directive_resolver.ts index 00e39c8287..bba5e2531d 100644 --- a/modules/angular2/src/core/compiler/directive_resolver.ts +++ b/modules/angular2/src/core/compiler/directive_resolver.ts @@ -1,11 +1,13 @@ import {resolveForwardRef, Injectable} from 'angular2/di'; import {Type, isPresent, BaseException, stringify} from 'angular2/src/core/facade/lang'; -import {ListWrapper, StringMapWrapper} from 'angular2/src/core/facade/collection'; +import {ListWrapper, StringMap, StringMapWrapper} from 'angular2/src/core/facade/collection'; import { DirectiveMetadata, ComponentMetadata, PropertyMetadata, - EventMetadata + EventMetadata, + HostBindingMetadata, + HostListenerMetadata } from 'angular2/metadata'; import {reflector} from 'angular2/src/core/reflection/reflection'; @@ -40,6 +42,7 @@ export class DirectiveResolver { StringMap): DirectiveMetadata { var properties = []; var events = []; + var host = {}; StringMapWrapper.forEach(propertyMetadata, (metadata: any[], propName: string) => { metadata.forEach(a => { @@ -58,23 +61,37 @@ export class DirectiveResolver { events.push(propName); } } + + if (a instanceof HostBindingMetadata) { + if (isPresent(a.hostPropertyName)) { + host[`[${a.hostPropertyName}]`] = propName; + } else { + host[`[${propName}]`] = propName; + } + } + + if (a instanceof HostListenerMetadata) { + var args = isPresent(a.args) ? a.args.join(', ') : ''; + host[`(${a.eventName})`] = `${propName}(${args})`; + } }); }); - - return this._merge(dm, properties, events); + return this._merge(dm, properties, events, host); } - private _merge(dm: DirectiveMetadata, properties: string[], events: string[]): DirectiveMetadata { + private _merge(dm: DirectiveMetadata, properties: string[], events: string[], + host: StringMap): DirectiveMetadata { var mergedProperties = isPresent(dm.properties) ? ListWrapper.concat(dm.properties, properties) : properties; var mergedEvents = isPresent(dm.events) ? ListWrapper.concat(dm.events, events) : events; + var mergedHost = isPresent(dm.host) ? StringMapWrapper.merge(dm.host, host) : host; if (dm instanceof ComponentMetadata) { return new ComponentMetadata({ selector: dm.selector, properties: mergedProperties, events: mergedEvents, - host: dm.host, + host: mergedHost, lifecycle: dm.lifecycle, bindings: dm.bindings, exportAs: dm.exportAs, @@ -88,7 +105,7 @@ export class DirectiveResolver { selector: dm.selector, properties: mergedProperties, events: mergedEvents, - host: dm.host, + host: mergedHost, lifecycle: dm.lifecycle, bindings: dm.bindings, exportAs: dm.exportAs, diff --git a/modules/angular2/src/core/metadata.dart b/modules/angular2/src/core/metadata.dart index 2de191d85a..d069f28ba7 100644 --- a/modules/angular2/src/core/metadata.dart +++ b/modules/angular2/src/core/metadata.dart @@ -112,3 +112,19 @@ class Event extends EventMetadata { const Event([String bindingPropertyName]) : super(bindingPropertyName); } + +/** + * See: [HostBindingMetadata] for docs. + */ +class HostBinding extends HostBindingMetadata { + const HostBinding([String hostPropertyName]) + : super(hostPropertyName); +} + +/** + * See: [HostListenerMetadata] for docs. + */ +class HostListener extends HostListenerMetadata { + const HostListener(String eventName, [List args]) + : super(eventName, args); +} \ No newline at end of file diff --git a/modules/angular2/src/core/metadata.ts b/modules/angular2/src/core/metadata.ts index 0ed9646c71..ba55eb857e 100644 --- a/modules/angular2/src/core/metadata.ts +++ b/modules/angular2/src/core/metadata.ts @@ -15,7 +15,9 @@ export { PipeMetadata, LifecycleEvent, PropertyMetadata, - EventMetadata + EventMetadata, + HostBindingMetadata, + HostListenerMetadata } from './metadata/directives'; export {ViewMetadata, ViewEncapsulation} from './metadata/view'; @@ -33,7 +35,9 @@ import { PipeMetadata, LifecycleEvent, PropertyMetadata, - EventMetadata + EventMetadata, + HostBindingMetadata, + HostListenerMetadata } from './metadata/directives'; import {ViewMetadata, ViewEncapsulation} from './metadata/view'; @@ -447,6 +451,45 @@ export interface EventFactory { new (bindingPropertyName?: string): any; } +/** + * {@link HostBindingMetadata} factory for creating decorators. + * + * ## Example as TypeScript Decorator + * + * ``` + * @Directive({ + * selector: 'sample-dir' + * }) + * class SampleDir { + * @HostBinding() prop1; // Same as @HostBinding('prop1') prop1; + * @HostBinding("el-prop") prop1; + * } + * ``` + */ +export interface HostBindingFactory { + (hostPropertyName?: string): any; + new (hostPropertyName?: string): any; +} + +/** + * {@link HostListenerMetadata} factory for creating decorators. + * + * ## Example as TypeScript Decorator + * + * ``` + * @Directive({ + * selector: 'sample-dir' + * }) + * class SampleDir { + * @HostListener("change", ['$event.target.value']) onChange(value){} + * } + * ``` + */ +export interface HostListenerFactory { + (eventName: string, args?: string[]): any; + new (eventName: string, args?: string[]): any; +} + /** * {@link ComponentMetadata} factory function. */ @@ -492,4 +535,14 @@ export var Property: PropertyFactory = makePropDecorator(PropertyMetadata); /** * {@link EventMetadata} factory function. */ -export var Event: EventFactory = makePropDecorator(EventMetadata); \ No newline at end of file +export var Event: EventFactory = makePropDecorator(EventMetadata); + +/** + * {@link HostBindingMetadata} factory function. + */ +export var HostBinding: HostBindingFactory = makePropDecorator(HostBindingMetadata); + +/** + * {@link HostListenerMetadata} factory function. + */ +export var HostListener: HostListenerFactory = makePropDecorator(HostListenerMetadata); \ No newline at end of file diff --git a/modules/angular2/src/core/metadata/directives.ts b/modules/angular2/src/core/metadata/directives.ts index 84fd1e99d5..4c9f6dd50a 100644 --- a/modules/angular2/src/core/metadata/directives.ts +++ b/modules/angular2/src/core/metadata/directives.ts @@ -1109,4 +1109,68 @@ export class PropertyMetadata { @CONST() export class EventMetadata { constructor(public bindingPropertyName?: string) {} +} + +/** + * Declare a host property binding. + * + * ## Example + * + * ``` + * @Directive({ + * selector: 'sample-dir' + * }) + * class SampleDir { + * @HostBinding() prop1; // Same as @HostBinding('prop1') prop1; + * @HostBinding("el-prop") prop2; + * } + * ``` + * + * This is equivalent to + * + * ``` + * @Directive({ + * selector: 'sample-dir', + * host: {'[prop1]': 'prop1', '[el-prop]': 'prop2'} + * }) + * class SampleDir { + * prop1; + * prop2; + * } + * ``` + */ +@CONST() +export class HostBindingMetadata { + constructor(public hostPropertyName?: string) {} +} + +/** + * Declare a host listener. + * + * ## Example + * + * ``` + * @Directive({ + * selector: 'sample-dir' + * }) + * class SampleDir { + * @HostListener("change", ['$event.target.value']) onChange(value){} + * } + * ``` + * + * This is equivalent to + * + * ``` + * @Directive({ + * selector: 'sample-dir', + * host: {'(change)': 'onChange($event.target.value)'} + * }) + * class SampleDir { + * onChange(value){} + * } + * ``` + */ +@CONST() +export class HostListenerMetadata { + constructor(public eventName: string, public args?: string[]) {} } \ No newline at end of file diff --git a/modules/angular2/test/core/compiler/directive_resolver_spec.ts b/modules/angular2/test/core/compiler/directive_resolver_spec.ts index d8f9f5a4cc..68c233246d 100644 --- a/modules/angular2/test/core/compiler/directive_resolver_spec.ts +++ b/modules/angular2/test/core/compiler/directive_resolver_spec.ts @@ -1,6 +1,13 @@ import {ddescribe, describe, it, iit, expect, beforeEach} from 'angular2/test_lib'; import {DirectiveResolver} from 'angular2/src/core/compiler/directive_resolver'; -import {DirectiveMetadata, Directive, Property, Event} from 'angular2/metadata'; +import { + DirectiveMetadata, + Directive, + Property, + Event, + HostBinding, + HostListener +} from 'angular2/metadata'; @Directive({selector: 'someDirective'}) class SomeDirective { @@ -11,7 +18,7 @@ class SomeChildDirective extends SomeDirective { } @Directive({selector: 'someDirective', properties: ['c']}) -class SomeDirectiveWithProps { +class SomeDirectiveWithProperties { @Property() a; @Property("renamed") b; c; @@ -40,6 +47,23 @@ class SomeDirectiveWithGetterEvents { } } +@Directive({selector: 'someDirective', host: {'[c]': 'c'}}) +class SomeDirectiveWithHostBindings { + @HostBinding() a; + @HostBinding("renamed") b; + c; +} + +@Directive({selector: 'someDirective', host: {'(c)': 'onC()'}}) +class SomeDirectiveWithHostListeners { + @HostListener('a') + onA() { + } + @HostListener('b', ['$event.value']) + onB(value) { + } +} + class SomeDirectiveWithoutMetadata {} @@ -52,7 +76,8 @@ export function main() { it('should read out the Directive metadata', () => { var directiveMetadata = resolver.resolve(SomeDirective); expect(directiveMetadata) - .toEqual(new DirectiveMetadata({selector: 'someDirective', properties: [], events: []})); + .toEqual(new DirectiveMetadata( + {selector: 'someDirective', properties: [], events: [], host: {}})); }); it('should throw if not matching metadata is found', () => { @@ -63,39 +88,44 @@ export function main() { it('should not read parent class Directive metadata', function() { var directiveMetadata = resolver.resolve(SomeChildDirective); expect(directiveMetadata) - .toEqual( - new DirectiveMetadata({selector: 'someChildDirective', properties: [], events: []})); + .toEqual(new DirectiveMetadata( + {selector: 'someChildDirective', properties: [], events: [], host: {}})); }); describe('properties', () => { it('should append directive properties', () => { - var directiveMetadata = resolver.resolve(SomeDirectiveWithProps); - expect(directiveMetadata) - .toEqual(new DirectiveMetadata( - {selector: 'someDirective', properties: ['c', 'a', 'b: renamed'], events: []})); + var directiveMetadata = resolver.resolve(SomeDirectiveWithProperties); + expect(directiveMetadata.properties).toEqual(['c', 'a', 'b: renamed']); }); it('should work with getters and setters', () => { var directiveMetadata = resolver.resolve(SomeDirectiveWithSetterProps); - expect(directiveMetadata) - .toEqual(new DirectiveMetadata( - {selector: 'someDirective', properties: ['a: renamed'], events: []})); + expect(directiveMetadata.properties).toEqual(['a: renamed']); }); }); describe('events', () => { it('should append directive events', () => { var directiveMetadata = resolver.resolve(SomeDirectiveWithEvents); - expect(directiveMetadata) - .toEqual(new DirectiveMetadata( - {selector: 'someDirective', properties: [], events: ['c', 'a', 'b: renamed']})); + expect(directiveMetadata.events).toEqual(['c', 'a', 'b: renamed']); }); it('should work with getters and setters', () => { var directiveMetadata = resolver.resolve(SomeDirectiveWithGetterEvents); - expect(directiveMetadata) - .toEqual(new DirectiveMetadata( - {selector: 'someDirective', properties: [], events: ['a: renamed']})); + expect(directiveMetadata.events).toEqual(['a: renamed']); + }); + }); + + describe('host', () => { + it('should append host bindings', () => { + var directiveMetadata = resolver.resolve(SomeDirectiveWithHostBindings); + expect(directiveMetadata.host).toEqual({'[c]': 'c', '[a]': 'a', '[renamed]': 'b'}); + }); + + it('should append host listeners', () => { + var directiveMetadata = resolver.resolve(SomeDirectiveWithHostListeners); + expect(directiveMetadata.host) + .toEqual({'(c)': 'onC()', '(a)': 'onA()', '(b)': 'onB($event.value)'}); }); }); }); diff --git a/modules/angular2/test/core/compiler/integration_spec.ts b/modules/angular2/test/core/compiler/integration_spec.ts index f71333cbe8..2a60b1f923 100644 --- a/modules/angular2/test/core/compiler/integration_spec.ts +++ b/modules/angular2/test/core/compiler/integration_spec.ts @@ -73,7 +73,9 @@ import { Query, Pipe, Property, - Event + Event, + HostBinding, + HostListener } from 'angular2/metadata'; import {QueryList} from 'angular2/src/core/compiler/query_list'; @@ -1625,6 +1627,24 @@ export function main() { }); })); + it('should support host binding decorators', + inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { + tcb.overrideView(MyComp, new ViewMetadata({ + template: '', + directives: [DirectiveWithPropDecorators] + })) + .createAsync(MyComp) + .then((rootTC) => { + rootTC.detectChanges(); + var dir = rootTC.componentViewChildren[0].inject(DirectiveWithPropDecorators); + dir.myAttr = "aaa"; + + rootTC.detectChanges(); + expect(DOM.getOuterHTML(rootTC.componentViewChildren[0].nativeElement)) + .toContain('my-attr="aaa"'); + async.done(); + }); + })); if (DOM.supportsDOMEvents()) { it('should support events decorators', @@ -1647,6 +1667,26 @@ export function main() { expect(rootTC.componentInstance.ctxProp).toEqual("called"); }))); + + + it('should support host listener decorators', + inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, + async) => { + tcb.overrideView(MyComp, new ViewMetadata({ + template: '', + directives: [DirectiveWithPropDecorators] + })) + .createAsync(MyComp) + .then((rootTC) => { + rootTC.detectChanges(); + var dir = rootTC.componentViewChildren[0].inject(DirectiveWithPropDecorators); + var native = rootTC.componentViewChildren[0].nativeElement; + native.click(); + + expect(dir.target).toBe(native); + async.done(); + }); + })); } }); }); @@ -2203,8 +2243,16 @@ class DirectiveThrowingAnError { @Directive({selector: 'with-prop-decorators'}) class DirectiveWithPropDecorators { + target; + @Property("elProp") dirProp: string; @Event('elEvent') event = new EventEmitter(); + @HostBinding("attr.my-attr") myAttr: string; + @HostListener("click", ["$event.target"]) + onClick(target) { + this.target = target; + } + fireEvent(msg) { ObservableWrapper.callNext(this.event, msg); } } \ No newline at end of file