diff --git a/src/ng/parse.js b/src/ng/parse.js
index c9fa42b63e87..4455129d7ea7 100644
--- a/src/ng/parse.js
+++ b/src/ng/parse.js
@@ -622,15 +622,44 @@ function isStateless($filter, filterName) {
return !fn.$stateful;
}
-function findConstantAndWatchExpressions(ast, $filter) {
+// Detect nodes which could depend on non-shallow state of objects
+function isPure(node, parentIsPure) {
+ switch (node.type) {
+ // Computed members might invoke a stateful toString()
+ case AST.MemberExpression:
+ if (node.computed) {
+ return false;
+ }
+ break;
+
+ // Unary always convert to primative
+ case AST.UnaryExpression:
+ return true;
+
+ // The binary + operator can invoke a stateful toString().
+ case AST.BinaryExpression:
+ return node.operator !== '+';
+
+ // Functions / filters probably read state from within objects
+ case AST.CallExpression:
+ return false;
+ }
+
+ return (undefined === parentIsPure) || parentIsPure;
+}
+
+function findConstantAndWatchExpressions(ast, $filter, parentIsPure) {
var allConstants;
var argsToWatch;
var isStatelessFilter;
+
+ var astIsPure = ast.isPure = isPure(ast, parentIsPure);
+
switch (ast.type) {
case AST.Program:
allConstants = true;
forEach(ast.body, function(expr) {
- findConstantAndWatchExpressions(expr.expression, $filter);
+ findConstantAndWatchExpressions(expr.expression, $filter, astIsPure);
allConstants = allConstants && expr.expression.constant;
});
ast.constant = allConstants;
@@ -640,26 +669,26 @@ function findConstantAndWatchExpressions(ast, $filter) {
ast.toWatch = [];
break;
case AST.UnaryExpression:
- findConstantAndWatchExpressions(ast.argument, $filter);
+ findConstantAndWatchExpressions(ast.argument, $filter, astIsPure);
ast.constant = ast.argument.constant;
ast.toWatch = ast.argument.toWatch;
break;
case AST.BinaryExpression:
- findConstantAndWatchExpressions(ast.left, $filter);
- findConstantAndWatchExpressions(ast.right, $filter);
+ findConstantAndWatchExpressions(ast.left, $filter, astIsPure);
+ findConstantAndWatchExpressions(ast.right, $filter, astIsPure);
ast.constant = ast.left.constant && ast.right.constant;
ast.toWatch = ast.left.toWatch.concat(ast.right.toWatch);
break;
case AST.LogicalExpression:
- findConstantAndWatchExpressions(ast.left, $filter);
- findConstantAndWatchExpressions(ast.right, $filter);
+ findConstantAndWatchExpressions(ast.left, $filter, astIsPure);
+ findConstantAndWatchExpressions(ast.right, $filter, astIsPure);
ast.constant = ast.left.constant && ast.right.constant;
ast.toWatch = ast.constant ? [] : [ast];
break;
case AST.ConditionalExpression:
- findConstantAndWatchExpressions(ast.test, $filter);
- findConstantAndWatchExpressions(ast.alternate, $filter);
- findConstantAndWatchExpressions(ast.consequent, $filter);
+ findConstantAndWatchExpressions(ast.test, $filter, astIsPure);
+ findConstantAndWatchExpressions(ast.alternate, $filter, astIsPure);
+ findConstantAndWatchExpressions(ast.consequent, $filter, astIsPure);
ast.constant = ast.test.constant && ast.alternate.constant && ast.consequent.constant;
ast.toWatch = ast.constant ? [] : [ast];
break;
@@ -668,9 +697,9 @@ function findConstantAndWatchExpressions(ast, $filter) {
ast.toWatch = [ast];
break;
case AST.MemberExpression:
- findConstantAndWatchExpressions(ast.object, $filter);
+ findConstantAndWatchExpressions(ast.object, $filter, astIsPure);
if (ast.computed) {
- findConstantAndWatchExpressions(ast.property, $filter);
+ findConstantAndWatchExpressions(ast.property, $filter, astIsPure);
}
ast.constant = ast.object.constant && (!ast.computed || ast.property.constant);
ast.toWatch = [ast];
@@ -680,7 +709,7 @@ function findConstantAndWatchExpressions(ast, $filter) {
allConstants = isStatelessFilter;
argsToWatch = [];
forEach(ast.arguments, function(expr) {
- findConstantAndWatchExpressions(expr, $filter);
+ findConstantAndWatchExpressions(expr, $filter, astIsPure);
allConstants = allConstants && expr.constant;
if (!expr.constant) {
argsToWatch.push.apply(argsToWatch, expr.toWatch);
@@ -690,8 +719,8 @@ function findConstantAndWatchExpressions(ast, $filter) {
ast.toWatch = isStatelessFilter ? argsToWatch : [ast];
break;
case AST.AssignmentExpression:
- findConstantAndWatchExpressions(ast.left, $filter);
- findConstantAndWatchExpressions(ast.right, $filter);
+ findConstantAndWatchExpressions(ast.left, $filter, astIsPure);
+ findConstantAndWatchExpressions(ast.right, $filter, astIsPure);
ast.constant = ast.left.constant && ast.right.constant;
ast.toWatch = [ast];
break;
@@ -699,7 +728,7 @@ function findConstantAndWatchExpressions(ast, $filter) {
allConstants = true;
argsToWatch = [];
forEach(ast.elements, function(expr) {
- findConstantAndWatchExpressions(expr, $filter);
+ findConstantAndWatchExpressions(expr, $filter, astIsPure);
allConstants = allConstants && expr.constant;
if (!expr.constant) {
argsToWatch.push.apply(argsToWatch, expr.toWatch);
@@ -712,13 +741,13 @@ function findConstantAndWatchExpressions(ast, $filter) {
allConstants = true;
argsToWatch = [];
forEach(ast.properties, function(property) {
- findConstantAndWatchExpressions(property.value, $filter);
+ findConstantAndWatchExpressions(property.value, $filter, astIsPure);
allConstants = allConstants && property.value.constant && !property.computed;
if (!property.value.constant) {
argsToWatch.push.apply(argsToWatch, property.value.toWatch);
}
if (property.computed) {
- findConstantAndWatchExpressions(property.key, $filter);
+ findConstantAndWatchExpressions(property.key, $filter, astIsPure);
if (!property.key.constant) {
argsToWatch.push.apply(argsToWatch, property.key.toWatch);
}
@@ -803,7 +832,7 @@ ASTCompiler.prototype = {
var intoId = self.nextId();
self.recurse(watch, intoId);
self.return_(intoId);
- self.state.inputs.push(fnKey);
+ self.state.inputs.push({name: fnKey, isPure: watch.isPure});
watch.watchId = key;
});
this.state.computing = 'fn';
@@ -839,13 +868,16 @@ ASTCompiler.prototype = {
watchFns: function() {
var result = [];
- var fns = this.state.inputs;
+ var inputs = this.state.inputs;
var self = this;
- forEach(fns, function(name) {
- result.push('var ' + name + '=' + self.generateFunction(name, 's'));
+ forEach(inputs, function(input) {
+ result.push('var ' + input.name + '=' + self.generateFunction(input.name, 's'));
+ if (input.isPure) {
+ result.push(input.name, '.isPure=true;');
+ }
});
- if (fns.length) {
- result.push('fn.inputs=[' + fns.join(',') + '];');
+ if (inputs.length) {
+ result.push('fn.inputs=[' + inputs.map(function(i) { return i.name; }).join(',') + '];');
}
return result.join('');
},
@@ -1251,6 +1283,7 @@ ASTInterpreter.prototype = {
inputs = [];
forEach(toWatch, function(watch, key) {
var input = self.recurse(watch);
+ input.isPure = watch.isPure;
watch.input = input;
inputs.push(input);
watch.watchId = key;
@@ -1817,7 +1850,7 @@ function $ParseProvider() {
inputExpressions = inputExpressions[0];
return scope.$watch(function expressionInputWatch(scope) {
var newInputValue = inputExpressions(scope);
- if (!expressionInputDirtyCheck(newInputValue, oldInputValueOf, parsedExpression.literal)) {
+ if (!expressionInputDirtyCheck(newInputValue, oldInputValueOf, inputExpressions.isPure)) {
lastResult = parsedExpression(scope, undefined, undefined, [newInputValue]);
oldInputValueOf = newInputValue && getValueOf(newInputValue);
}
@@ -1837,7 +1870,7 @@ function $ParseProvider() {
for (var i = 0, ii = inputExpressions.length; i < ii; i++) {
var newInputValue = inputExpressions[i](scope);
- if (changed || (changed = !expressionInputDirtyCheck(newInputValue, oldInputValueOfValues[i], parsedExpression.literal))) {
+ if (changed || (changed = !expressionInputDirtyCheck(newInputValue, oldInputValueOfValues[i], inputExpressions[i].isPure))) {
oldInputValues[i] = newInputValue;
oldInputValueOfValues[i] = newInputValue && getValueOf(newInputValue);
}
diff --git a/test/ng/directive/ngClassSpec.js b/test/ng/directive/ngClassSpec.js
index ae671d050bd3..655f3b9cea1c 100644
--- a/test/ng/directive/ngClassSpec.js
+++ b/test/ng/directive/ngClassSpec.js
@@ -567,6 +567,26 @@ describe('ngClass', function() {
})
);
+ //https://github.com/angular/angular.js/issues/15960#issuecomment-299109412
+ it('should always reevaluate filters with non-primitive inputs within literals', function() {
+ module(function($filterProvider) {
+ $filterProvider.register('foo', valueFn(function(o) {
+ return o.a || o.b;
+ }));
+ });
+
+ inject(function($rootScope, $compile) {
+ $rootScope.testObj = {};
+ element = $compile('
')($rootScope);
+
+ $rootScope.$apply();
+ expect(element).not.toHaveClass('x');
+
+ $rootScope.$apply('testObj.a = true');
+ expect(element).toHaveClass('x');
+ });
+ });
+
describe('large objects', function() {
var getProp;
var veryLargeObj;
diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js
index 13d7a10039b0..3c21649db71e 100644
--- a/test/ng/parseSpec.js
+++ b/test/ng/parseSpec.js
@@ -2817,323 +2817,677 @@ describe('parser', function() {
expect(bCalled).toBe(true);
});
- it('should not invoke filters unless the input/arguments change', function() {
- var filterCalled = false;
- $filterProvider.register('foo', valueFn(function(input) {
- filterCalled = true;
- return input;
+ describe('filters', function() {
+
+ it('should not be invoked unless the input/arguments change', function() {
+ var filterCalled = false;
+ $filterProvider.register('foo', valueFn(function(input) {
+ filterCalled = true;
+ return input;
+ }));
+
+ scope.$watch('a | foo:b:1');
+ scope.a = 0;
+ scope.$digest();
+ expect(filterCalled).toBe(true);
+
+ filterCalled = false;
+ scope.$digest();
+ expect(filterCalled).toBe(false);
+
+ scope.a++;
+ scope.$digest();
+ expect(filterCalled).toBe(true);
+ });
+
+ it('should always be invoked if they are marked as having $stateful', function() {
+ var filterCalled = false;
+ $filterProvider.register('foo', valueFn(extend(function(input) {
+ filterCalled = true;
+ return input;
+ }, {$stateful: true})));
+
+ scope.$watch('a | foo:b:1');
+ scope.a = 0;
+ scope.$digest();
+ expect(filterCalled).toBe(true);
+
+ filterCalled = false;
+ scope.$digest();
+ expect(filterCalled).toBe(true);
+ });
+
+ it('should be treated as constant when input are constant', inject(function($parse) {
+ var filterCalls = 0;
+ $filterProvider.register('foo', valueFn(function(input) {
+ filterCalls++;
+ return input;
+ }));
+
+ var parsed = $parse('{x: 1} | foo:1');
+
+ expect(parsed.constant).toBe(true);
+
+ var watcherCalls = 0;
+ scope.$watch(parsed, function(input) {
+ expect(input).toEqual({x:1});
+ watcherCalls++;
+ });
+
+ scope.$digest();
+ expect(filterCalls).toBe(1);
+ expect(watcherCalls).toBe(1);
+
+ scope.$digest();
+ expect(filterCalls).toBe(1);
+ expect(watcherCalls).toBe(1);
}));
- scope.$watch('a | foo:b:1');
- scope.a = 0;
- scope.$digest();
- expect(filterCalled).toBe(true);
+ describe('with non-primitive input', function() {
- filterCalled = false;
- scope.$digest();
- expect(filterCalled).toBe(false);
+ describe('that does NOT support valueOf()', function() {
- scope.a++;
- scope.$digest();
- expect(filterCalled).toBe(true);
- });
+ it('should always be reevaluated', inject(function($parse) {
+ var filterCalls = 0;
+ $filterProvider.register('foo', valueFn(function(input) {
+ filterCalls++;
+ return input;
+ }));
- it('should invoke filters if they are marked as having $stateful', function() {
- var filterCalled = false;
- $filterProvider.register('foo', valueFn(extend(function(input) {
- filterCalled = true;
- return input;
- }, {$stateful: true})));
+ var parsed = $parse('obj | foo');
+ var obj = scope.obj = {};
- scope.$watch('a | foo:b:1');
- scope.a = 0;
- scope.$digest();
- expect(filterCalled).toBe(true);
+ var watcherCalls = 0;
+ scope.$watch(parsed, function(input) {
+ expect(input).toBe(obj);
+ watcherCalls++;
+ });
- filterCalled = false;
- scope.$digest();
- expect(filterCalled).toBe(true);
- });
+ scope.$digest();
+ expect(filterCalls).toBe(2);
+ expect(watcherCalls).toBe(1);
- it('should not invoke interceptorFns unless the input changes', inject(function($parse) {
- var called = false;
- function interceptor(v) {
- called = true;
- return v;
- }
- scope.$watch($parse('a', interceptor));
- scope.$watch($parse('a + b', interceptor));
- scope.a = scope.b = 0;
- scope.$digest();
- expect(called).toBe(true);
+ scope.$digest();
+ expect(filterCalls).toBe(3);
+ expect(watcherCalls).toBe(1);
+ }));
- called = false;
- scope.$digest();
- expect(called).toBe(false);
+ it('should always be reevaluated in literals', inject(function($parse) {
+ $filterProvider.register('foo', valueFn(function(input) {
+ return input.b > 0;
+ }));
- scope.a++;
- scope.$digest();
- expect(called).toBe(true);
- }));
+ scope.$watch('[(a | foo)]', function() {});
- it('should not invoke interceptorFns unless the input.valueOf changes even if the instance changes', inject(function($parse) {
- var called = false;
- function interceptor(v) {
- called = true;
- return v;
- }
- scope.$watch($parse('a', interceptor));
- scope.a = new Date();
- scope.$digest();
- expect(called).toBe(true);
+ // Would be great if filter-output was checked for changes and this didn't throw...
+ expect(function() { scope.$apply('a = {b: 1}'); }).toThrowMinErr('$rootScope', 'infdig');
+ }));
- called = false;
- scope.a = new Date(scope.a.valueOf());
- scope.$digest();
- expect(called).toBe(false);
- }));
+ it('should always be reevaluated when passed literals', inject(function($parse) {
+ scope.$watch('[a] | filter', function() {});
- it('should invoke interceptorFns if input.valueOf changes even if the instance does not', inject(function($parse) {
- var called = false;
- function interceptor(v) {
- called = true;
- return v;
- }
- scope.$watch($parse('a', interceptor));
- scope.a = new Date();
- scope.$digest();
- expect(called).toBe(true);
+ scope.$apply('a = 1');
- called = false;
- scope.a.setTime(scope.a.getTime() + 1);
- scope.$digest();
- expect(called).toBe(true);
- }));
+ // Would be great if filter-output was checked for changes and this didn't throw...
+ expect(function() { scope.$apply('a = {}'); }).toThrowMinErr('$rootScope', 'infdig');
+ }));
+ });
- it('should invoke interceptors when the expression is `undefined`', inject(function($parse) {
- var called = false;
- function interceptor(v) {
- called = true;
- return v;
- }
- scope.$watch($parse(undefined, interceptor));
- scope.$digest();
- expect(called).toBe(true);
- }));
+ describe('that does support valueOf()', function() {
+
+ it('should not be reevaluated',
+ inject(function($parse) {
+ var filterCalls = 0;
+ $filterProvider.register('foo', valueFn(function(input) {
+ filterCalls++;
+ expect(input instanceof Date).toBe(true);
+ return input;
+ }));
+
+ var parsed = $parse('date | foo:a');
+ var date = scope.date = new Date();
+
+ var watcherCalls = 0;
+ scope.$watch(parsed, function(input) {
+ expect(input).toBe(date);
+ watcherCalls++;
+ });
+
+ scope.$digest();
+ expect(filterCalls).toBe(1);
+ expect(watcherCalls).toBe(1);
+
+ scope.$digest();
+ expect(filterCalls).toBe(1);
+ expect(watcherCalls).toBe(1);
+ }));
- it('should treat filters with constant input as constants', inject(function($parse) {
- var filterCalls = 0;
- $filterProvider.register('foo', valueFn(function(input) {
- filterCalls++;
- return input;
- }));
+ it('should not be reevaluated in literals', inject(function($parse) {
+ var filterCalls = 0;
+ $filterProvider.register('foo', valueFn(function(input) {
+ filterCalls++;
+ return input;
+ }));
+
+ scope.date = new Date(1234567890123);
+
+ var watcherCalls = 0;
+ scope.$watch('[(date | foo)]', function(input) {
+ watcherCalls++;
+ });
+
+ scope.$digest();
+ expect(filterCalls).toBe(1);
+ expect(watcherCalls).toBe(1);
+
+ scope.$digest();
+ expect(filterCalls).toBe(1);
+ expect(watcherCalls).toBe(1);
+ }));
+
+ it('should be reevaluated when valueOf() changes', inject(function($parse) {
+ var filterCalls = 0;
+ $filterProvider.register('foo', valueFn(function(input) {
+ filterCalls++;
+ expect(input instanceof Date).toBe(true);
+ return input;
+ }));
+
+ var parsed = $parse('date | foo:a');
+ var date = scope.date = new Date();
+
+ var watcherCalls = 0;
+ scope.$watch(parsed, function(input) {
+ expect(input).toBe(date);
+ watcherCalls++;
+ });
+
+ scope.$digest();
+ expect(filterCalls).toBe(1);
+ expect(watcherCalls).toBe(1);
- var parsed = $parse('{x: 1} | foo:1');
+ date.setYear(1901);
- expect(parsed.constant).toBe(true);
+ scope.$digest();
+ expect(filterCalls).toBe(2);
+ expect(watcherCalls).toBe(1);
+ }));
+
+ it('should be reevaluated in literals when valueOf() changes', inject(function($parse) {
+ var filterCalls = 0;
+ $filterProvider.register('foo', valueFn(function(input) {
+ filterCalls++;
+ return input;
+ }));
+
+ scope.date = new Date(1234567890123);
+
+ var watcherCalls = 0;
+ scope.$watch('[(date | foo)]', function(input) {
+ watcherCalls++;
+ });
+
+ scope.$digest();
+ expect(filterCalls).toBe(1);
+ expect(watcherCalls).toBe(1);
+
+ scope.date.setTime(1234567890);
+
+ scope.$digest();
+ expect(filterCalls).toBe(2);
+ expect(watcherCalls).toBe(2);
+ }));
+
+ it('should not be reevaluated when the instance changes but valueOf() does not', inject(function($parse) {
+ var filterCalls = 0;
+ $filterProvider.register('foo', valueFn(function(input) {
+ filterCalls++;
+ return input;
+ }));
+
+ scope.date = new Date(1234567890123);
+
+ var watcherCalls = 0;
+ scope.$watch($parse('[(date | foo)]'), function(input) {
+ watcherCalls++;
+ });
+
+ scope.$digest();
+ expect(watcherCalls).toBe(1);
+ expect(filterCalls).toBe(1);
+
+ scope.date = new Date(1234567890123);
+ scope.$digest();
+ expect(watcherCalls).toBe(1);
+ expect(filterCalls).toBe(1);
+ }));
+ });
+
+ it('should not be reevaluated when input is simplified via unary operators', inject(function($parse) {
+ var filterCalls = 0;
+ $filterProvider.register('foo', valueFn(function(input) {
+ filterCalls++;
+ return input;
+ }));
+
+ scope.obj = {};
+
+ var watcherCalls = 0;
+ scope.$watch('!obj | foo:!obj', function(input) {
+ watcherCalls++;
+ });
+
+ scope.$digest();
+ expect(filterCalls).toBe(1);
+ expect(watcherCalls).toBe(1);
+
+ scope.$digest();
+ expect(filterCalls).toBe(1);
+ expect(watcherCalls).toBe(1);
+ }));
+
+ it('should not be reevaluated when input is simplified via non-plus/concat binary operators', inject(function($parse) {
+ var filterCalls = 0;
+ $filterProvider.register('foo', valueFn(function(input) {
+ filterCalls++;
+ return input;
+ }));
+
+ scope.obj = {};
- var watcherCalls = 0;
- scope.$watch(parsed, function(input) {
- expect(input).toEqual({x:1});
- watcherCalls++;
+ var watcherCalls = 0;
+ scope.$watch('1 - obj | foo:(1 * obj)', function(input) {
+ watcherCalls++;
+ });
+
+ scope.$digest();
+ expect(filterCalls).toBe(1);
+ expect(watcherCalls).toBe(1);
+
+ scope.$digest();
+ expect(filterCalls).toBe(1);
+ expect(watcherCalls).toBe(1);
+ }));
+
+ it('should be reevaluated when input is simplified via plus/concat', inject(function($parse) {
+ var filterCalls = 0;
+ $filterProvider.register('foo', valueFn(function(input) {
+ filterCalls++;
+ return input;
+ }));
+
+ scope.obj = {};
+
+ var watcherCalls = 0;
+ scope.$watch('1 + obj | foo', function(input) {
+ watcherCalls++;
+ });
+
+ scope.$digest();
+ expect(filterCalls).toBe(2);
+ expect(watcherCalls).toBe(1);
+
+ scope.$digest();
+ expect(filterCalls).toBe(3);
+ expect(watcherCalls).toBe(1);
+ }));
+
+ it('should reevaluate computed member expressions', inject(function($parse) {
+ var toStringCalls = 0;
+
+ scope.obj = {};
+ scope.key = {
+ toString: function() {
+ toStringCalls++;
+ return 'foo';
+ }
+ };
+
+ var watcherCalls = 0;
+ scope.$watch('obj[key]', function(input) {
+ watcherCalls++;
+ });
+
+ scope.$digest();
+ expect(toStringCalls).toBe(2);
+ expect(watcherCalls).toBe(1);
+
+ scope.$digest();
+ expect(toStringCalls).toBe(3);
+ expect(watcherCalls).toBe(1);
+ }));
+
+ it('should be reevaluated with input created with null prototype', inject(function($parse) {
+ var filterCalls = 0;
+ $filterProvider.register('foo', valueFn(function(input) {
+ filterCalls++;
+ return input;
+ }));
+
+ var parsed = $parse('obj | foo');
+ var obj = scope.obj = Object.create(null);
+
+ var watcherCalls = 0;
+ scope.$watch(parsed, function(input) {
+ expect(input).toBe(obj);
+ watcherCalls++;
+ });
+
+ scope.$digest();
+ expect(filterCalls).toBe(2);
+ expect(watcherCalls).toBe(1);
+
+ scope.$digest();
+ expect(filterCalls).toBe(3);
+ expect(watcherCalls).toBe(1);
+ }));
});
- scope.$digest();
- expect(filterCalls).toBe(1);
- expect(watcherCalls).toBe(1);
+ describe('with primitive input', function() {
- scope.$digest();
- expect(filterCalls).toBe(1);
- expect(watcherCalls).toBe(1);
- }));
+ it('should not be reevaluated when passed literals', inject(function($parse) {
+ var filterCalls = 0;
+ $filterProvider.register('foo', valueFn(function(input) {
+ filterCalls++;
+ return input;
+ }));
- it('should always reevaluate filters with non-primitive input that doesn\'t support valueOf()',
- inject(function($parse) {
- var filterCalls = 0;
- $filterProvider.register('foo', valueFn(function(input) {
- filterCalls++;
- return input;
- }));
+ var watcherCalls = 0;
+ scope.$watch('[a] | foo', function(input) {
+ watcherCalls++;
+ });
+
+ scope.$apply('a = 1');
+ expect(filterCalls).toBe(1);
+ expect(watcherCalls).toBe(1);
+
+ scope.$apply('a = 2');
+ expect(filterCalls).toBe(2);
+ expect(watcherCalls).toBe(2);
+ }));
+
+ it('should not be reevaluated in literals', inject(function($parse) {
+ var filterCalls = 0;
+ $filterProvider.register('foo', valueFn(function(input) {
+ filterCalls++;
+ return input;
+ }));
+
+ scope.prim = 1234567890123;
- var parsed = $parse('obj | foo');
- var obj = scope.obj = {};
+ var watcherCalls = 0;
+ scope.$watch('[(prim | foo)]', function(input) {
+ watcherCalls++;
+ });
- var watcherCalls = 0;
- scope.$watch(parsed, function(input) {
- expect(input).toBe(obj);
- watcherCalls++;
+ scope.$digest();
+ expect(filterCalls).toBe(1);
+ expect(watcherCalls).toBe(1);
+
+ scope.$digest();
+ expect(filterCalls).toBe(1);
+ expect(watcherCalls).toBe(1);
+ }));
});
+ });
- scope.$digest();
- expect(filterCalls).toBe(2);
- expect(watcherCalls).toBe(1);
+ describe('interceptorFns', function() {
- scope.$digest();
- expect(filterCalls).toBe(3);
- expect(watcherCalls).toBe(1);
- }));
+ it('should always be invoked if they are flagged as having $stateful',
+ inject(function($parse) {
+ var called = false;
+ function interceptor() {
+ called = true;
+ }
+ interceptor.$stateful = true;
- it('should always reevaluate filters with non-primitive input created with null prototype',
- inject(function($parse) {
- var filterCalls = 0;
- $filterProvider.register('foo', valueFn(function(input) {
- filterCalls++;
- return input;
+ scope.$watch($parse('a', interceptor));
+ scope.a = 0;
+ scope.$digest();
+ expect(called).toBe(true);
+
+ called = false;
+ scope.$digest();
+ expect(called).toBe(true);
+
+ scope.a++;
+ called = false;
+ scope.$digest();
+ expect(called).toBe(true);
}));
- var parsed = $parse('obj | foo');
- var obj = scope.obj = Object.create(null);
+ it('should not be invoked unless the input changes', inject(function($parse) {
+ var called = false;
+ function interceptor(v) {
+ called = true;
+ return v;
+ }
+ scope.$watch($parse('a', interceptor));
+ scope.$watch($parse('a + b', interceptor));
+ scope.a = scope.b = 0;
+ scope.$digest();
+ expect(called).toBe(true);
+
+ called = false;
+ scope.$digest();
+ expect(called).toBe(false);
+
+ scope.a++;
+ scope.$digest();
+ expect(called).toBe(true);
+ }));
- var watcherCalls = 0;
- scope.$watch(parsed, function(input) {
- expect(input).toBe(obj);
- watcherCalls++;
- });
+ it('should not be invoked unless the input.valueOf() changes even if the instance changes', inject(function($parse) {
+ var called = false;
+ function interceptor(v) {
+ called = true;
+ return v;
+ }
+ scope.$watch($parse('a', interceptor));
+ scope.a = new Date();
+ scope.$digest();
+ expect(called).toBe(true);
+
+ called = false;
+ scope.a = new Date(scope.a.valueOf());
+ scope.$digest();
+ expect(called).toBe(false);
+ }));
- scope.$digest();
- expect(filterCalls).toBe(2);
- expect(watcherCalls).toBe(1);
+ it('should be invoked if input.valueOf() changes even if the instance does not', inject(function($parse) {
+ var called = false;
+ function interceptor(v) {
+ called = true;
+ return v;
+ }
+ scope.$watch($parse('a', interceptor));
+ scope.a = new Date();
+ scope.$digest();
+ expect(called).toBe(true);
+
+ called = false;
+ scope.a.setTime(scope.a.getTime() + 1);
+ scope.$digest();
+ expect(called).toBe(true);
+ }));
- scope.$digest();
- expect(filterCalls).toBe(3);
- expect(watcherCalls).toBe(1);
- }));
+ it('should be invoked when the expression is `undefined`', inject(function($parse) {
+ var called = false;
+ function interceptor(v) {
+ called = true;
+ return v;
+ }
+ scope.$watch($parse(undefined, interceptor));
+ scope.$digest();
+ expect(called).toBe(true);
+ }));
+ });
- it('should not reevaluate filters with non-primitive input that does support valueOf()',
- inject(function($parse) {
- var filterCalls = 0;
- $filterProvider.register('foo', valueFn(function(input) {
- filterCalls++;
- expect(input instanceof Date).toBe(true);
- return input;
+ describe('literals', function() {
+
+ it('should support watching', inject(function($parse) {
+ var lastVal = NaN;
+ var callCount = 0;
+ var listener = function(val) { callCount++; lastVal = val; };
+
+ scope.$watch('{val: val}', listener);
+
+ scope.$apply('val = 1');
+ expect(callCount).toBe(1);
+ expect(lastVal).toEqual({val: 1});
+
+ scope.$apply('val = []');
+ expect(callCount).toBe(2);
+ expect(lastVal).toEqual({val: []});
+
+ scope.$apply('val = []');
+ expect(callCount).toBe(3);
+ expect(lastVal).toEqual({val: []});
+
+ scope.$apply('val = {}');
+ expect(callCount).toBe(4);
+ expect(lastVal).toEqual({val: {}});
}));
- var parsed = $parse('date | foo:a');
- var date = scope.date = new Date();
+ it('should only watch the direct inputs', inject(function($parse) {
+ var lastVal = NaN;
+ var callCount = 0;
+ var listener = function(val) { callCount++; lastVal = val; };
- var watcherCalls = 0;
- scope.$watch(parsed, function(input) {
- expect(input).toBe(date);
- watcherCalls++;
- });
+ scope.$watch('{val: val}', listener);
- scope.$digest();
- expect(filterCalls).toBe(1);
- expect(watcherCalls).toBe(1);
+ scope.$apply('val = 1');
+ expect(callCount).toBe(1);
+ expect(lastVal).toEqual({val: 1});
- scope.$digest();
- expect(filterCalls).toBe(1);
- expect(watcherCalls).toBe(1);
- }));
+ scope.$apply('val = [2]');
+ expect(callCount).toBe(2);
+ expect(lastVal).toEqual({val: [2]});
- it('should reevaluate filters with non-primitive input that does support valueOf() when' +
- 'valueOf() value changes', inject(function($parse) {
- var filterCalls = 0;
- $filterProvider.register('foo', valueFn(function(input) {
- filterCalls++;
- expect(input instanceof Date).toBe(true);
- return input;
+ scope.$apply('val.push(3)');
+ expect(callCount).toBe(2);
+
+ scope.$apply('val.length = 0');
+ expect(callCount).toBe(2);
}));
- var parsed = $parse('date | foo:a');
- var date = scope.date = new Date();
+ it('should only watch the direct inputs when nested', inject(function($parse) {
+ var lastVal = NaN;
+ var callCount = 0;
+ var listener = function(val) { callCount++; lastVal = val; };
- var watcherCalls = 0;
- scope.$watch(parsed, function(input) {
- expect(input).toBe(date);
- watcherCalls++;
- });
+ scope.$watch('[{val: [val]}]', listener);
- scope.$digest();
- expect(filterCalls).toBe(1);
- expect(watcherCalls).toBe(1);
+ scope.$apply('val = 1');
+ expect(callCount).toBe(1);
+ expect(lastVal).toEqual([{val: [1]}]);
- date.setYear(1901);
+ scope.$apply('val = [2]');
+ expect(callCount).toBe(2);
+ expect(lastVal).toEqual([{val: [[2]]}]);
- scope.$digest();
- expect(filterCalls).toBe(2);
- expect(watcherCalls).toBe(1);
- }));
+ scope.$apply('val.push(3)');
+ expect(callCount).toBe(2);
- it('should invoke interceptorFns if they are flagged as having $stateful',
- inject(function($parse) {
- var called = false;
- function interceptor() {
- called = true;
- }
- interceptor.$stateful = true;
+ scope.$apply('val.length = 0');
+ expect(callCount).toBe(2);
+ }));
- scope.$watch($parse('a', interceptor));
- scope.a = 0;
- scope.$digest();
- expect(called).toBe(true);
+ describe('with non-primative input', function() {
- called = false;
- scope.$digest();
- expect(called).toBe(true);
+ describe('that does NOT support valueOf()', function() {
+ it('should not be reevaluated', inject(function($parse) {
+ var obj = scope.obj = {};
- scope.a++;
- called = false;
- scope.$digest();
- expect(called).toBe(true);
- }));
+ var parsed = $parse('[obj]');
+ var watcherCalls = 0;
+ scope.$watch(parsed, function(input) {
+ expect(input[0]).toBe(obj);
+ watcherCalls++;
+ });
- it('should not reevaluate literals with non-primitive input that does support valueOf()',
- inject(function($parse) {
+ scope.$digest();
+ expect(watcherCalls).toBe(1);
+
+ scope.$digest();
+ expect(watcherCalls).toBe(1);
+ }));
+ });
- var date = scope.date = new Date();
+ describe('that does support valueOf()', function() {
+ it('should not be reevaluated', inject(function($parse) {
+ var date = scope.date = new Date();
- var parsed = $parse('[date]');
- var watcherCalls = 0;
- scope.$watch(parsed, function(input) {
- expect(input[0]).toBe(date);
- watcherCalls++;
- });
+ var parsed = $parse('[date]');
+ var watcherCalls = 0;
+ scope.$watch(parsed, function(input) {
+ expect(input[0]).toBe(date);
+ watcherCalls++;
+ });
- scope.$digest();
- expect(watcherCalls).toBe(1);
+ scope.$digest();
+ expect(watcherCalls).toBe(1);
- scope.$digest();
- expect(watcherCalls).toBe(1);
- }));
+ scope.$digest();
+ expect(watcherCalls).toBe(1);
+ }));
- it('should not reevaluate literals with non-primitive input that does support valueOf()' +
- ' when the instance changes but valueOf() does not', inject(function($parse) {
+ it('should be reevaluated even when valueOf() changes', inject(function($parse) {
+ var date = scope.date = new Date();
- scope.date = new Date(1234567890123);
+ var parsed = $parse('[date]');
+ var watcherCalls = 0;
+ scope.$watch(parsed, function(input) {
+ expect(input[0]).toBe(date);
+ watcherCalls++;
+ });
- var parsed = $parse('[date]');
- var watcherCalls = 0;
- scope.$watch(parsed, function(input) {
- watcherCalls++;
- });
+ scope.$digest();
+ expect(watcherCalls).toBe(1);
- scope.$digest();
- expect(watcherCalls).toBe(1);
+ date.setYear(1901);
- scope.date = new Date(1234567890123);
- scope.$digest();
- expect(watcherCalls).toBe(1);
- }));
+ scope.$digest();
+ expect(watcherCalls).toBe(2);
+ }));
- it('should reevaluate literals with non-primitive input that does support valueOf()' +
- ' when the instance does not change but valueOf() does', inject(function($parse) {
+ it('should not be reevaluated when the instance changes but valueOf() does not', inject(function($parse) {
+ scope.date = new Date(1234567890123);
- scope.date = new Date(1234567890123);
+ var parsed = $parse('[date]');
+ var watcherCalls = 0;
+ scope.$watch(parsed, function(input) {
+ watcherCalls++;
+ });
- var parsed = $parse('[date]');
- var watcherCalls = 0;
- scope.$watch(parsed, function(input) {
- watcherCalls++;
- });
+ scope.$digest();
+ expect(watcherCalls).toBe(1);
- scope.$digest();
- expect(watcherCalls).toBe(1);
+ scope.date = new Date(1234567890123);
+ scope.$digest();
+ expect(watcherCalls).toBe(1);
+ }));
- scope.date.setTime(scope.date.getTime() + 1);
- scope.$digest();
- expect(watcherCalls).toBe(2);
- }));
+ it('should be reevaluated when the instance does not change but valueOf() does', inject(function($parse) {
+
+ scope.date = new Date(1234567890123);
+
+ var parsed = $parse('[date]');
+ var watcherCalls = 0;
+ scope.$watch(parsed, function(input) {
+ watcherCalls++;
+ });
+
+ scope.$digest();
+ expect(watcherCalls).toBe(1);
+
+ scope.date.setTime(scope.date.getTime() + 1);
+ scope.$digest();
+ expect(watcherCalls).toBe(2);
+ }));
+ });
+ });
+ });
it('should continue with the evaluation of the expression without invoking computed parts',
inject(function($parse) {
@@ -3229,74 +3583,6 @@ describe('parser', function() {
expect(count).toBe(4);
expect(values[3]).toEqual({'undefined': true});
});
-
- it('should support watching literals', inject(function($parse) {
- var lastVal = NaN;
- var callCount = 0;
- var listener = function(val) { callCount++; lastVal = val; };
-
- scope.$watch('{val: val}', listener);
-
- scope.$apply('val = 1');
- expect(callCount).toBe(1);
- expect(lastVal).toEqual({val: 1});
-
- scope.$apply('val = []');
- expect(callCount).toBe(2);
- expect(lastVal).toEqual({val: []});
-
- scope.$apply('val = []');
- expect(callCount).toBe(3);
- expect(lastVal).toEqual({val: []});
-
- scope.$apply('val = {}');
- expect(callCount).toBe(4);
- expect(lastVal).toEqual({val: {}});
- }));
-
- it('should only watch the direct inputs to literals', inject(function($parse) {
- var lastVal = NaN;
- var callCount = 0;
- var listener = function(val) { callCount++; lastVal = val; };
-
- scope.$watch('{val: val}', listener);
-
- scope.$apply('val = 1');
- expect(callCount).toBe(1);
- expect(lastVal).toEqual({val: 1});
-
- scope.$apply('val = [2]');
- expect(callCount).toBe(2);
- expect(lastVal).toEqual({val: [2]});
-
- scope.$apply('val.push(3)');
- expect(callCount).toBe(2);
-
- scope.$apply('val.length = 0');
- expect(callCount).toBe(2);
- }));
-
- it('should only watch the direct inputs to nested literals', inject(function($parse) {
- var lastVal = NaN;
- var callCount = 0;
- var listener = function(val) { callCount++; lastVal = val; };
-
- scope.$watch('[{val: [val]}]', listener);
-
- scope.$apply('val = 1');
- expect(callCount).toBe(1);
- expect(lastVal).toEqual([{val: [1]}]);
-
- scope.$apply('val = [2]');
- expect(callCount).toBe(2);
- expect(lastVal).toEqual([{val: [[2]]}]);
-
- scope.$apply('val.push(3)');
- expect(callCount).toBe(2);
-
- scope.$apply('val.length = 0');
- expect(callCount).toBe(2);
- }));
});
describe('locals', function() {