From 85f54d2436034eb3254a8d1e9fe36116ee6000da Mon Sep 17 00:00:00 2001 From: vsavkin Date: Mon, 2 Nov 2015 12:49:22 -0800 Subject: [PATCH 1/5] refactor(playground): update the order management example to use the recommended APIs --- .../playground/src/order_management/index.ts | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/modules/playground/src/order_management/index.ts b/modules/playground/src/order_management/index.ts index 1b27c4128668..b6836f5ff3df 100644 --- a/modules/playground/src/order_management/index.ts +++ b/modules/playground/src/order_management/index.ts @@ -10,7 +10,9 @@ import { Provider, EventEmitter, FORM_DIRECTIVES, - Injectable + Injectable, + Input, + Output } from 'angular2/core'; import {ListWrapper} from 'angular2/src/core/facade/collection'; @@ -79,8 +81,8 @@ class DataService { // ---- components -@Component({selector: 'order-list-cmp'}) -@View({ +@Component({ + selector: 'order-list-cmp', template: `

Orders

@@ -116,8 +118,8 @@ class OrderListComponent { } -@Component({selector: 'order-item-cmp', inputs: ['item'], outputs: ['delete']}) -@View({ +@Component({ + selector: 'order-item-cmp', template: `
@@ -143,14 +145,14 @@ class OrderListComponent { directives: [FORM_DIRECTIVES] }) class OrderItemComponent { - item: OrderItem; - delete = new EventEmitter(); + @Input() item: OrderItem; + @Output() delete = new EventEmitter(); onDelete(): void { this.delete.next(this.item); } } -@Component({selector: 'order-details-cmp'}) -@View({ +@Component({ + selector: 'order-details-cmp', template: `

Selected Order

@@ -189,8 +191,9 @@ class OrderDetailsComponent { addItem(): void { this._service.addItemForOrder(this.order); } } -@Component({selector: 'order-management-app', bindings: [DataService]}) -@View({ +@Component({ + selector: 'order-management-app', + providers: [DataService], template: ` From ac17774c08ca5138a1257deb9a99432cac6a8d6f Mon Sep 17 00:00:00 2001 From: vsavkin Date: Mon, 2 Nov 2015 15:37:01 -0800 Subject: [PATCH 2/5] refactor(playground): update Zippy to use @Input and @Output --- modules/playground/src/zippy_component/zippy.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/modules/playground/src/zippy_component/zippy.ts b/modules/playground/src/zippy_component/zippy.ts index cca3d0332496..f4ad5ac08a86 100644 --- a/modules/playground/src/zippy_component/zippy.ts +++ b/modules/playground/src/zippy_component/zippy.ts @@ -1,21 +1,19 @@ -import {Component, View, EventEmitter} from 'angular2/angular2'; +import {Component, View, EventEmitter, Input, Output} from 'angular2/angular2'; import {ObservableWrapper} from 'angular2/src/core/facade/async'; -@Component( - {selector: 'zippy', inputs: ['title'], outputs: ['openHandler: open', 'closeHandler: close']}) -@View({templateUrl: 'zippy.html'}) +@Component({selector: 'zippy', templateUrl: 'zippy.html'}) export class Zippy { visible: boolean = true; - title: string = ''; - openHandler: EventEmitter = new EventEmitter(); - closeHandler: EventEmitter = new EventEmitter(); + @Input() title: string = ''; + @Output() open: EventEmitter = new EventEmitter(); + @Output() close: EventEmitter = new EventEmitter(); toggle() { this.visible = !this.visible; if (this.visible) { - ObservableWrapper.callNext(this.openHandler, null); + ObservableWrapper.callNext(this.open, null); } else { - ObservableWrapper.callNext(this.closeHandler, null); + ObservableWrapper.callNext(this.close, null); } } } From b92f50b18e941a7534cc6737ba8cc5ac59d727fc Mon Sep 17 00:00:00 2001 From: vsavkin Date: Mon, 2 Nov 2015 16:42:57 -0800 Subject: [PATCH 3/5] refactor(playground): update the order management example to use the recommended APIs --- modules/playground/src/order_management/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/playground/src/order_management/index.ts b/modules/playground/src/order_management/index.ts index b6836f5ff3df..bd9ab3e5c165 100644 --- a/modules/playground/src/order_management/index.ts +++ b/modules/playground/src/order_management/index.ts @@ -4,7 +4,6 @@ import { NgFor, Component, Directive, - View, Host, forwardRef, Provider, From 7015c9629bb040365a30e86803d6c7ea2b55372e Mon Sep 17 00:00:00 2001 From: vsavkin Date: Mon, 2 Nov 2015 16:43:04 -0800 Subject: [PATCH 4/5] refactor(playground): update Zippy to use @Input and @Output --- modules/playground/src/zippy_component/zippy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/playground/src/zippy_component/zippy.ts b/modules/playground/src/zippy_component/zippy.ts index f4ad5ac08a86..cd79ee08a5bd 100644 --- a/modules/playground/src/zippy_component/zippy.ts +++ b/modules/playground/src/zippy_component/zippy.ts @@ -1,4 +1,4 @@ -import {Component, View, EventEmitter, Input, Output} from 'angular2/angular2'; +import {Component, EventEmitter, Input, Output} from 'angular2/angular2'; import {ObservableWrapper} from 'angular2/src/core/facade/async'; @Component({selector: 'zippy', templateUrl: 'zippy.html'}) From d7b6472955002dde83133613969d91c02d9efdfe Mon Sep 17 00:00:00 2001 From: vsavkin Date: Mon, 2 Nov 2015 16:45:31 -0800 Subject: [PATCH 5/5] feat(core): make transformers handle @Input/@Output/@HostBinding/@HostListener --- .../common/directive_metadata_reader.dart | 91 +++++++++++++++++++ .../directive_processor/all_tests.dart | 28 ++++++ .../directives_files/components.dart | 46 +++++++++- 3 files changed, 164 insertions(+), 1 deletion(-) diff --git a/modules_dart/transform/lib/src/transform/common/directive_metadata_reader.dart b/modules_dart/transform/lib/src/transform/common/directive_metadata_reader.dart index ef325acfd2e9..383d437335e1 100644 --- a/modules_dart/transform/lib/src/transform/common/directive_metadata_reader.dart +++ b/modules_dart/transform/lib/src/transform/common/directive_metadata_reader.dart @@ -225,6 +225,95 @@ class _DirectiveMetadataVisitor extends Object return null; } + @override + Object visitFieldDeclaration(FieldDeclaration node) { + for (var variable in node.fields.variables) { + for (var meta in node.metadata) { + if (_isAnnotation(meta, 'Output')) { + final renamed = _getRenamedValue(meta); + if (renamed != null) { + _outputs.add('${variable.name}: ${renamed}'); + } else { + _outputs.add('${variable.name}'); + } + } + + if (_isAnnotation(meta, 'Input')) { + final renamed = _getRenamedValue(meta); + if (renamed != null) { + _inputs.add('${variable.name}: ${renamed}'); + } else { + _inputs.add('${variable.name}'); + } + } + + if (_isAnnotation(meta, 'HostBinding')) { + final renamed = _getRenamedValue(meta); + if (renamed != null) { + _host['[${renamed}]'] = '${variable.name}'; + } else { + _host['[${variable.name}]'] = '${variable.name}'; + } + } + } + } + return null; + } + + @override + Object visitMethodDeclaration(MethodDeclaration node) { + for (var meta in node.metadata) { + if (_isAnnotation(meta, 'HostListener')) { + if (meta.arguments.arguments.length == 0 || meta.arguments.arguments.length > 2) { + throw new ArgumentError( + 'Incorrect value passed to HostListener. Expected 1 or 2.'); + } + + final eventName = _getHostListenerEventName(meta); + final params = _getHostListenerParams(meta); + _host['(${eventName})'] = '${node.name}($params)'; + } + } + return null; + } + + //TODO Use AnnotationMatcher instead of string matching + bool _isAnnotation(Annotation node, String annotationName) { + var id = node.name; + final name = id is PrefixedIdentifier ? '${id.identifier}' : '$id'; + return name == annotationName; + } + + String _getRenamedValue(Annotation node) { + if (node.arguments.arguments.length == 1) { + final renamed = naiveEval(node.arguments.arguments.single); + if (renamed is! String) { + throw new ArgumentError( + 'Incorrect value. Expected a String, but got "${renamed}".'); + } + return renamed; + } else { + return null; + } + } + + String _getHostListenerEventName(Annotation node) { + final name = naiveEval(node.arguments.arguments.first); + if (name is! String) { + throw new ArgumentError( + 'Incorrect event name. Expected a String, but got "${name}".'); + } + return name; + } + + String _getHostListenerParams(Annotation node) { + if (node.arguments.arguments.length == 2) { + return naiveEval(node.arguments.arguments[1]).join(','); + } else { + return ""; + } + } + @override Object visitClassDeclaration(ClassDeclaration node) { node.metadata.accept(this); @@ -237,6 +326,8 @@ class _DirectiveMetadataVisitor extends Object _lifecycleHooks = node.implementsClause != null ? node.implementsClause.accept(_lifecycleVisitor) : const []; + + node.members.accept(this); } return null; } diff --git a/modules_dart/transform/test/transform/directive_processor/all_tests.dart b/modules_dart/transform/test/transform/directive_processor/all_tests.dart index 9597cd318fe8..6ea05410ad2c 100644 --- a/modules_dart/transform/test/transform/directive_processor/all_tests.dart +++ b/modules_dart/transform/test/transform/directive_processor/all_tests.dart @@ -494,6 +494,34 @@ void allTests() { ..prefix = 'dep2'); }); + it('should merge `outputs` from the annotation and fields.', + () async { + var model = await _testCreateModel('directives_files/components.dart'); + expect(model.types['ComponentWithOutputs'].outputs). + toEqual({'a': 'a', 'b': 'b', 'c': 'renamed'}); + }); + + it('should merge `inputs` from the annotation and fields.', + () async { + var model = await _testCreateModel('directives_files/components.dart'); + expect(model.types['ComponentWithInputs'].inputs). + toEqual({'a': 'a', 'b': 'b', 'c': 'renamed'}); + }); + + it('should merge host bindings from the annotation and fields.', + () async { + var model = await _testCreateModel('directives_files/components.dart'); + expect(model.types['ComponentWithHostBindings'].hostProperties). + toEqual({'a': 'a', 'b': 'b', 'renamed': 'c'}); + }); + + it('should merge host listeners from the annotation and fields.', + () async { + var model = await _testCreateModel('directives_files/components.dart'); + expect(model.types['ComponentWithHostListeners'].hostListeners). + toEqual({'a': 'onA()', 'b': 'onB()', 'c': 'onC(\$event.target,\$event.target.value)'}); + }); + it('should warn if @Component has a `template` and @View is present.', () async { final logger = new RecordingLogger(); diff --git a/modules_dart/transform/test/transform/directive_processor/directives_files/components.dart b/modules_dart/transform/test/transform/directive_processor/directives_files/components.dart index bba9769f3c78..beb545c82523 100644 --- a/modules_dart/transform/test/transform/directive_processor/directives_files/components.dart +++ b/modules_dart/transform/test/transform/directive_processor/directives_files/components.dart @@ -1,7 +1,7 @@ library angular2.test.transform.directive_processor.directive_files.components; import 'package:angular2/angular2.dart' - show Component, Directive, View, NgElement; + show Component, Directive, View, NgElement, Output, Input; import 'dep1.dart'; import 'dep2.dart' as dep2; @@ -18,3 +18,47 @@ class ViewFirst {} template: '', directives: [Dep, dep2.Dep]) class ComponentOnly {} + +@Component( + selector: 'component-with-outputs', + template: '', + outputs: ['a'] +) +class ComponentWithOutputs { + @Output() Object b; + @Output('renamed') Object c; +} + +@Component( + selector: 'component-with-inputs', + template: '', + inputs: ['a'] +) +class ComponentWithInputs { + @Input() Object b; + @Input('renamed') Object c; +} + +@Component( + selector: 'component-with-inputs', + template: '', + host: { + '[a]':'a' + } +) +class ComponentWithHostBindings { + @HostBinding() Object b; + @HostBinding('renamed') Object c; +} + +@Component( + selector: 'component-with-inputs', + template: '', + host: { + '(a)':'onA()' + } +) +class ComponentWithHostListeners { + @HostListener('b') void onB() {} + @HostListener('c', ['\$event.target', '\$event.target.value']) void onC(t,v) {} +}