fix($parse): always pass the intercepted value to watchers

Fixes #16021
This commit is contained in:
Jason Bedard
2017-06-16 01:32:34 -07:00
parent de74034ddf
commit 2ee5033967
4 changed files with 117 additions and 40 deletions
-6
View File
@@ -91,12 +91,6 @@ function classDirective(name, selector) {
}
function ngClassWatchAction(newClassString) {
// When using a one-time binding the newClassString will return
// the pre-interceptor value until the one-time is complete
if (!isString(newClassString)) {
newClassString = toClassString(newClassString);
}
if (oldModulo === selector) {
updateClasses(oldClassString, newClassString);
}
+48 -31
View File
@@ -1884,28 +1884,37 @@ function $ParseProvider() {
function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression, prettyPrintExpression) {
var isDone = parsedExpression.literal ? isAllDefined : isDefined;
var unwatch, lastValue;
if (parsedExpression.inputs) {
unwatch = inputsWatchDelegate(scope, oneTimeListener, objectEquality, parsedExpression, prettyPrintExpression);
} else {
unwatch = scope.$watch(oneTimeWatch, oneTimeListener, objectEquality);
}
var exp = parsedExpression.$$intercepted || parsedExpression;
var post = parsedExpression.$$interceptor || identity;
var useInputs = parsedExpression.inputs && !exp.inputs;
// Propogate the literal/inputs/constant attributes
// ... but not oneTime since we are handling it
oneTimeWatch.literal = parsedExpression.literal;
oneTimeWatch.constant = parsedExpression.constant;
oneTimeWatch.inputs = parsedExpression.inputs;
// Allow other delegates to run on this wrapped expression
addWatchDelegate(oneTimeWatch);
unwatch = scope.$watch(oneTimeWatch, listener, objectEquality, prettyPrintExpression);
return unwatch;
function oneTimeWatch(scope) {
return parsedExpression(scope);
function unwatchIfDone() {
if (isDone(lastValue)) {
unwatch();
}
}
function oneTimeListener(value, old, scope) {
lastValue = value;
if (isFunction(listener)) {
listener(value, old, scope);
}
if (isDone(value)) {
scope.$$postDigest(function() {
if (isDone(lastValue)) {
unwatch();
}
});
function oneTimeWatch(scope, locals, assign, inputs) {
lastValue = useInputs && inputs ? inputs[0] : exp(scope, locals, assign, inputs);
if (isDone(lastValue)) {
scope.$$postDigest(unwatchIfDone);
}
return post(lastValue, scope, locals);
}
}
@@ -1937,27 +1946,35 @@ function $ParseProvider() {
return parsedExpression;
}
function chainInterceptors(first, second) {
function chainedInterceptor(value) {
return second(first(value));
}
chainedInterceptor.$stateful = first.$stateful || second.$stateful;
return chainedInterceptor;
}
function addInterceptor(parsedExpression, interceptorFn) {
if (!interceptorFn) return parsedExpression;
// Extract any existing interceptors out of the parsedExpression
// to ensure the original parsedExpression is always the $$intercepted
if (parsedExpression.$$interceptor) {
interceptorFn = chainInterceptors(parsedExpression.$$interceptor, interceptorFn);
parsedExpression = parsedExpression.$$intercepted;
}
var useInputs = false;
var isDone = parsedExpression.literal ? isAllDefined : isDefined;
function regularInterceptedExpression(scope, locals, assign, inputs) {
var fn = function interceptedExpression(scope, locals, assign, inputs) {
var value = useInputs && inputs ? inputs[0] : parsedExpression(scope, locals, assign, inputs);
return interceptorFn(value);
}
};
function oneTimeInterceptedExpression(scope, locals, assign, inputs) {
var value = useInputs && inputs ? inputs[0] : parsedExpression(scope, locals, assign, inputs);
var result = interceptorFn(value);
// we only return the interceptor's result if the
// initial value is defined (for bind-once)
return isDone(value) ? result : value;
}
var fn = parsedExpression.oneTime ? oneTimeInterceptedExpression : regularInterceptedExpression;
// Maintain references to the interceptor/intercepted
fn.$$intercepted = parsedExpression;
fn.$$interceptor = interceptorFn;
// Propogate the literal/oneTime/constant attributes
fn.literal = parsedExpression.literal;
+16
View File
@@ -149,6 +149,22 @@ describe('$interpolate', function() {
expect($rootScope.$countWatchers()).toBe(0);
}));
it('should respect one-time bindings for literals', inject(function($interpolate, $rootScope) {
var calls = [];
$rootScope.$watch($interpolate('{{ ::{x: x} }}'), function(val) {
calls.push(val);
});
$rootScope.$apply();
expect(calls.pop()).toBe('{}');
$rootScope.$apply('x = 1');
expect(calls.pop()).toBe('{"x":1}');
$rootScope.$apply('x = 2');
expect(calls.pop()).toBeUndefined();
}));
it('should stop watching strings with no expressions after first execution',
inject(function($interpolate, $rootScope) {
var spy = jasmine.createSpy();
+53 -3
View File
@@ -3380,13 +3380,12 @@ describe('parser', function() {
scope.$watch($parse('::[a]', interceptor));
// Would be great if interceptor-output was checked for changes and this didn't throw...
interceptorCalls = 0;
expect(function() { scope.$digest(); }).toThrowMinErr('$rootScope', 'infdig');
scope.$digest();
expect(interceptorCalls).not.toBe(0);
interceptorCalls = 0;
expect(function() { scope.$digest(); }).toThrowMinErr('$rootScope', 'infdig');
scope.$digest();
expect(interceptorCalls).not.toBe(0);
}));
@@ -3501,6 +3500,57 @@ describe('parser', function() {
expect(scope.$$watchersCount).toBe(0);
}));
it('should watch the intercepted value of one-time bindings', inject(function($parse, log) {
scope.$watch($parse('::{x:x, y:y}', function(lit) { return lit.x; }), log);
scope.$apply();
expect(log.empty()).toEqual([undefined]);
scope.$apply('x = 1');
expect(log.empty()).toEqual([1]);
scope.$apply('x = 2; y=1');
expect(log.empty()).toEqual([2]);
scope.$apply('x = 1; y=2');
expect(log.empty()).toEqual([]);
}));
it('should watch the intercepted value of one-time bindings in nested interceptors', inject(function($parse, log) {
scope.$watch($parse($parse('::{x:x, y:y}', function(lit) { return lit.x; }), identity), log);
scope.$apply();
expect(log.empty()).toEqual([undefined]);
scope.$apply('x = 1');
expect(log.empty()).toEqual([1]);
scope.$apply('x = 2; y=1');
expect(log.empty()).toEqual([2]);
scope.$apply('x = 1; y=2');
expect(log.empty()).toEqual([]);
}));
it('should nest interceptors around eachother, not around the intercepted', inject(function($parse) {
function origin() { return 0; }
var fn = origin;
function addOne(n) { return n + 1; }
fn = $parse(fn, addOne);
expect(fn.$$intercepted).toBe(origin);
expect(fn()).toBe(1);
fn = $parse(fn, addOne);
expect(fn.$$intercepted).toBe(origin);
expect(fn()).toBe(2);
fn = $parse(fn, addOne);
expect(fn.$$intercepted).toBe(origin);
expect(fn()).toBe(3);
}));
it('should not propogate $$watchDelegate to the interceptor wrapped expression', inject(function($parse) {
function getter(s) {
return s.x;