diff --git a/src/ng/directive/ngClass.js b/src/ng/directive/ngClass.js index 090c2c387f8c..a26c1c77187b 100644 --- a/src/ng/directive/ngClass.js +++ b/src/ng/directive/ngClass.js @@ -14,6 +14,13 @@ function classDirective(name, selector) { return { restrict: 'AC', link: function(scope, element, attr) { + var expression = attr[name].trim(); + var isOneTime = (expression.charAt(0) === ':') && (expression.charAt(1) === ':'); + + var watchInterceptor = isOneTime ? toFlatValue : toClassString; + var watchExpression = $parse(expression, watchInterceptor); + var watchAction = isOneTime ? ngClassOneTimeWatchAction : ngClassWatchAction; + var classCounts = element.data('$classCounts'); var oldModulo = true; var oldClassString; @@ -36,7 +43,7 @@ function classDirective(name, selector) { scope.$watch(indexWatchExpression, ngClassIndexWatchAction); } - scope.$watch($parse(attr[name], toClassString), ngClassWatchAction); + scope.$watch(watchExpression, watchAction, isOneTime); function addClasses(classString) { classString = digestClassCounts(split(classString), 1); @@ -78,9 +85,9 @@ function classDirective(name, selector) { } function ngClassIndexWatchAction(newModulo) { - // This watch-action should run before the `ngClassWatchAction()`, thus it + // This watch-action should run before the `ngClass[OneTime]WatchAction()`, thus it // adds/removes `oldClassString`. If the `ngClass` expression has changed as well, the - // `ngClassWatchAction()` will update the classes. + // `ngClass[OneTime]WatchAction()` will update the classes. if (newModulo === selector) { addClasses(oldClassString); } else { @@ -90,13 +97,15 @@ function classDirective(name, selector) { oldModulo = newModulo; } - 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); + function ngClassOneTimeWatchAction(newClassValue) { + var newClassString = toClassString(newClassValue); + + if (newClassString !== oldClassString) { + ngClassWatchAction(newClassString); } + } + function ngClassWatchAction(newClassString) { if (oldModulo === selector) { updateClasses(oldClassString, newClassString); } @@ -143,6 +152,34 @@ function classDirective(name, selector) { return classString; } + + function toFlatValue(classValue) { + var flatValue = classValue; + + if (isArray(classValue)) { + flatValue = classValue.map(toFlatValue); + } else if (isObject(classValue)) { + var hasUndefined = false; + + flatValue = Object.keys(classValue).filter(function(key) { + var value = classValue[key]; + + if (!hasUndefined && isUndefined(value)) { + hasUndefined = true; + } + + return value; + }); + + if (hasUndefined) { + // Prevent the `oneTimeLiteralWatchInterceptor` from unregistering + // the watcher, by including at least one `undefined` value. + flatValue.push(undefined); + } + } + + return flatValue; + } } /** diff --git a/src/ng/parse.js b/src/ng/parse.js index 155e7589b083..7aa379edf5c3 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -1801,8 +1801,8 @@ function $ParseProvider() { if (parsedExpression.constant) { parsedExpression.$$watchDelegate = constantWatchDelegate; } else if (oneTime) { - parsedExpression.oneTime = true; - parsedExpression.$$watchDelegate = oneTimeWatchDelegate; + parsedExpression.$$watchDelegate = parsedExpression.literal ? + oneTimeLiteralWatchDelegate : oneTimeWatchDelegate; } else if (parsedExpression.inputs) { parsedExpression.$$watchDelegate = inputsWatchDelegate; } @@ -1888,7 +1888,6 @@ 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); @@ -1905,9 +1904,9 @@ function $ParseProvider() { if (isFunction(listener)) { listener(value, old, scope); } - if (isDone(value)) { + if (isDefined(value)) { scope.$$postDigest(function() { - if (isDone(lastValue)) { + if (isDefined(lastValue)) { unwatch(); } }); @@ -1915,12 +1914,31 @@ function $ParseProvider() { } } - function isAllDefined(value) { - var allDefined = true; - forEach(value, function(val) { - if (!isDefined(val)) allDefined = false; - }); - return allDefined; + function oneTimeLiteralWatchDelegate(scope, listener, objectEquality, parsedExpression) { + var unwatch, lastValue; + unwatch = scope.$watch(function oneTimeWatch(scope) { + return parsedExpression(scope); + }, function oneTimeListener(value, old, scope) { + lastValue = value; + if (isFunction(listener)) { + listener(value, old, scope); + } + if (isAllDefined(value)) { + scope.$$postDigest(function() { + if (isAllDefined(lastValue)) unwatch(); + }); + } + }, objectEquality); + + return unwatch; + + function isAllDefined(value) { + var allDefined = true; + forEach(value, function(val) { + if (!isDefined(val)) allDefined = false; + }); + return allDefined; + } } function constantWatchDelegate(scope, listener, objectEquality, parsedExpression) { @@ -1936,28 +1954,22 @@ function $ParseProvider() { var watchDelegate = parsedExpression.$$watchDelegate; var useInputs = false; - var isDone = parsedExpression.literal ? isAllDefined : isDefined; + var regularWatch = + watchDelegate !== oneTimeLiteralWatchDelegate && + watchDelegate !== oneTimeWatchDelegate; - function regularInterceptedExpression(scope, locals, assign, inputs) { + var fn = regularWatch ? function regularInterceptedExpression(scope, locals, assign, inputs) { var value = useInputs && inputs ? inputs[0] : parsedExpression(scope, locals, assign, inputs); return interceptorFn(value, scope, locals); - } - - function oneTimeInterceptedExpression(scope, locals, assign, inputs) { - var value = useInputs && inputs ? inputs[0] : parsedExpression(scope, locals, assign, inputs); + } : function oneTimeInterceptedExpression(scope, locals, assign, inputs) { + var value = parsedExpression(scope, locals, assign, inputs); var result = interceptorFn(value, scope, locals); // 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; - - // Propogate the literal/oneTime attributes - fn.literal = parsedExpression.literal; - fn.oneTime = parsedExpression.oneTime; + return isDefined(value) ? result : value; + }; - // Propagate or create inputs / $$watchDelegates + // Propagate $$watchDelegates other then inputsWatchDelegate useInputs = !parsedExpression.inputs; if (watchDelegate && watchDelegate !== inputsWatchDelegate) { fn.$$watchDelegate = watchDelegate; diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index 0af6b7ebcf6f..463e4e6a497f 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -2688,86 +2688,82 @@ describe('parser', function() { expect($parse(':: ').literal).toBe(true); })); - [true, false].forEach(function(isDeep) { - describe(isDeep ? 'deepWatch' : 'watch', function() { - it('should only become stable when all the properties of an object have defined values', inject(function($parse, $rootScope, log) { - var fn = $parse('::{foo: foo, bar: bar}'); - $rootScope.$watch(fn, function(value) { log(value); }, isDeep); - - expect(log.empty()).toEqual([]); - expect($rootScope.$$watchers.length).toBe(1); - - $rootScope.$digest(); - expect($rootScope.$$watchers.length).toBe(1); - expect(log.empty()).toEqual([{foo: undefined, bar: undefined}]); - - $rootScope.foo = 'foo'; - $rootScope.$digest(); - expect($rootScope.$$watchers.length).toBe(1); - expect(log.empty()).toEqual([{foo: 'foo', bar: undefined}]); - - $rootScope.foo = 'foobar'; - $rootScope.bar = 'bar'; - $rootScope.$digest(); - expect($rootScope.$$watchers.length).toBe(0); - expect(log.empty()).toEqual([{foo: 'foobar', bar: 'bar'}]); - - $rootScope.foo = 'baz'; - $rootScope.$digest(); - expect($rootScope.$$watchers.length).toBe(0); - expect(log.empty()).toEqual([]); - })); + it('should only become stable when all the properties of an object have defined values', inject(function($parse, $rootScope, log) { + var fn = $parse('::{foo: foo, bar: bar}'); + $rootScope.$watch(fn, function(value) { log(value); }, true); + + expect(log.empty()).toEqual([]); + expect($rootScope.$$watchers.length).toBe(1); + + $rootScope.$digest(); + expect($rootScope.$$watchers.length).toBe(1); + expect(log.empty()).toEqual([{foo: undefined, bar: undefined}]); + + $rootScope.foo = 'foo'; + $rootScope.$digest(); + expect($rootScope.$$watchers.length).toBe(1); + expect(log.empty()).toEqual([{foo: 'foo', bar: undefined}]); + + $rootScope.foo = 'foobar'; + $rootScope.bar = 'bar'; + $rootScope.$digest(); + expect($rootScope.$$watchers.length).toBe(0); + expect(log.empty()).toEqual([{foo: 'foobar', bar: 'bar'}]); + + $rootScope.foo = 'baz'; + $rootScope.$digest(); + expect($rootScope.$$watchers.length).toBe(0); + expect(log.empty()).toEqual([]); + })); - it('should only become stable when all the elements of an array have defined values', inject(function($parse, $rootScope, log) { - var fn = $parse('::[foo,bar]'); - $rootScope.$watch(fn, function(value) { log(value); }, isDeep); + it('should only become stable when all the elements of an array have defined values', inject(function($parse, $rootScope, log) { + var fn = $parse('::[foo,bar]'); + $rootScope.$watch(fn, function(value) { log(value); }, true); - expect(log.empty()).toEqual([]); - expect($rootScope.$$watchers.length).toBe(1); + expect(log.empty()).toEqual([]); + expect($rootScope.$$watchers.length).toBe(1); - $rootScope.$digest(); - expect($rootScope.$$watchers.length).toBe(1); - expect(log.empty()).toEqual([[undefined, undefined]]); + $rootScope.$digest(); + expect($rootScope.$$watchers.length).toBe(1); + expect(log.empty()).toEqual([[undefined, undefined]]); - $rootScope.foo = 'foo'; - $rootScope.$digest(); - expect($rootScope.$$watchers.length).toBe(1); - expect(log.empty()).toEqual([['foo', undefined]]); + $rootScope.foo = 'foo'; + $rootScope.$digest(); + expect($rootScope.$$watchers.length).toBe(1); + expect(log.empty()).toEqual([['foo', undefined]]); - $rootScope.foo = 'foobar'; - $rootScope.bar = 'bar'; - $rootScope.$digest(); - expect($rootScope.$$watchers.length).toBe(0); - expect(log.empty()).toEqual([['foobar', 'bar']]); + $rootScope.foo = 'foobar'; + $rootScope.bar = 'bar'; + $rootScope.$digest(); + expect($rootScope.$$watchers.length).toBe(0); + expect(log.empty()).toEqual([['foobar', 'bar']]); - $rootScope.foo = 'baz'; - $rootScope.$digest(); - expect($rootScope.$$watchers.length).toBe(0); - expect(log.empty()).toEqual([]); - })); + $rootScope.foo = 'baz'; + $rootScope.$digest(); + expect($rootScope.$$watchers.length).toBe(0); + expect(log.empty()).toEqual([]); + })); - it('should only become stable when all the elements of an array have defined values at the end of a $digest', inject(function($parse, $rootScope, log) { - var fn = $parse('::[foo]'); - $rootScope.$watch(fn, function(value) { log(value); }, isDeep); - $rootScope.$watch('foo', function() { if ($rootScope.foo === 'bar') {$rootScope.foo = undefined; } }); - - $rootScope.foo = 'bar'; - $rootScope.$digest(); - expect($rootScope.$$watchers.length).toBe(2); - expect(log.empty()).toEqual([['bar'], [undefined]]); - - $rootScope.foo = 'baz'; - $rootScope.$digest(); - expect($rootScope.$$watchers.length).toBe(1); - expect(log.empty()).toEqual([['baz']]); - - $rootScope.bar = 'qux'; - $rootScope.$digest(); - expect($rootScope.$$watchers.length).toBe(1); - expect(log).toEqual([]); - })); - }); - }); + it('should only become stable when all the elements of an array have defined values at the end of a $digest', inject(function($parse, $rootScope, log) { + var fn = $parse('::[foo]'); + $rootScope.$watch(fn, function(value) { log(value); }, true); + $rootScope.$watch('foo', function() { if ($rootScope.foo === 'bar') {$rootScope.foo = undefined; } }); + + $rootScope.foo = 'bar'; + $rootScope.$digest(); + expect($rootScope.$$watchers.length).toBe(2); + expect(log.empty()).toEqual([['bar'], [undefined]]); + + $rootScope.foo = 'baz'; + $rootScope.$digest(); + expect($rootScope.$$watchers.length).toBe(1); + expect(log.empty()).toEqual([['baz']]); + + $rootScope.bar = 'qux'; + $rootScope.$digest(); + expect($rootScope.$$watchers.length).toBe(1); + expect(log).toEqual([]); + })); }); });