Skip to content

Commit 96d67c4

Browse files
committed
fix($parse): always pass the intercepted value to watchers
Fixes angular#16021
1 parent 1c04fdf commit 96d67c4

File tree

4 files changed

+117
-40
lines changed

4 files changed

+117
-40
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

+48-31
Original file line numberDiff line numberDiff line change
@@ -1884,28 +1884,37 @@ 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+
var useInputs = parsedExpression.inputs && !exp.inputs;
1892+
1893+
// Propogate the literal/inputs/constant attributes
1894+
// ... but not oneTime since we are handling it
1895+
oneTimeWatch.literal = parsedExpression.literal;
1896+
oneTimeWatch.constant = parsedExpression.constant;
1897+
oneTimeWatch.inputs = parsedExpression.inputs;
1898+
1899+
// Allow other delegates to run on this wrapped expression
1900+
addWatchDelegate(oneTimeWatch);
1901+
1902+
unwatch = scope.$watch(oneTimeWatch, listener, objectEquality, prettyPrintExpression);
1903+
18921904
return unwatch;
18931905

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);
1906+
function unwatchIfDone() {
1907+
if (isDone(lastValue)) {
1908+
unwatch();
19011909
}
1902-
if (isDone(value)) {
1903-
scope.$$postDigest(function() {
1904-
if (isDone(lastValue)) {
1905-
unwatch();
1906-
}
1907-
});
1910+
}
1911+
1912+
function oneTimeWatch(scope, locals, assign, inputs) {
1913+
lastValue = useInputs && inputs ? inputs[0] : exp(scope, locals, assign, inputs);
1914+
if (isDone(lastValue)) {
1915+
scope.$$postDigest(unwatchIfDone);
19081916
}
1917+
return post(lastValue, scope, locals);
19091918
}
19101919
}
19111920

@@ -1937,27 +1946,35 @@ function $ParseProvider() {
19371946
return parsedExpression;
19381947
}
19391948

1949+
function chainInterceptors(first, second) {
1950+
function chainedInterceptor(value, scope, locals) {
1951+
return second(first(value, scope, locals), scope, locals);
1952+
}
1953+
chainedInterceptor.$stateful = first.$stateful || second.$stateful;
1954+
1955+
return chainedInterceptor;
1956+
}
1957+
19401958
function addInterceptor(parsedExpression, interceptorFn) {
19411959
if (!interceptorFn) return parsedExpression;
19421960

1943-
var useInputs = false;
1961+
// Extract any existing interceptors out of the parsedExpression
1962+
// to ensure the original parsedExpression is always the $$intercepted
1963+
if (parsedExpression.$$interceptor) {
1964+
interceptorFn = chainInterceptors(parsedExpression.$$interceptor, interceptorFn);
1965+
parsedExpression = parsedExpression.$$intercepted;
1966+
}
19441967

1945-
var isDone = parsedExpression.literal ? isAllDefined : isDefined;
1968+
var useInputs = false;
19461969

1947-
function regularInterceptedExpression(scope, locals, assign, inputs) {
1970+
var fn = function interceptedExpression(scope, locals, assign, inputs) {
19481971
var value = useInputs && inputs ? inputs[0] : parsedExpression(scope, locals, assign, inputs);
19491972
return interceptorFn(value, scope, locals);
1950-
}
1951-
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-
}
1973+
};
19591974

1960-
var fn = parsedExpression.oneTime ? oneTimeInterceptedExpression : regularInterceptedExpression;
1975+
// Maintain references to the interceptor/intercepted
1976+
fn.$$intercepted = parsedExpression;
1977+
fn.$$interceptor = interceptorFn;
19611978

19621979
// Propogate the literal/oneTime/constant attributes
19631980
fn.literal = parsedExpression.literal;

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

+53-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,57 @@ 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 watch the intercepted value of one-time bindings in nested interceptors', inject(function($parse, log) {
3452+
scope.$watch($parse($parse('::{x:x, y:y}', function(lit) { return lit.x; }), identity), log);
3453+
3454+
scope.$apply();
3455+
expect(log.empty()).toEqual([undefined]);
3456+
3457+
scope.$apply('x = 1');
3458+
expect(log.empty()).toEqual([1]);
3459+
3460+
scope.$apply('x = 2; y=1');
3461+
expect(log.empty()).toEqual([2]);
3462+
3463+
scope.$apply('x = 1; y=2');
3464+
expect(log.empty()).toEqual([]);
3465+
}));
3466+
3467+
it('should nest interceptors around eachother, not around the intercepted', inject(function($parse) {
3468+
function origin() { return 0; }
3469+
3470+
var fn = origin;
3471+
function addOne(n) { return n + 1; }
3472+
3473+
fn = $parse(fn, addOne);
3474+
expect(fn.$$intercepted).toBe(origin);
3475+
expect(fn()).toBe(1);
3476+
3477+
fn = $parse(fn, addOne);
3478+
expect(fn.$$intercepted).toBe(origin);
3479+
expect(fn()).toBe(2);
3480+
3481+
fn = $parse(fn, addOne);
3482+
expect(fn.$$intercepted).toBe(origin);
3483+
expect(fn()).toBe(3);
3484+
}));
3485+
34363486
it('should not propogate $$watchDelegate to the interceptor wrapped expression', inject(function($parse) {
34373487
function getter(s) {
34383488
return s.x;

0 commit comments

Comments
 (0)