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

Commit 55fe6d6

Browse files
committed
fix(ngClass): handle ngClassOdd/Even affecting the same classes
The basic approach is to introduce a new elt.data() called $classCounts that keeps track of how many times ngClass, ngClassEven, or ngClassOdd tries to add a given class. The class is added only when the count goes from 0 to 1, and removed only when the count hits 0. To avoid duplicating work, some of the logic for checking which classes to add/remove move into this directive and the directive calls $animate. Closes #5271
1 parent f4c08fe commit 55fe6d6

File tree

2 files changed

+92
-25
lines changed

2 files changed

+92
-25
lines changed

src/ng/directive/ngClass.js

+79-25
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
function classDirective(name, selector) {
44
name = 'ngClass' + name;
5-
return function() {
5+
return ['$animate', function($animate) {
66
return {
77
restrict: 'AC',
88
link: function(scope, element, attr) {
@@ -20,46 +20,100 @@ function classDirective(name, selector) {
2020
// jshint bitwise: false
2121
var mod = $index & 1;
2222
if (mod !== old$index & 1) {
23-
var classes = flattenClasses(scope.$eval(attr[name]));
23+
var classes = arrayClasses(scope.$eval(attr[name]));
2424
mod === selector ?
25-
attr.$addClass(classes) :
26-
attr.$removeClass(classes);
25+
addClasses(classes) :
26+
removeClasses(classes);
2727
}
2828
});
2929
}
3030

31+
function addClasses(classes) {
32+
var newClasses = digestClassCounts(classes, 1);
33+
attr.$addClass(newClasses);
34+
}
35+
36+
function removeClasses(classes) {
37+
var newClasses = digestClassCounts(classes, -1);
38+
attr.$removeClass(newClasses);
39+
}
40+
41+
function digestClassCounts (classes, count) {
42+
var classCounts = element.data('$classCounts') || {};
43+
var classesToUpdate = [];
44+
forEach(classes, function (className) {
45+
if (count > 0 || classCounts[className]) {
46+
classCounts[className] = (classCounts[className] || 0) + count;
47+
if (classCounts[className] === +(count > 0)) {
48+
classesToUpdate.push(className);
49+
}
50+
}
51+
});
52+
element.data('$classCounts', classCounts);
53+
return classesToUpdate.join(' ');
54+
}
55+
56+
function updateClasses (oldClasses, newClasses) {
57+
var toAdd = arrayDifference(newClasses, oldClasses);
58+
var toRemove = arrayDifference(oldClasses, newClasses);
59+
toRemove = digestClassCounts(toRemove, -1);
60+
toAdd = digestClassCounts(toAdd, 1);
61+
62+
if (toAdd.length === 0) {
63+
$animate.removeClass(element, toRemove);
64+
} else if (toRemove.length === 0) {
65+
$animate.addClass(element, toAdd);
66+
} else {
67+
$animate.setClass(element, toAdd, toRemove);
68+
}
69+
}
3170

3271
function ngClassWatchAction(newVal) {
3372
if (selector === true || scope.$index % 2 === selector) {
34-
var newClasses = flattenClasses(newVal || '');
35-
if(!oldVal) {
36-
attr.$addClass(newClasses);
37-
} else if(!equals(newVal,oldVal)) {
38-
attr.$updateClass(newClasses, flattenClasses(oldVal));
73+
var newClasses = arrayClasses(newVal || []);
74+
if (!oldVal) {
75+
addClasses(newClasses);
76+
} else if (!equals(newVal,oldVal)) {
77+
var oldClasses = arrayClasses(oldVal);
78+
updateClasses(oldClasses, newClasses);
3979
}
4080
}
4181
oldVal = copy(newVal);
4282
}
83+
}
84+
};
4385

86+
function arrayDifference(tokens1, tokens2) {
87+
var values = [];
4488

45-
function flattenClasses(classVal) {
46-
if(isArray(classVal)) {
47-
return classVal.join(' ');
48-
} else if (isObject(classVal)) {
49-
var classes = [], i = 0;
50-
forEach(classVal, function(v, k) {
51-
if (v) {
52-
classes.push(k);
53-
}
54-
});
55-
return classes.join(' ');
56-
}
57-
58-
return classVal;
89+
outer:
90+
for(var i = 0; i < tokens1.length; i++) {
91+
var token = tokens1[i];
92+
for(var j = 0; j < tokens2.length; j++) {
93+
if(token == tokens2[j]) continue outer;
5994
}
95+
values.push(token);
6096
}
61-
};
62-
};
97+
return values;
98+
}
99+
100+
function arrayClasses (classVal) {
101+
if (isArray(classVal)) {
102+
return classVal;
103+
} else if (isString(classVal)) {
104+
return classVal.split(' ');
105+
} else if (isObject(classVal)) {
106+
var classes = [], i = 0;
107+
forEach(classVal, function(v, k) {
108+
if (v) {
109+
classes.push(k);
110+
}
111+
});
112+
return classes;
113+
}
114+
return classVal;
115+
}
116+
}];
63117
}
64118

65119
/**

test/ng/directive/ngClassSpec.js

+13
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,19 @@ describe('ngClass', function() {
192192
}));
193193

194194

195+
it("should allow ngClassOdd/Even on the same element with overlapping classes", inject(function($rootScope, $compile, $animate) {
196+
var className;
197+
198+
element = $compile('<ul><li ng-repeat="i in [0,1,2]" ng-class-odd="\'same odd\'" ng-class-even="\'same even\'"></li><ul>')($rootScope);
199+
$rootScope.$digest();
200+
var e1 = jqLite(element[0].childNodes[1]);
201+
var e2 = jqLite(element[0].childNodes[5]);
202+
expect(e1.hasClass('same')).toBeTruthy();
203+
expect(e1.hasClass('odd')).toBeTruthy();
204+
expect(e2.hasClass('same')).toBeTruthy();
205+
expect(e2.hasClass('odd')).toBeTruthy();
206+
}));
207+
195208
it('should allow both ngClass and ngClassOdd/Even with multiple classes', inject(function($rootScope, $compile) {
196209
element = $compile('<ul>' +
197210
'<li ng-repeat="i in [0,1]" ng-class="[\'A\', \'B\']" ' +

0 commit comments

Comments
 (0)