diff --git a/src/ng/directive/ngClass.js b/src/ng/directive/ngClass.js index 7e138b065dc2..a26c1c77187b 100644 --- a/src/ng/directive/ngClass.js +++ b/src/ng/directive/ngClass.js @@ -8,51 +8,71 @@ function classDirective(name, selector) { name = 'ngClass' + name; - return ['$animate', function($animate) { + var indexWatchExpression; + + return ['$parse', function($parse) { return { restrict: 'AC', link: function(scope, element, attr) { - var oldVal; + var expression = attr[name].trim(); + var isOneTime = (expression.charAt(0) === ':') && (expression.charAt(1) === ':'); - scope.$watch(attr[name], ngClassWatchAction, true); + var watchInterceptor = isOneTime ? toFlatValue : toClassString; + var watchExpression = $parse(expression, watchInterceptor); + var watchAction = isOneTime ? ngClassOneTimeWatchAction : ngClassWatchAction; - attr.$observe('class', function(value) { - ngClassWatchAction(scope.$eval(attr[name])); - }); + var classCounts = element.data('$classCounts'); + var oldModulo = true; + var oldClassString; + if (!classCounts) { + // Use createMap() to prevent class assumptions involving property + // names in Object.prototype + classCounts = createMap(); + element.data('$classCounts', classCounts); + } if (name !== 'ngClass') { - scope.$watch('$index', function($index, old$index) { - /* eslint-disable no-bitwise */ - var mod = $index & 1; - if (mod !== (old$index & 1)) { - var classes = arrayClasses(scope.$eval(attr[name])); - if (mod === selector) { - addClasses(classes); - } else { - removeClasses(classes); - } - } - /* eslint-enable */ - }); + if (!indexWatchExpression) { + indexWatchExpression = $parse('$index', function moduloTwo($index) { + // eslint-disable-next-line no-bitwise + return $index & 1; + }); + } + + scope.$watch(indexWatchExpression, ngClassIndexWatchAction); } - function addClasses(classes) { - var newClasses = digestClassCounts(classes, 1); - attr.$addClass(newClasses); + scope.$watch(watchExpression, watchAction, isOneTime); + + function addClasses(classString) { + classString = digestClassCounts(split(classString), 1); + attr.$addClass(classString); } - function removeClasses(classes) { - var newClasses = digestClassCounts(classes, -1); - attr.$removeClass(newClasses); + function removeClasses(classString) { + classString = digestClassCounts(split(classString), -1); + attr.$removeClass(classString); } - function digestClassCounts(classes, count) { - // Use createMap() to prevent class assumptions involving property - // names in Object.prototype - var classCounts = element.data('$classCounts') || createMap(); + function updateClasses(oldClassString, newClassString) { + var oldClassArray = split(oldClassString); + var newClassArray = split(newClassString); + + var toRemoveArray = arrayDifference(oldClassArray, newClassArray); + var toAddArray = arrayDifference(newClassArray, oldClassArray); + + var toRemoveString = digestClassCounts(toRemoveArray, -1); + var toAddString = digestClassCounts(toAddArray, 1); + + attr.$addClass(toAddString); + attr.$removeClass(toRemoveString); + } + + function digestClassCounts(classArray, count) { var classesToUpdate = []; - forEach(classes, function(className) { + + forEach(classArray, function(className) { if (count > 0 || classCounts[className]) { classCounts[className] = (classCounts[className] || 0) + count; if (classCounts[className] === +(count > 0)) { @@ -60,77 +80,106 @@ function classDirective(name, selector) { } } }); - element.data('$classCounts', classCounts); + return classesToUpdate.join(' '); } - function updateClasses(oldClasses, newClasses) { - var toAdd = arrayDifference(newClasses, oldClasses); - var toRemove = arrayDifference(oldClasses, newClasses); - toAdd = digestClassCounts(toAdd, 1); - toRemove = digestClassCounts(toRemove, -1); - if (toAdd && toAdd.length) { - $animate.addClass(element, toAdd); - } - if (toRemove && toRemove.length) { - $animate.removeClass(element, toRemove); + function ngClassIndexWatchAction(newModulo) { + // This watch-action should run before the `ngClass[OneTime]WatchAction()`, thus it + // adds/removes `oldClassString`. If the `ngClass` expression has changed as well, the + // `ngClass[OneTime]WatchAction()` will update the classes. + if (newModulo === selector) { + addClasses(oldClassString); + } else { + removeClasses(oldClassString); } + + oldModulo = newModulo; } - function ngClassWatchAction(newVal) { - // eslint-disable-next-line no-bitwise - if (selector === true || (scope.$index & 1) === selector) { - var newClasses = arrayClasses(newVal || []); - if (!oldVal) { - addClasses(newClasses); - } else if (!equals(newVal,oldVal)) { - var oldClasses = arrayClasses(oldVal); - updateClasses(oldClasses, newClasses); - } + function ngClassOneTimeWatchAction(newClassValue) { + var newClassString = toClassString(newClassValue); + + if (newClassString !== oldClassString) { + ngClassWatchAction(newClassString); } - if (isArray(newVal)) { - oldVal = newVal.map(function(v) { return shallowCopy(v); }); - } else { - oldVal = shallowCopy(newVal); + } + + function ngClassWatchAction(newClassString) { + if (oldModulo === selector) { + updateClasses(oldClassString, newClassString); } + + oldClassString = newClassString; } } }; + }]; - function arrayDifference(tokens1, tokens2) { - var values = []; + // Helpers + function arrayDifference(tokens1, tokens2) { + if (!tokens1 || !tokens1.length) return []; + if (!tokens2 || !tokens2.length) return tokens1; - outer: - for (var i = 0; i < tokens1.length; i++) { - var token = tokens1[i]; - for (var j = 0; j < tokens2.length; j++) { - if (token === tokens2[j]) continue outer; - } - values.push(token); + var values = []; + + outer: + for (var i = 0; i < tokens1.length; i++) { + var token = tokens1[i]; + for (var j = 0; j < tokens2.length; j++) { + if (token === tokens2[j]) continue outer; } - return values; + values.push(token); } - function arrayClasses(classVal) { - var classes = []; - if (isArray(classVal)) { - forEach(classVal, function(v) { - classes = classes.concat(arrayClasses(v)); - }); - return classes; - } else if (isString(classVal)) { - return classVal.split(' '); - } else if (isObject(classVal)) { - forEach(classVal, function(v, k) { - if (v) { - classes = classes.concat(k.split(' ')); - } - }); - return classes; + return values; + } + + function split(classString) { + return classString && classString.split(' '); + } + + function toClassString(classValue) { + var classString = classValue; + + if (isArray(classValue)) { + classString = classValue.map(toClassString).join(' '); + } else if (isObject(classValue)) { + classString = Object.keys(classValue). + filter(function(key) { return classValue[key]; }). + join(' '); + } + + 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 classVal; } - }]; + + return flatValue; + } } /** diff --git a/test/ng/directive/ngClassSpec.js b/test/ng/directive/ngClassSpec.js index b30d1318317c..ae671d050bd3 100644 --- a/test/ng/directive/ngClassSpec.js +++ b/test/ng/directive/ngClassSpec.js @@ -244,21 +244,34 @@ describe('ngClass', function() { })); - it('should allow ngClassOdd/Even on the same element with overlapping classes', inject(function($rootScope, $compile, $animate) { - var className; - - element = $compile('