From 655dc14b2affca85e00d8c30d75fa6934ba11bf8 Mon Sep 17 00:00:00 2001 From: Patrice Chalin Date: Thu, 20 Mar 2014 11:14:29 -0500 Subject: [PATCH] feat(filters): add filters in support of pure fields and methods, and to observe lists and maps Closes #359, #394, #397, #757. Relates to #772, #773. --- lib/filter/module.dart | 5 +++ lib/filter/pure.dart | 64 ++++++++++++++++++++++++++++++++++++++ test/core/scope_spec.dart | 17 ++++++++-- test/filter/pure_spec.dart | 49 +++++++++++++++++++++++++++++ 4 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 lib/filter/pure.dart create mode 100644 test/filter/pure_spec.dart diff --git a/lib/filter/module.dart b/lib/filter/module.dart index 5c90c5af4..1a251334a 100644 --- a/lib/filter/module.dart +++ b/lib/filter/module.dart @@ -1,6 +1,7 @@ library angular.filter; import 'dart:convert' show JSON; +import 'dart:mirrors'; import 'package:intl/intl.dart'; import 'package:di/di.dart'; import 'package:angular/core/module.dart'; @@ -15,6 +16,7 @@ part 'lowercase.dart'; part 'number.dart'; part 'order_by.dart'; part 'uppercase.dart'; +part 'pure.dart'; class NgFilterModule extends Module { NgFilterModule() { @@ -27,5 +29,8 @@ class NgFilterModule extends Module { type(NumberFilter); type(OrderByFilter); type(UppercaseFilter); + type(ObserveFilter); + type(GetPureFieldFilter); + type(ApplyPureMethodFilter); } } diff --git a/lib/filter/pure.dart b/lib/filter/pure.dart new file mode 100644 index 000000000..bc50f7b34 --- /dev/null +++ b/lib/filter/pure.dart @@ -0,0 +1,64 @@ +part of angular.filter; + +/** + * This filter returns its argument unchanged but, for `List` and `Map` + * arguments, it causes the argument contents to be observed (as opposed to + * only its identity). + * + * Example: + * + * {{ list | observe }} + */ +@NgFilter(name: 'observe') +class ObserveFilter implements Function { + dynamic call(dynamic _) => _; +} + +/** + * This filter returns the argument's value of the named field. Use this only + * when the field get operation is known to be pure (side-effect free). + * + * Examples: + * + * {{ map | field:'keys' }} + * {{ map | field:'values' }} + * {{ list | field:'reversed' }} + */ +@NgFilter(name: 'field') +class GetPureFieldFilter implements Function { + dynamic call(Object o, String fieldName) => + o == null ? null : + reflect(o).getField(new Symbol(fieldName)).reflectee; +} + +/** + * This filter returns the result of invoking the named method on the filter + * argument. Use this only when the method is known to be pure (side-effect free). + * + * Examples: + * + * {{ expression | method:'toString' }} + * + * + * The first example is useful when _expression_ yields a new identity but its + * string rendering is unchanged. + */ +@NgFilter(name: 'method') +class ApplyPureMethodFilter implements Function { + dynamic call(Object o, String methodName, [args, Map namedArgs]) { + if (o == null) return null; + + if (args is Map) { + namedArgs = args; + args = const []; + } else if (args == null) { + args = const []; + } + final Map _namedArgs = namedArgs == null ? + const {} : {}; + if (namedArgs != null) { + namedArgs.forEach((k,v) => _namedArgs[new Symbol(k)] = v); + } + return reflect(o).invoke(new Symbol(methodName), args, _namedArgs).reflectee; + } +} diff --git a/test/core/scope_spec.dart b/test/core/scope_spec.dart index d7a9d8766..4cc31fccd 100644 --- a/test/core/scope_spec.dart +++ b/test/core/scope_spec.dart @@ -150,7 +150,6 @@ void main() { expect(logger).toEqual([]); }); - it('should prefix', (Logger logger, Map context, RootScope rootScope) { context['a'] = true; rootScope.watch('!a', (value, previous) => logger(value)); @@ -228,7 +227,21 @@ void main() { expect(logger).toEqual(['identity', 'keys', ['foo', 'bar']]); logger.clear(); }); - + + it('should watch list value (vs. identity) changes when "observe" filter is used', + (Logger logger, Map context, RootScope rootScope, AstParser parser, + FilterMap filters) { + final list = [true, 2, 'abc']; + final logVal = (value, _) => logger(value); + context['list'] = list; + rootScope.watch( parser('list | observe', filters: filters), logVal); + rootScope.digest(); + expect(logger).toEqual([list]); + logger.clear(); + context['list'][2] = 'def'; + rootScope.digest(); + expect(logger).toEqual([[true, 2, 'def']]); + }); }); diff --git a/test/filter/pure_spec.dart b/test/filter/pure_spec.dart new file mode 100644 index 000000000..061bfe736 --- /dev/null +++ b/test/filter/pure_spec.dart @@ -0,0 +1,49 @@ +library pure_spec; + +import '../_specs.dart'; + +void main() { + describe('pure filters', () { + beforeEach((Scope scope, Parser parse, FilterMap filters) { + scope.context['string'] = 'abc'; + scope.context['list'] = 'abc'.split(''); + scope.context['map'] = { 'a': 1, 'b': 2, 'c': 3 }; + }); + + // Note that the `observe` filter is tested in [scope_spec.dart]. + + it('should return the value of the named field', + (Scope scope, Parser parse, FilterMap filters) { + expect(parse("list | field:'reversed'").eval(scope.context, filters) + ).toEqual(['c', 'b', 'a']); + expect(parse("map | field:'keys'").eval(scope.context, filters)).toEqual( + ['a', 'b', 'c']); + expect(parse("map | field:'values'").eval(scope.context, filters) + ).toEqual([1, 2, 3]); + }); + + it('should return method call result', + (Scope scope, Parser parse, FilterMap filters) { + expect(parse("list | method:'toString'").eval(scope.context, filters) + ).toEqual('[a, b, c]'); + expect(parse("list | method:'join':['']").eval(scope.context, filters) + ).toEqual('abc'); + expect(parse("string | method:'split':['']").eval(scope.context, filters) + ).toEqual(['a', 'b', 'c']); + }); + + it('should return method call result using namedArgs', + (Scope scope, Parser parse, FilterMap filters) { + scope.context['isB'] = (s) => s == 'b'; + scope.context['zero'] = () => 0; + + // Test for no positional args but with named args. + expect(parse("list | method:'toList':{'growable':false}").eval( + scope.context, filters)).toEqual(['a', 'b', 'c']); + + // Test for both positional and named args. + expect(parse("list | method:'firstWhere':[isB]:{'orElse':zero}").eval( + scope.context, filters)).toEqual('b'); + }); + }); +}