From 869644edc72bd7a22281d608cc5f6ca0b2027d92 Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Fri, 26 May 2017 00:28:34 -0700 Subject: [PATCH] fix($parse): do not shallow-watch inputs when wrapped in an interceptor fn Fixes #15905 Closes #16018 --- src/ng/parse.js | 23 ++++++++++++++++------- test/ng/directive/ngClassSpec.js | 16 ++++++++++++++++ test/ng/parseSpec.js | 19 +++++++++++++++++++ 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index 4455129d7ea7..38bb20e7a2a2 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -622,6 +622,9 @@ function isStateless($filter, filterName) { return !fn.$stateful; } +var PURITY_ABSOLUTE = 1; +var PURITY_RELATIVE = 2; + // Detect nodes which could depend on non-shallow state of objects function isPure(node, parentIsPure) { switch (node.type) { @@ -634,18 +637,18 @@ function isPure(node, parentIsPure) { // Unary always convert to primative case AST.UnaryExpression: - return true; + return PURITY_ABSOLUTE; // The binary + operator can invoke a stateful toString(). case AST.BinaryExpression: - return node.operator !== '+'; + return node.operator !== '+' ? PURITY_ABSOLUTE : false; // Functions / filters probably read state from within objects case AST.CallExpression: return false; } - return (undefined === parentIsPure) || parentIsPure; + return (undefined === parentIsPure) ? PURITY_RELATIVE : parentIsPure; } function findConstantAndWatchExpressions(ast, $filter, parentIsPure) { @@ -873,7 +876,7 @@ ASTCompiler.prototype = { forEach(inputs, function(input) { result.push('var ' + input.name + '=' + self.generateFunction(input.name, 's')); if (input.isPure) { - result.push(input.name, '.isPure=true;'); + result.push(input.name, '.isPure=' + JSON.stringify(input.isPure) + ';'); } }); if (inputs.length) { @@ -1960,10 +1963,16 @@ function $ParseProvider() { fn.$$watchDelegate = watchDelegate; fn.inputs = parsedExpression.inputs; } else if (!interceptorFn.$stateful) { - // If there is an interceptor, but no watchDelegate then treat the interceptor like - // we treat filters - it is assumed to be a pure function unless flagged with $stateful + // Treat interceptor like filters - assume non-stateful by default and use the inputsWatchDelegate fn.$$watchDelegate = inputsWatchDelegate; - fn.inputs = parsedExpression.inputs ? parsedExpression.inputs : [parsedExpression]; + fn.inputs = (parsedExpression.inputs ? parsedExpression.inputs : [parsedExpression]).map(function(e) { + // Remove the isPure flag of inputs when it is not absolute because they are now wrapped in a + // potentially non-pure interceptor function. + if (e.isPure === PURITY_RELATIVE) { + return function depurifier(s) { return e(s); }; + } + return e; + }); } return fn; diff --git a/test/ng/directive/ngClassSpec.js b/test/ng/directive/ngClassSpec.js index 655f3b9cea1c..1c08a1c4133c 100644 --- a/test/ng/directive/ngClassSpec.js +++ b/test/ng/directive/ngClassSpec.js @@ -517,6 +517,22 @@ describe('ngClass', function() { }) ); + // https://github.com/angular/angular.js/issues/15905 + it('should support a mixed literal-array/object variable', inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + + $rootScope.classVar = {orange: true}; + $rootScope.$digest(); + expect(element).toHaveClass('orange'); + + $rootScope.classVar.orange = false; + $rootScope.$digest(); + + expect(element).not.toHaveClass('orange'); + }) + ); + + it('should do value stabilization as expected when one-time binding', inject(function($rootScope, $compile) { element = $compile('
')($rootScope); diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index 3c21649db71e..0af6b7ebcf6f 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -3277,6 +3277,25 @@ describe('parser', function() { expect(called).toBe(true); })); + it('should always be invoked if inputs are non-primitive', inject(function($parse) { + var called = false; + function interceptor(v) { + called = true; + return v.sub; + } + + scope.$watch($parse('[o]', interceptor)); + scope.o = {sub: 1}; + + called = false; + scope.$digest(); + expect(called).toBe(true); + + called = false; + scope.$digest(); + expect(called).toBe(true); + })); + it('should not be invoked unless the input.valueOf() changes even if the instance changes', inject(function($parse) { var called = false; function interceptor(v) {