Skip to content

Commit

Permalink
feat(directive): Add ng-attr-* interpolation support
Browse files Browse the repository at this point in the history
fix(expression_extractor): implemented support for wildcard attr selector

Fixes dart-archive#447
  • Loading branch information
nerdrew authored and jbdeboer committed Jan 28, 2014
1 parent 48bebe2 commit e2cc5b7
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 7 deletions.
15 changes: 12 additions & 3 deletions lib/core_dom/selector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class _ContainsSelector {
_ContainsSelector(this.annotation, String regexp) : regexp = new RegExp(regexp);
}

RegExp _SELECTOR_REGEXP = new RegExp(r'^(?:([\w\-]+)|(?:\.([\w\-]+))|(?:\[([\w\-]+)(?:=([^\]]*))?\]))');
RegExp _SELECTOR_REGEXP = new RegExp(r'^(?:([\w\-]+)|(?:\.([\w\-]+))|(?:\[([\w\-\*]+)(?:=([^\]]*))?\]))');
RegExp _COMMENT_COMPONENT_REGEXP = new RegExp(r'^\[([\w\-]+)(?:\=(.*))?\]$');
RegExp _CONTAINS_REGEXP = new RegExp(r'^:contains\(\/(.+)\/\)$'); //
RegExp _ATTR_CONTAINS_REGEXP = new RegExp(r'^\[\*=\/(.+)\/\]$'); //
Expand Down Expand Up @@ -153,8 +153,11 @@ class _ElementSelector {

List<_ElementSelector> selectAttr(List<DirectiveRef> refs, List<_ElementSelector> partialSelection,
dom.Node node, String attrName, String attrValue) {
if (attrValueMap.containsKey(attrName)) {
Map<String, _Directive> valuesMap = attrValueMap[attrName];

String matchingKey = _matchingKey(attrValueMap.keys, attrName);

if (matchingKey != null) {
Map<String, _Directive> valuesMap = attrValueMap[matchingKey];
if (valuesMap.containsKey('')) {
_Directive directive = valuesMap[''];
refs.add(new DirectiveRef(node, directive.type, directive.annotation, attrValue));
Expand All @@ -178,6 +181,12 @@ class _ElementSelector {
return partialSelection;
}

String _matchingKey(Iterable<String> keys, String attrName) {
return keys.firstWhere(
(key) => new RegExp('^${key.replaceAll('*', r'[\w\-]+')}\$').hasMatch(attrName),
orElse: () => null);
}

toString() => 'ElementSelector($name)';
}

Expand Down
1 change: 1 addition & 0 deletions lib/directive/module.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class NgDirectiveModule extends Module {

value(NgBooleanAttributeDirective, null);
value(NgSourceDirective, null);
value(NgAttributeDirective, null);

value(NgEventDirective, null);
value(NgStyleDirective, null);
Expand Down
29 changes: 29 additions & 0 deletions lib/directive/ng_src_boolean.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,32 @@ class NgSourceDirective {
set srcset(value) => attrs['srcset'] = value;

}

/**
* In SVG some attributes have a specific syntax. Placing `{{interpolation}}` in those
* attributes will break the attribute syntax, and browser will clear the attribute.
*
* The `ng-attr-*` is a generic way to use interpolation without breaking the attribute
* syntax validator. The `ng-attr-` part get stripped.
*
* @example
<svg>
<circle ng-attr-cx="{{cx}}"></circle>
</svg>
*/
@NgDirective(selector: '[ng-attr-*]')
class NgAttributeDirective implements NgAttachAware {
NodeAttrs _attrs;

NgAttributeDirective(NodeAttrs this._attrs);

void attach() {
_attrs.forEach((key, value) {
if (key.startsWith('ngAttr')) {
String newKey = snakecase(key.substring(6));
_attrs[newKey] = value;
_attrs.observe(snakecase(key), (newValue) => _attrs[newKey] = newValue );
}
});
}
}
14 changes: 10 additions & 4 deletions lib/tools/selector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class ContainsSelector {
}
}

RegExp _SELECTOR_REGEXP = new RegExp(r'^(?:([\w\-]+)|(?:\.([\w\-]+))|(?:\[([\w\-]+)(?:=([^\]]*))?\]))');
RegExp _SELECTOR_REGEXP = new RegExp(r'^(?:([\w\-]+)|(?:\.([\w\-]+))|(?:\[([\w\-\*]+)(?:=([^\]]*))?\]))');
RegExp _COMMENT_COMPONENT_REGEXP = new RegExp(r'^\[([\w\-]+)(?:\=(.*))?\]$');
RegExp _CONTAINS_REGEXP = new RegExp(r'^:contains\(\/(.+)\/\)$'); //
RegExp _ATTR_CONTAINS_REGEXP = new RegExp(r'^\[\*=\/(.+)\/\]$'); //
Expand Down Expand Up @@ -101,9 +101,10 @@ bool matchesNode(Node node, String selector) {
stillGood = false;
}
} else if (part.attrName != null) {
if (part.attrValue == '' ?
node.attributes[part.attrName] == null :
node.attributes[part.attrName] != part.attrValue) {
String matchingKey = _matchingKey(node.attributes.keys, part.attrName);
if (matchingKey == null || part.attrValue == '' ?
node.attributes[matchingKey] == null :
node.attributes[matchingKey] != part.attrValue) {
stillGood = false;
}
}
Expand All @@ -122,3 +123,8 @@ bool matchesNode(Node node, String selector) {
}
return false;
}

String _matchingKey(Iterable keys, String attrName) =>
keys.firstWhere(
(key) => new RegExp('^${attrName.replaceAll('*', r'[\w\-]+')}\$').hasMatch(key.toString()),
orElse: () => null);
10 changes: 10 additions & 0 deletions test/core_dom/selector_spec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import '../_specs.dart';
@NgDirective(selector:'b') class _BElement{}
@NgDirective(selector:'.b') class _BClass{}
@NgDirective(selector:'[directive]') class _DirectiveAttr{}
@NgDirective(selector:'[wildcard-*]') class _WildcardDirectiveAttr{}
@NgDirective(selector:'[directive=d][foo=f]') class _DirectiveFooAttr{}
@NgDirective(selector:'b[directive]') class _BElementDirectiveAttr{}
@NgDirective(selector:'[directive=value]') class _DirectiveValueAttr{}
Expand Down Expand Up @@ -43,6 +44,7 @@ main() {
..type(_BElement)
..type(_BClass)
..type(_DirectiveAttr)
..type(_WildcardDirectiveAttr)
..type(_DirectiveFooAttr)
..type(_BElementDirectiveAttr)
..type(_DirectiveValueAttr)
Expand Down Expand Up @@ -126,6 +128,14 @@ main() {
]));
});

it('should match attribute names', () {
expect(selector(element = e('<div wildcard-match=ignored></div>')),
toEqualsDirectiveInfos([
{ "selector": '[wildcard-*]', "value": 'ignored',
"element": element, "name": 'wildcard-match'}
]));
});

it('should match text', () {
expect(selector(element = e('before-abc-after')),
toEqualsDirectiveInfos([
Expand Down
42 changes: 42 additions & 0 deletions test/directive/ng_src_boolean_spec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -204,4 +204,46 @@ main() {
expect(_.rootElement.attributes['href']).toEqual('http://server');
}));
});

describe('ngAttr', () {
TestBed _;
beforeEach(inject((TestBed tb) => _ = tb));

it('should interpolate the expression and bind to *', inject(() {
_.compile('<div ng-attr-foo="some/{{id}}"></div>');
_.rootScope.$digest();
expect(_.rootElement.attributes['foo']).toEqual('some/');

_.rootScope.$apply(() {
_.rootScope.id = 1;
});
expect(_.rootElement.attributes['foo']).toEqual('some/1');
}));


it('should bind * and merge with other attrs', inject(() {
_.compile('<div ng-attr-bar="{{bar}}" ng-attr-bar2="{{bar2}}" bam="{{bam}}"></a>');
_.rootScope.bar = 'foo';
_.rootScope.bar2 = 'foo2';
_.rootScope.bam = 'boom';
_.rootScope.$digest();
expect(_.rootElement.attributes['bar']).toEqual('foo');
expect(_.rootElement.attributes['bar2']).toEqual('foo2');
expect(_.rootElement.attributes['bam']).toEqual('boom');
_.rootScope.bar = 'FOO';
_.rootScope.bar2 = 'FOO2';
_.rootScope.bam = 'BOOM';
_.rootScope.$digest();
expect(_.rootElement.attributes['bar']).toEqual('FOO');
expect(_.rootElement.attributes['bar2']).toEqual('FOO2');
expect(_.rootElement.attributes['bam']).toEqual('BOOM');
}));


it('should bind * even if no interpolation', inject(() {
_.compile('<a ng-attr-quack="vanilla"></a>');
_.rootScope.$digest();
expect(_.rootElement.attributes['quack']).toEqual('vanilla');
}));
});
}
10 changes: 10 additions & 0 deletions test/tools/selector_spec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ main() => describe('selector', () {
expect(matchesNode(node, '[*=/xyzz/]'), isFalse);
});

it('should match whildcard attributes', () {
var node = new Element.html('<div attr-foo="blah"></div>');
expect(matchesNode(node, '[attr-*]'), isTrue);
expect(matchesNode(node, '[attr-*=blah]'), isTrue);
expect(matchesNode(node, '[attr-*=halb]'), isFalse);
expect(matchesNode(node, '[foo-*]'), isFalse);
expect(matchesNode(node, '[foo-*=blah]'), isFalse);
expect(matchesNode(node, '[foo-*=halb]'), isFalse);
});

it('should match text', () {
var node = new Text('before-abc-after');
expect(matchesNode(node, ':contains(/abc/)'), isTrue);
Expand Down

0 comments on commit e2cc5b7

Please sign in to comment.