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');
+ });
+ });
+}