From 16bc238f104c5cc2486202b5a6452f7ba6637a94 Mon Sep 17 00:00:00 2001 From: vsavkin Date: Mon, 2 Nov 2015 16:45:31 -0800 Subject: [PATCH] feat(core): make transformers handle @Input/@Output/@HostBinding/@HostListener Closes #5080 --- .../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 ef325acfd2..383d437335 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 9597cd318f..6ea05410ad 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 bba9769f3c..beb545c825 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) {} +}