Skip to content

Commit 6699f60

Browse files
committed
fix($parse): do not shallow-watch inputs when wrapped in an interceptor fn
Fixes angular#15905
1 parent b4651e5 commit 6699f60

File tree

3 files changed

+51
-6
lines changed

3 files changed

+51
-6
lines changed

src/ng/parse.js

+15-5
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,9 @@ function isStateless($filter, filterName) {
622622
return !fn.$stateful;
623623
}
624624

625+
var PURITY_ABSOLUTE = 1;
626+
var PURITY_RELATIVE = 2;
627+
625628
// Detect nodes which could depend on non-shallow state of objects
626629
function isPure(node, parentIsPure) {
627630
switch (node.type) {
@@ -634,18 +637,18 @@ function isPure(node, parentIsPure) {
634637

635638
// Unary always convert to primative
636639
case AST.UnaryExpression:
637-
return true;
640+
return PURITY_ABSOLUTE;
638641

639642
// The binary + operator can invoke a stateful toString().
640643
case AST.BinaryExpression:
641-
return node.operator !== '+';
644+
return node.operator !== '+' ? PURITY_ABSOLUTE : false;
642645

643646
// Functions / filters probably read state from within objects
644647
case AST.CallExpression:
645648
return false;
646649
}
647650

648-
return (undefined === parentIsPure) || parentIsPure;
651+
return (undefined === parentIsPure) ? PURITY_RELATIVE : parentIsPure;
649652
}
650653

651654
function findConstantAndWatchExpressions(ast, $filter, parentIsPure) {
@@ -873,7 +876,7 @@ ASTCompiler.prototype = {
873876
forEach(inputs, function(input) {
874877
result.push('var ' + input.name + '=' + self.generateFunction(input.name, 's'));
875878
if (input.isPure) {
876-
result.push(input.name, '.isPure=true;');
879+
result.push(input.name, '.isPure=' + JSON.stringify(input.isPure) + ';');
877880
}
878881
});
879882
if (inputs.length) {
@@ -1963,7 +1966,14 @@ function $ParseProvider() {
19631966
// If there is an interceptor, but no watchDelegate then treat the interceptor like
19641967
// we treat filters - it is assumed to be a pure function unless flagged with $stateful
19651968
fn.$$watchDelegate = inputsWatchDelegate;
1966-
fn.inputs = parsedExpression.inputs ? parsedExpression.inputs : [parsedExpression];
1969+
fn.inputs = (parsedExpression.inputs ? parsedExpression.inputs : [parsedExpression]).map(function(e) {
1970+
// Remove the isPure flag of inputs when it is not absolute because they are now wrapped in a
1971+
// potentially non-pure interceptor function.
1972+
if (e.isPure === PURITY_RELATIVE) {
1973+
return function depurifier(s) { return e(s); };
1974+
}
1975+
return e;
1976+
});
19671977
}
19681978

19691979
return fn;

test/ng/directive/ngClassSpec.js

+17-1
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,22 @@ describe('ngClass', function() {
517517
})
518518
);
519519

520+
// https://github.com/angular/angular.js/issues/15905
521+
it('should support a mixed literal-array/object variable', inject(function($rootScope, $compile) {
522+
element = $compile('<div ng-class="[classVar]"></div>')($rootScope);
523+
524+
$rootScope.classVar = {orange: true};
525+
$rootScope.$digest();
526+
expect(element).toHaveClass('orange');
527+
528+
$rootScope.classVar.orange = false;
529+
$rootScope.$digest();
530+
531+
expect(element).not.toHaveClass('orange');
532+
})
533+
);
534+
535+
520536
it('should do value stabilization as expected when one-time binding',
521537
inject(function($rootScope, $compile) {
522538
element = $compile('<div ng-class="::className"></div>')($rootScope);
@@ -629,7 +645,7 @@ describe('ngClass', function() {
629645
}));
630646

631647
it('should not be copied when using one-time binding', inject(function($compile, $rootScope) {
632-
element = $compile('<div ng-class="::{foo: veryLargeObj, bar: bar}"></div>')($rootScope);
648+
element = $compile('<div ng-class="::{foo: !!veryLargeObj, bar: bar}"></div>')($rootScope);
633649
$rootScope.veryLargeObj = veryLargeObj;
634650
$rootScope.$digest();
635651

test/ng/parseSpec.js

+19
Original file line numberDiff line numberDiff line change
@@ -3277,6 +3277,25 @@ describe('parser', function() {
32773277
expect(called).toBe(true);
32783278
}));
32793279

3280+
it('should always be invoked if inputs are non-primitive', inject(function($parse) {
3281+
var called = false;
3282+
function interceptor(v) {
3283+
called = true;
3284+
return v.sub;
3285+
}
3286+
3287+
scope.$watch($parse('[o]', interceptor));
3288+
scope.o = {sub: 1};
3289+
3290+
called = false;
3291+
scope.$digest();
3292+
expect(called).toBe(true);
3293+
3294+
called = false;
3295+
scope.$digest();
3296+
expect(called).toBe(true);
3297+
}));
3298+
32803299
it('should not be invoked unless the input.valueOf() changes even if the instance changes', inject(function($parse) {
32813300
var called = false;
32823301
function interceptor(v) {

0 commit comments

Comments
 (0)