Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 48ede5f

Browse files
committed
fix($parse): always pass the intercepted value to watchers
Fixes #16021
1 parent 798d277 commit 48ede5f

File tree

4 files changed

+106
-49
lines changed

4 files changed

+106
-49
lines changed

src/ng/directive/ngClass.js

-6
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,6 @@ function classDirective(name, selector) {
9191
}
9292

9393
function ngClassWatchAction(newClassString) {
94-
// When using a one-time binding the newClassString will return
95-
// the pre-interceptor value until the one-time is complete
96-
if (!isString(newClassString)) {
97-
newClassString = toClassString(newClassString);
98-
}
99-
10094
if (oldModulo === selector) {
10195
updateClasses(oldClassString, newClassString);
10296
}

src/ng/parse.js

+53-40
Original file line numberDiff line numberDiff line change
@@ -1884,28 +1884,35 @@ function $ParseProvider() {
18841884
function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression, prettyPrintExpression) {
18851885
var isDone = parsedExpression.literal ? isAllDefined : isDefined;
18861886
var unwatch, lastValue;
1887-
if (parsedExpression.inputs) {
1888-
unwatch = inputsWatchDelegate(scope, oneTimeListener, objectEquality, parsedExpression, prettyPrintExpression);
1889-
} else {
1890-
unwatch = scope.$watch(oneTimeWatch, oneTimeListener, objectEquality);
1891-
}
1887+
1888+
var exp = parsedExpression.$$intercepted || parsedExpression;
1889+
var post = parsedExpression.$$interceptor || identity;
1890+
1891+
// Propogate the literal/inputs/constant attributes
1892+
// ... but not oneTime since we are handling it
1893+
oneTimeWatch.literal = parsedExpression.literal;
1894+
oneTimeWatch.constant = parsedExpression.constant;
1895+
oneTimeWatch.inputs = !post.$stateful && exp.inputs;
1896+
1897+
// Allow other delegates to run on this wrapped expression
1898+
addWatchDelegate(oneTimeWatch);
1899+
1900+
unwatch = scope.$watch(oneTimeWatch, listener, objectEquality, prettyPrintExpression);
1901+
18921902
return unwatch;
18931903

1894-
function oneTimeWatch(scope) {
1895-
return parsedExpression(scope);
1896-
}
1897-
function oneTimeListener(value, old, scope) {
1898-
lastValue = value;
1899-
if (isFunction(listener)) {
1900-
listener(value, old, scope);
1904+
function unwatchIfDone() {
1905+
if (isDone(lastValue)) {
1906+
unwatch();
19011907
}
1902-
if (isDone(value)) {
1903-
scope.$$postDigest(function() {
1904-
if (isDone(lastValue)) {
1905-
unwatch();
1906-
}
1907-
});
1908+
}
1909+
1910+
function oneTimeWatch(scope, locals, assign, inputs) {
1911+
lastValue = exp(scope, locals, assign, inputs);
1912+
if (isDone(lastValue)) {
1913+
scope.$$postDigest(unwatchIfDone);
19081914
}
1915+
return post(lastValue, scope, locals);
19091916
}
19101917
}
19111918

@@ -1937,39 +1944,45 @@ function $ParseProvider() {
19371944
return parsedExpression;
19381945
}
19391946

1940-
function addInterceptor(parsedExpression, interceptorFn) {
1941-
if (!interceptorFn) return parsedExpression;
1947+
function chainInterceptors(first, second) {
1948+
function chainedInterceptor(value, scope, locals) {
1949+
return second(first(value, scope, locals), scope, locals);
1950+
}
1951+
chainedInterceptor.$stateful = first.$stateful || second.$stateful;
19421952

1943-
var useInputs = false;
1953+
return chainedInterceptor;
1954+
}
19441955

1945-
var isDone = parsedExpression.literal ? isAllDefined : isDefined;
1956+
function addInterceptor(exp, interceptorFn) {
1957+
if (!interceptorFn) return exp;
19461958

1947-
function regularInterceptedExpression(scope, locals, assign, inputs) {
1948-
var value = useInputs && inputs ? inputs[0] : parsedExpression(scope, locals, assign, inputs);
1949-
return interceptorFn(value, scope, locals);
1950-
}
1959+
var intercepted = exp.$$intercepted || exp;
1960+
var interceptor = exp.$$interceptor
1961+
? chainInterceptors(exp.$$interceptor, interceptorFn)
1962+
: interceptorFn;
19511963

1952-
function oneTimeInterceptedExpression(scope, locals, assign, inputs) {
1953-
var value = useInputs && inputs ? inputs[0] : parsedExpression(scope, locals, assign, inputs);
1954-
var result = interceptorFn(value, scope, locals);
1955-
// we only return the interceptor's result if the
1956-
// initial value is defined (for bind-once)
1957-
return isDone(value) ? result : value;
1958-
}
1964+
var useInputs = false;
1965+
1966+
var fn = function regularInterceptedExpression(scope, locals, assign, inputs) {
1967+
var value = useInputs && inputs ? inputs[0] : intercepted(scope, locals, assign, inputs);
1968+
return interceptor(value, scope, locals);
1969+
};
19591970

1960-
var fn = parsedExpression.oneTime ? oneTimeInterceptedExpression : regularInterceptedExpression;
1971+
// Maintain references to the interceptor/intercepted
1972+
fn.$$intercepted = intercepted;
1973+
fn.$$interceptor = interceptor;
19611974

19621975
// Propogate the literal/oneTime/constant attributes
1963-
fn.literal = parsedExpression.literal;
1964-
fn.oneTime = parsedExpression.oneTime;
1965-
fn.constant = parsedExpression.constant;
1976+
fn.literal = intercepted.literal;
1977+
fn.oneTime = intercepted.oneTime;
1978+
fn.constant = intercepted.constant;
19661979

19671980
// Treat the interceptor like filters.
19681981
// If it is not $stateful then only watch its inputs.
19691982
// If the expression itself has no inputs then use the full expression as an input.
1970-
if (!interceptorFn.$stateful) {
1971-
useInputs = !parsedExpression.inputs;
1972-
fn.inputs = (parsedExpression.inputs ? parsedExpression.inputs : [parsedExpression]).map(function(e) {
1983+
if (!interceptor.$stateful) {
1984+
useInputs = !intercepted.inputs;
1985+
fn.inputs = (intercepted.inputs ? intercepted.inputs : [intercepted]).map(function(e) {
19731986
// Remove the isPure flag of inputs when it is not absolute because they are now wrapped in a
19741987
// potentially non-pure interceptor function.
19751988
if (e.isPure === PURITY_RELATIVE) {

test/ng/interpolateSpec.js

+16
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,22 @@ describe('$interpolate', function() {
149149
expect($rootScope.$countWatchers()).toBe(0);
150150
}));
151151

152+
it('should respect one-time bindings for literals', inject(function($interpolate, $rootScope) {
153+
var calls = [];
154+
$rootScope.$watch($interpolate('{{ ::{x: x} }}'), function(val) {
155+
calls.push(val);
156+
});
157+
158+
$rootScope.$apply();
159+
expect(calls.pop()).toBe('{}');
160+
161+
$rootScope.$apply('x = 1');
162+
expect(calls.pop()).toBe('{"x":1}');
163+
164+
$rootScope.$apply('x = 2');
165+
expect(calls.pop()).toBeUndefined();
166+
}));
167+
152168
it('should stop watching strings with no expressions after first execution',
153169
inject(function($interpolate, $rootScope) {
154170
var spy = jasmine.createSpy();

test/ng/parseSpec.js

+37-3
Original file line numberDiff line numberDiff line change
@@ -3312,13 +3312,12 @@ describe('parser', function() {
33123312

33133313
scope.$watch($parse('::[a]', interceptor));
33143314

3315-
// Would be great if interceptor-output was checked for changes and this didn't throw...
33163315
interceptorCalls = 0;
3317-
expect(function() { scope.$digest(); }).toThrowMinErr('$rootScope', 'infdig');
3316+
scope.$digest();
33183317
expect(interceptorCalls).not.toBe(0);
33193318

33203319
interceptorCalls = 0;
3321-
expect(function() { scope.$digest(); }).toThrowMinErr('$rootScope', 'infdig');
3320+
scope.$digest();
33223321
expect(interceptorCalls).not.toBe(0);
33233322
}));
33243323

@@ -3433,6 +3432,41 @@ describe('parser', function() {
34333432
expect(scope.$$watchersCount).toBe(0);
34343433
}));
34353434

3435+
it('should watch the intercepted value of one-time bindings', inject(function($parse, log) {
3436+
scope.$watch($parse('::{x:x, y:y}', function(lit) { return lit.x; }), log);
3437+
3438+
scope.$apply();
3439+
expect(log.empty()).toEqual([undefined]);
3440+
3441+
scope.$apply('x = 1');
3442+
expect(log.empty()).toEqual([1]);
3443+
3444+
scope.$apply('x = 2; y=1');
3445+
expect(log.empty()).toEqual([2]);
3446+
3447+
scope.$apply('x = 1; y=2');
3448+
expect(log.empty()).toEqual([]);
3449+
}));
3450+
3451+
it('should nest interceptors around eachother, not around the intercepted', inject(function($parse) {
3452+
function origin() { return 0; }
3453+
3454+
var fn = origin;
3455+
function addOne(n) { return n + 1; }
3456+
3457+
fn = $parse(fn, addOne);
3458+
expect(fn.$$intercepted).toBe(origin);
3459+
expect(fn()).toBe(1);
3460+
3461+
fn = $parse(fn, addOne);
3462+
expect(fn.$$intercepted).toBe(origin);
3463+
expect(fn()).toBe(2);
3464+
3465+
fn = $parse(fn, addOne);
3466+
expect(fn.$$intercepted).toBe(origin);
3467+
expect(fn()).toBe(3);
3468+
}));
3469+
34363470
it('should not propogate $$watchDelegate to the interceptor wrapped expression', inject(function($parse) {
34373471
function getter(s) {
34383472
return s.x;

0 commit comments

Comments
 (0)