Skip to content
This repository has been archived by the owner on Feb 22, 2018. It is now read-only.

Commit

Permalink
feat(compiler): allow specifying attribute mappings using annotations
Browse files Browse the repository at this point in the history
Closes #227
  • Loading branch information
pavelgj committed Nov 18, 2013
1 parent 33e30db commit 50585eb
Show file tree
Hide file tree
Showing 7 changed files with 301 additions and 21 deletions.
158 changes: 155 additions & 3 deletions lib/core/directive.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
part of angular.core;

class NgAnnotation {
abstract class NgAnnotation {
/**
* CSS selector which will trigger this component/directive.
* CSS Selectors are limited to a single element and can contain:
Expand Down Expand Up @@ -159,6 +159,7 @@ class NgAnnotation {
operator==(other) =>
other is NgAnnotation && this.selector == other.selector;

NgAnnotation cloneWithNewMap(newMap);
}


Expand Down Expand Up @@ -228,6 +229,21 @@ class NgComponent extends NgAnnotation {
map: map,
exportExpressions: exportExpressions,
exportExpressionAttrs: exportExpressionAttrs);

NgAnnotation cloneWithNewMap(newMap) =>
new NgComponent(
template: this.template,
templateUrl: this.templateUrl,
cssUrl: this.cssUrl,
applyAuthorStyles: this.applyAuthorStyles,
resetStyleInheritance: this.resetStyleInheritance,
publishAs: this.publishAs,
map: newMap,
selector: this.selector,
visibility: this.visibility,
publishTypes: this.publishTypes,
exportExpressions: this.exportExpressions,
exportExpressionAttrs: this.exportExpressionAttrs);
}

RegExp _ATTR_NAME = new RegExp(r'\[([^\]]+)\]$');
Expand Down Expand Up @@ -263,6 +279,17 @@ class NgDirective extends NgAnnotation {
publishTypes: publishTypes, publishAs: publishAs, map: map,
exportExpressions: exportExpressions,
exportExpressionAttrs: exportExpressionAttrs);

NgAnnotation cloneWithNewMap(newMap) =>
new NgDirective(
children: this.children,
publishAs: this.publishAs,
map: newMap,
selector: this.selector,
visibility: this.visibility,
publishTypes: this.publishTypes,
exportExpressions: this.exportExpressions,
exportExpressionAttrs: this.exportExpressionAttrs);
}

/**
Expand Down Expand Up @@ -299,6 +326,78 @@ class NgController extends NgDirective {
publishTypes: publishTypes, publishAs: publishAs, map: map,
exportExpressions: exportExpressions,
exportExpressionAttrs: exportExpressionAttrs);

NgAnnotation cloneWithNewMap(newMap) =>
new NgController(
children: this.children,
publishAs: this.publishAs,
map: newMap,
selector: this.selector,
visibility: this.visibility,
publishTypes: this.publishTypes,
exportExpressions: this.exportExpressions,
exportExpressionAttrs: this.exportExpressionAttrs);
}

abstract class AttrFieldAnnotation {
final String attrName;
const AttrFieldAnnotation(this.attrName);
String get mappingSpec;
}

/**
* When applied as an annotation on a directive field specifies that
* the field is to be mapped to DOM attribute with the provided [attrName].
* The value of the attribute to be treated as a string, equivalent
* to `@` specification.
*/
class NgAttr extends AttrFieldAnnotation {
final mappingSpec = '@';
const NgAttr(String attrName) : super(attrName);
}

/**
* When applied as an annotation on a directive field specifies that
* the field is to be mapped to DOM attribute with the provided [attrName].
* The value of the attribute to be treated as a one-way expession, equivalent
* to `=>` specification.
*/
class NgOneWay extends AttrFieldAnnotation {
final mappingSpec = '=>';
const NgOneWay(String attrName) : super(attrName);
}

/**
* When applied as an annotation on a directive field specifies that
* the field is to be mapped to DOM attribute with the provided [attrName].
* The value of the attribute to be treated as a one time one-way expession,
* equivalent to `=>!` specification.
*/
class NgOneWayOneTime extends AttrFieldAnnotation {
final mappingSpec = '=>!';
const NgOneWayOneTime(String attrName) : super(attrName);
}

/**
* When applied as an annotation on a directive field specifies that
* the field is to be mapped to DOM attribute with the provided [attrName].
* The value of the attribute to be treated as a two-way expession,
* equivalent to `<=>` specification.
*/
class NgTwoWay extends AttrFieldAnnotation {
final mappingSpec = '<=>';
const NgTwoWay(String attrName) : super(attrName);
}

/**
* When applied as an annotation on a directive field specifies that
* the field is to be mapped to DOM attribute with the provided [attrName].
* The value of the attribute to be treated as a callback expession,
* equivalent to `&` specification.
*/
class NgCallback extends AttrFieldAnnotation {
final mappingSpec = '&';
const NgCallback(String attrName) : super(attrName);
}

/**
Expand All @@ -320,6 +419,59 @@ abstract class NgDetachAware {
}

class DirectiveMap extends AnnotationMap<NgAnnotation> {
DirectiveMap(Injector injector, MetadataExtractor metadataExtractor)
: super(injector, metadataExtractor);
DirectiveMap(Injector injector, MetadataExtractor metadataExtractor,
FieldMetadataExtractor fieldMetadataExtractor)
: super(injector, metadataExtractor) {
Map<NgAnnotation, Type> directives = {};
forEach((NgAnnotation annotation, Type type) {
var match;
var fieldMetadata = fieldMetadataExtractor(type);
if (fieldMetadata.isNotEmpty) {
var newMap = annotation.map == null ? {} : new Map.from(annotation.map);
fieldMetadata.forEach((String fieldName, AttrFieldAnnotation ann) {
var attrName = ann.attrName;
if (newMap.containsKey(attrName)) {
throw 'Mapping for attribute $attrName is already defined (while '
'processing annottation for field $fieldName of $type)';
}
newMap[attrName] = '${ann.mappingSpec}$fieldName';
});
annotation = annotation.cloneWithNewMap(newMap);
}
directives[annotation] = type;
});
_map.clear();
_map.addAll(directives);
}
}

class FieldMetadataExtractor {
List<TypeMirror> _fieldAnnotations = [reflectType(NgAttr),
reflectType(NgOneWay), reflectType(NgOneWayOneTime),
reflectType(NgTwoWay), reflectType(NgCallback)];

Map<String, AttrFieldAnnotation> call(Type type) {
ClassMirror cm = reflectType(type);
Map<String, AttrFieldAnnotation> fields = <String, AttrFieldAnnotation>{};
cm.declarations.forEach((Symbol name, DeclarationMirror decl) {
if (decl is VariableMirror ||
(decl is MethodMirror && (decl.isGetter || decl.isSetter))) {
var fieldName = MirrorSystem.getName(name);
if (decl is MethodMirror && decl.isSetter) {
// Remove = from the end of the setter.
fieldName = fieldName.substring(0, fieldName.length - 1);
}
decl.metadata.forEach((InstanceMirror meta) {
if (_fieldAnnotations.contains(meta.type)) {
if (fields[fieldName] != null) {
throw 'Attribute annotation for $fieldName is defined more '
'than once in $type';
}
fields[fieldName] = meta.reflectee as AttrFieldAnnotation;
}
});
}
});
return fields;
}
}
1 change: 1 addition & 0 deletions lib/core/module.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class NgCoreModule extends Module {
type(ScopeDigestTTL);

type(MetadataExtractor);
type(FieldMetadataExtractor);
type(Cache);
type(DirectiveMap);
type(ExceptionHandler);
Expand Down
10 changes: 10 additions & 0 deletions lib/core/registry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ abstract class AnnotationMap<K> {
}

forEach(fn(K, Type)) => _map.forEach(fn);

List<K> annotationsFor(Type type) {
var res = <K>[];
forEach((ann, annType) {
if (annType == type) {
res.add(ann);
}
});
return res;
}
}

class MetadataExtractor {
Expand Down
2 changes: 1 addition & 1 deletion lib/tools/source_metadata_extractor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ library angular.source_metadata_extractor ;
import 'package:analyzer/src/generated/ast.dart';

import 'source_crawler.dart';
import 'utils.dart';
import '../utils.dart';
import 'common.dart';

const String _COMPONENT = '-component';
Expand Down
17 changes: 0 additions & 17 deletions lib/tools/utils.dart

This file was deleted.

16 changes: 16 additions & 0 deletions lib/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,19 @@ relaxFnArgs(Function fn) {
};
}
}

camelcase(String s) {
var part = s.split('-').map((s) => s.toLowerCase());
if (part.length <= 1) {
return part.join();
}
return part.first + part.skip(1).map(capitalize).join();
}

capitalize(String s) => s.substring(0, 1).toUpperCase() + s.substring(1);

var SNAKE_CASE_REGEXP = new RegExp("[A-Z]");

snakecase(String name, [separator = '-']) =>
name.replaceAllMapped(SNAKE_CASE_REGEXP, (Match match) =>
(match.start != 0 ? separator : '') + match.group(0).toLowerCase());
118 changes: 118 additions & 0 deletions test/core_dom/directive_spec.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
library directive_spec;

import '../_specs.dart';

main() => describe('DirectiveMap', () {

beforeEach(module((Module module) {
module
..type(AnnotatedIoComponent);
}));

it('should extract attr map from annotated component', inject((DirectiveMap directives) {
var annotations = directives.annotationsFor(AnnotatedIoComponent);
expect(annotations.length).toEqual(1);
expect(annotations[0] is NgComponent).toBeTruthy();

NgComponent annotation = annotations[0];
expect(annotation.map).toEqual({
'foo': '=>foo',
'attr': '@attr',
'expr': '<=>expr',
'expr-one-way': '=>exprOneWay',
'expr-one-way-one-shot': '=>!exprOneWayOneShot',
'callback': '&callback',
'expr-one-way2': '=>exprOneWay2',
'expr-two-way': '<=>exprTwoWay'
});
}));

describe('exceptions', () {
it('should throw when annotation is for existing mapping', () {
var module = new Module()
..type(DirectiveMap)
..type(Bad1Component)
..type(MetadataExtractor)
..type(FieldMetadataExtractor);

var injector = new DynamicInjector(modules: [module]);
expect(() {
injector.get(DirectiveMap);
}).toThrow('Mapping for attribute foo is already defined (while '
'processing annottation for field foo of Bad1Component)');
});

it('should throw when annotated both getter and setter', () {
var module = new Module()
..type(DirectiveMap)
..type(Bad2Component)
..type(MetadataExtractor)
..type(FieldMetadataExtractor);

var injector = new DynamicInjector(modules: [module]);
expect(() {
injector.get(DirectiveMap);
}).toThrow('Attribute annotation for foo is defined more than once '
'in Bad2Component');
});
});
});

@NgComponent(
selector: 'annotated-io',
template: r'<content></content>',
map: const {
'foo': '=>foo'
}
)
class AnnotatedIoComponent {
AnnotatedIoComponent(Scope scope) {
scope.$root.ioComponent = this;
}

@NgAttr('attr')
String attr;

@NgTwoWay('expr')
String expr;

@NgOneWay('expr-one-way')
String exprOneWay;

@NgOneWayOneTime('expr-one-way-one-shot')
String exprOneWayOneShot;

@NgCallback('callback')
Function callback;

@NgOneWay('expr-one-way2')
set exprOneWay2(val) {}

@NgTwoWay('expr-two-way')
get exprTwoWay => null;
set exprTwoWay(val) {}
}

@NgComponent(
selector: 'bad1',
template: r'<content></content>',
map: const {
'foo': '=>foo'
}
)
class Bad1Component {
@NgOneWay('foo')
String foo;
}

@NgComponent(
selector: 'bad2',
template: r'<content></content>'
)
class Bad2Component {
@NgOneWay('foo')
get foo => null;

@NgOneWay('foo')
set foo(val) {}
}

0 comments on commit 50585eb

Please sign in to comment.