Skip to content

Commit 544a9b9

Browse files
committed
[WIP] fix(parser): Allow access to Map properties in expressions
- E.g. `map.keys`, `map.length`, etc. This is a proposed solution to dart-archive#394. - Add support for the watching of map properties whose types are primitive (not collections). Work still needs to be done for map.keys and map.values.
1 parent 96c1a49 commit 544a9b9

File tree

7 files changed

+57
-5
lines changed

7 files changed

+57
-5
lines changed

bin/parser_generator_for_spec.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ main(arguments) {
101101
'items[1].name',
102102
'list[3] = 2',
103103
'map["square"] = 6',
104+
'map.isEmpty',
105+
'map.isNotEmpty',
106+
'map.keys',
107+
'map.length',
108+
'map.values',
104109
'method',
105110
'method()',
106111
'notAFn()',

lib/change_detection/dirty_checking_change_detector.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ library dirty_checking_change_detector;
33
import 'dart:mirrors';
44
import 'dart:collection';
55
import 'package:angular/change_detection/change_detection.dart';
6+
import 'package:angular/core/parser/utils.dart' show isMapProperty;
67

78
typedef FieldGetter(object);
89

@@ -424,7 +425,9 @@ class DirtyCheckingRecord<H> implements Record<H>, WatchRecord<H> {
424425
return;
425426
}
426427

427-
if (obj is Map) {
428+
if (obj is Map && (!isMapProperty(field)
429+
|| field == "values") // TODO: seems to be treated specially re. NgForm
430+
) {
428431
_mode = _MODE_MAP_FIELD_;
429432
_instanceMirror = null;
430433
} else if (_getter != null) {

lib/core/parser/eval_access.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ abstract class AccessReflective {
8080
if (holder == null) {
8181
_cachedKind = CACHED_VALUE;
8282
return _cachedValue = null;
83-
} else if (holder is Map) {
83+
} else if (holder is Map && !isMapProperty(name)) {
8484
_cachedKind = CACHED_MAP;
8585
_cachedValue = null;
8686
return holder[name];
@@ -164,7 +164,7 @@ abstract class AccessFast {
164164

165165
_eval(holder) {
166166
if (holder == null) return null;
167-
return (holder is Map) ? holder[name] : getter(holder);
167+
return (holder is Map && !isMapProperty(name)) ? holder[name] : getter(holder);
168168
}
169169

170170
_assign(scope, holder, value) {

lib/core/parser/utils.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,13 @@ setKeyed(object, key, value) {
103103
Symbol newSymbol(String name) {
104104
return isReservedWord(name) ? null : new Symbol(name);
105105
}
106+
107+
final Set<String> _MAP_PROPERTIES = new Set<String>.from([
108+
"hashCode", // any reason to exclude this?
109+
"isEmpty",
110+
"isNotEmpty",
111+
"keys",
112+
"length",
113+
"runtimeType", // any reason to exclude this?
114+
"values"]);
115+
bool isMapProperty(String key) => _MAP_PROPERTIES.contains(key);

lib/tools/parser_generator/dart_code_gen.dart

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ library dart_code_gen;
22

33
import 'package:angular/utils.dart' show isReservedWord;
44
import 'package:angular/core/parser/syntax.dart';
5+
import 'package:angular/core/parser/utils.dart';
56

67
escape(String s) => s.replaceAllMapped(new RegExp(r'(\"|\$|\n)'), (m) {
78
var char = m[1];
@@ -11,7 +12,7 @@ escape(String s) => s.replaceAllMapped(new RegExp(r'(\"|\$|\n)'), (m) {
1112

1213
class DartCodeGen {
1314
final HelperMap getters = new HelperMap('_',
14-
getterTemplate, getterTemplateForReserved);
15+
getterTemplate, getterTemplateForReserved, getterTemplateWithoutMapTest);
1516
final HelperMap holders = new HelperMap('_ensure\$',
1617
holderTemplate, holderTemplateForReserved);
1718
final HelperMap setters = new HelperMap('_set\$',
@@ -226,14 +227,17 @@ class HelperMap {
226227
final String prefix;
227228
final Function template;
228229
final Function templateForReserved;
230+
final Function templateWithoutMapTest;
229231

230-
HelperMap(this.prefix, this.template, this.templateForReserved);
232+
HelperMap(this.prefix, this.template, this.templateForReserved, [this.templateWithoutMapTest]);
231233

232234
String lookup(String key) {
233235
String name = _computeName(key);
234236
if (helpers.containsKey(key)) return name;
235237
helpers[key] = isReservedWord(key)
236238
? templateForReserved(name, key)
239+
: isMapProperty(key) && templateWithoutMapTest != null
240+
? templateWithoutMapTest(name, key)
237241
: template(name, key);
238242
return name;
239243
}
@@ -264,6 +268,9 @@ $name(o) {
264268
}
265269
""";
266270

271+
String getterTemplateWithoutMapTest(String name, String key) => """
272+
$name(o) => o == null ? null : o.$key;
273+
""";
267274

268275
// ------------------------------------------------------------------
269276
// Templates for generated holders (getters for assignment).

test/core/parser/parser_spec.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,16 @@ main() {
743743
});
744744

745745

746+
it('should evaluate map item and field access', () {
747+
context['map'] = { 'a': 1.1, 'b': 'B' };
748+
expect(eval("map.isEmpty")).toEqual(false);
749+
expect(eval("map.isNotEmpty")).toEqual(true);
750+
expect(eval("map.keys")).toEqual(['a', 'b']);
751+
expect(eval("map.length")).toEqual(2);
752+
expect(eval("map.values")).toEqual([1.1, 'B']);
753+
});
754+
755+
746756
it('should evaluate JSON', () {
747757
expect(eval("[{}]")).toEqual([{}]);
748758
expect(eval("[{a:[]}, {b:1}]")).toEqual([{"a":[]},{"b":1}]);

test/core/scope_spec.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,23 @@ void main() {
4848
expect(logger).toEqual(['AB', '123', 'XYZ']);
4949
}));
5050

51+
it('should watch map property of a primitive type', inject((Logger logger, Map context, RootScope rootScope) {
52+
context['a'] = {'b': 'AB', 'lengthx' : 2};
53+
rootScope.watch('a.length', (value, previous) => logger(value));
54+
rootScope.watch('a.isEmpty', (value, previous) => logger(value));
55+
rootScope.watch('a.isNotEmpty', (value, previous) => logger(value));
56+
rootScope.digest();
57+
expect(logger).toEqual([2, false, true]);
58+
context['a']['c'] = '123';
59+
logger.clear();
60+
rootScope.digest();
61+
expect(logger).toEqual([3]);
62+
context['a'] = {};
63+
logger.clear();
64+
rootScope.digest();
65+
expect(logger).toEqual([0, true, false]);
66+
}));
67+
5168
it('should watch math operations', inject((Logger logger, Map context, RootScope rootScope) {
5269
context['a'] = 1;
5370
context['b'] = 2;

0 commit comments

Comments
 (0)