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

Commit 2a8fe56

Browse files
committed
fix(ng:class): make ng:class friendly towards other code adding/removing classes
ng:class as well as ng:class-odd and ng:class-even always reset the class list to whatever it was before compilation, this makes it impossible to create another directive which adds its own classes on the element on which ng:class was applied. the fix simply removes all classes that were added previously by ng:class and add classes that the ng:class expression evaluates to. we can now guarantee that we won't clobber stuff added before or after compilation as long as all class names are unique. in order to implement this I had to beef up jqLite#addClass and jqLite#removeClass to be able to add/remove multiple classes without creating duplicates.
1 parent 622c3ec commit 2a8fe56

File tree

5 files changed

+167
-37
lines changed

5 files changed

+167
-37
lines changed

src/directives.js

+26-19
Original file line numberDiff line numberDiff line change
@@ -549,16 +549,11 @@ angularDirective("ng:submit", function(expression, element) {
549549

550550
function ngClass(selector) {
551551
return function(expression, element) {
552-
var existing = element[0].className + ' ';
553552
return function(element) {
554-
this.$watch(function(scope) {
553+
this.$watch(expression, function(scope, newVal, oldVal) {
555554
if (selector(scope.$index)) {
556-
var ngClassVal = scope.$eval(element.attr('ng:class'));
557-
if (isArray(ngClassVal)) ngClassVal = ngClassVal.join(' ');
558-
var value = scope.$eval(expression);
559-
if (isArray(value)) value = value.join(' ');
560-
if (ngClassVal && ngClassVal !== value) value = value + ' ' + ngClassVal;
561-
element[0].className = trim(existing + value);
555+
element.removeClass(isArray(oldVal) ? oldVal.join(' ') : oldVal)
556+
element.addClass(isArray(newVal) ? newVal.join(' ') : newVal);
562557
}
563558
});
564559
};
@@ -571,11 +566,17 @@ function ngClass(selector) {
571566
* @name angular.directive.ng:class
572567
*
573568
* @description
574-
* The `ng:class` allows you to set CSS class on HTML element
575-
* conditionally.
569+
* The `ng:class` allows you to set CSS class on HTML element dynamically by databinding an
570+
* expression that represents all classes to be added.
571+
*
572+
* The directive won't add duplicate classes if a particular class was already set.
573+
*
574+
* When the expression changes, the previously added classes are removed and only then the classes
575+
* new classes are added.
576576
*
577577
* @element ANY
578-
* @param {expression} expression {@link guide/dev_guide.expressions Expression} to eval.
578+
* @param {expression} expression {@link guide/dev_guide.expressions Expression} to eval. The result
579+
* of the evaluation can be a string representing space delimited class names or an array.
579580
*
580581
* @example
581582
<doc:example>
@@ -612,12 +613,15 @@ angularDirective("ng:class", ngClass(function(){return true;}));
612613
*
613614
* @description
614615
* The `ng:class-odd` and `ng:class-even` works exactly as
615-
* `ng:class`, except it works in conjunction with `ng:repeat`
616-
* and takes affect only on odd (even) rows.
616+
* {@link angular.directive.ng:class ng:class}, except it works in conjunction with `ng:repeat` and
617+
* takes affect only on odd (even) rows.
618+
*
619+
* This directive can be applied only within a scope of an
620+
* {@link angular.widget.@ng:repeat ng:repeat}.
617621
*
618622
* @element ANY
619-
* @param {expression} expression {@link guide/dev_guide.expressions Expression} to eval. Must be
620-
* inside `ng:repeat`.
623+
* @param {expression} expression {@link guide/dev_guide.expressions Expression} to eval. The result
624+
* of the evaluation can be a string representing space delimited class names or an array.
621625
*
622626
* @example
623627
<doc:example>
@@ -650,12 +654,15 @@ angularDirective("ng:class-odd", ngClass(function(i){return i % 2 === 0;}));
650654
*
651655
* @description
652656
* The `ng:class-odd` and `ng:class-even` works exactly as
653-
* `ng:class`, except it works in conjunction with `ng:repeat`
654-
* and takes affect only on odd (even) rows.
657+
* {@link angular.directive.ng:class ng:class}, except it works in conjunction with `ng:repeat` and
658+
* takes affect only on odd (even) rows.
659+
*
660+
* This directive can be applied only within a scope of an
661+
* {@link angular.widget.@ng:repeat ng:repeat}.
655662
*
656663
* @element ANY
657-
* @param {expression} expression {@link guide/dev_guide.expressions Expression} to eval. Must be
658-
* inside `ng:repeat`.
664+
* @param {expression} expression {@link guide/dev_guide.expressions Expression} to eval. The result
665+
* of the evaluation can be a string representing space delimited class names or an array.
659666
*
660667
* @example
661668
<doc:example>

src/jqLite.js

+15-7
Original file line numberDiff line numberDiff line change
@@ -155,16 +155,24 @@ function JQLiteHasClass(element, selector, _) {
155155
}
156156

157157
function JQLiteRemoveClass(element, selector) {
158-
element.className = trim(
159-
(" " + element.className + " ")
160-
.replace(/[\n\t]/g, " ")
161-
.replace(" " + selector + " ", " ")
162-
);
158+
if (selector) {
159+
forEach(selector.split(' '), function(cssClass) {
160+
element.className = trim(
161+
(" " + element.className + " ")
162+
.replace(/[\n\t]/g, " ")
163+
.replace(" " + trim(cssClass) + " ", " ")
164+
);
165+
});
166+
}
163167
}
164168

165169
function JQLiteAddClass(element, selector) {
166-
if (selector && !JQLiteHasClass(element, selector)) {
167-
element.className = trim(element.className + ' ' + selector);
170+
if (selector) {
171+
forEach(selector.split(' '), function(cssClass) {
172+
if (!JQLiteHasClass(element, cssClass)) {
173+
element.className = trim(element.className + ' ' + trim(cssClass));
174+
}
175+
});
168176
}
169177
}
170178

test/BinderSpec.js

-8
Original file line numberDiff line numberDiff line change
@@ -393,14 +393,6 @@ describe('Binder', function(){
393393
assertHidden(scope.$element);
394394
});
395395

396-
it('BindClassUndefined', function(){
397-
var scope = this.compile('<div ng:class="undefined"/>');
398-
scope.$apply();
399-
400-
assertEquals(
401-
'<div class="undefined" ng:class="undefined"></div>',
402-
sortedHtml(scope.$element));
403-
});
404396

405397
it('BindClass', function(){
406398
var scope = this.compile('<div ng:class="clazz"/>');

test/directivesSpec.js

+91-1
Original file line numberDiff line numberDiff line change
@@ -196,13 +196,103 @@ describe("directive", function(){
196196
expect(element.hasClass('B')).toBe(false);
197197
});
198198

199-
it('should support adding multiple classes', function(){
199+
200+
it('should support adding multiple classes via an array', function(){
200201
var scope = compile('<div class="existing" ng:class="[\'A\', \'B\']"></div>');
201202
scope.$digest();
202203
expect(element.hasClass('existing')).toBeTruthy();
203204
expect(element.hasClass('A')).toBeTruthy();
204205
expect(element.hasClass('B')).toBeTruthy();
205206
});
207+
208+
209+
it('should support adding multiple classes via a space delimited string', function(){
210+
var scope = compile('<div class="existing" ng:class="\'A B\'"></div>');
211+
scope.$digest();
212+
expect(element.hasClass('existing')).toBeTruthy();
213+
expect(element.hasClass('A')).toBeTruthy();
214+
expect(element.hasClass('B')).toBeTruthy();
215+
});
216+
217+
218+
it('should preserve class added post compilation with pre-existing classes', function() {
219+
var scope = compile('<div class="existing" ng:class="dynClass"></div>');
220+
scope.dynClass = 'A';
221+
scope.$digest();
222+
expect(element.hasClass('existing')).toBe(true);
223+
224+
// add extra class, change model and eval
225+
element.addClass('newClass');
226+
scope.dynClass = 'B';
227+
scope.$digest();
228+
229+
expect(element.hasClass('existing')).toBe(true);
230+
expect(element.hasClass('B')).toBe(true);
231+
expect(element.hasClass('newClass')).toBe(true);
232+
});
233+
234+
235+
it('should preserve class added post compilation without pre-existing classes"', function() {
236+
var scope = compile('<div ng:class="dynClass"></div>');
237+
scope.dynClass = 'A';
238+
scope.$digest();
239+
expect(element.hasClass('A')).toBe(true);
240+
241+
// add extra class, change model and eval
242+
element.addClass('newClass');
243+
scope.dynClass = 'B';
244+
scope.$digest();
245+
246+
expect(element.hasClass('B')).toBe(true);
247+
expect(element.hasClass('newClass')).toBe(true);
248+
});
249+
250+
251+
it('should preserve other classes with similar name"', function() {
252+
var scope = compile('<div class="ui-panel ui-selected" ng:class="dynCls"></div>');
253+
scope.dynCls = 'panel';
254+
scope.$digest();
255+
scope.dynCls = 'foo';
256+
scope.$digest();
257+
expect(element.attr('class')).toBe('ui-panel ui-selected ng-directive foo');
258+
});
259+
260+
261+
it('should not add duplicate classes', function() {
262+
var scope = compile('<div class="panel bar" ng:class="dynCls"></div>');
263+
scope.dynCls = 'panel';
264+
scope.$digest();
265+
expect(element.attr('class')).toBe('panel bar ng-directive');
266+
});
267+
268+
269+
it('should remove classes even if it was specified via class attribute', function() {
270+
var scope = compile('<div class="panel bar" ng:class="dynCls"></div>');
271+
scope.dynCls = 'panel';
272+
scope.$digest();
273+
scope.dynCls = 'window';
274+
scope.$digest();
275+
expect(element.attr('class')).toBe('bar ng-directive window');
276+
});
277+
278+
279+
it('should remove classes even if they were added by another code', function() {
280+
var scope = compile('<div ng:class="dynCls"></div>');
281+
scope.dynCls = 'foo';
282+
scope.$digest();
283+
element.addClass('foo');
284+
scope.dynCls = '';
285+
scope.$digest();
286+
expect(element.attr('class')).toBe('ng-directive');
287+
});
288+
289+
290+
it('should convert undefined and null values to an empty string', function() {
291+
var scope = compile('<div ng:class="dynCls"></div>');
292+
scope.dynCls = [undefined, null];
293+
scope.$digest();
294+
expect(element.attr('class')).toBe('ng-directive');
295+
});
206296
});
207297

208298

test/jqLiteSpec.js

+35-2
Original file line numberDiff line numberDiff line change
@@ -190,14 +190,15 @@ describe('jqLite', function(){
190190
});
191191

192192

193-
describe('addClass', function(){
194-
it('should allow adding of class', function(){
193+
describe('addClass', function() {
194+
it('should allow adding of class', function() {
195195
var selector = jqLite([a, b]);
196196
expect(selector.addClass('abc')).toEqual(selector);
197197
expect(jqLite(a).hasClass('abc')).toEqual(true);
198198
expect(jqLite(b).hasClass('abc')).toEqual(true);
199199
});
200200

201+
201202
it('should ignore falsy values', function() {
202203
var jqA = jqLite(a);
203204
expect(jqA[0].className).toBe('');
@@ -211,6 +212,28 @@ describe('jqLite', function(){
211212
jqA.addClass(false);
212213
expect(jqA[0].className).toBe('');
213214
});
215+
216+
217+
it('should allow multiple classes to be added in a single string', function() {
218+
var jqA = jqLite(a);
219+
expect(a.className).toBe('');
220+
221+
jqA.addClass('foo bar baz');
222+
expect(a.className).toBe('foo bar baz');
223+
});
224+
225+
226+
it('should not add duplicate classes', function() {
227+
var jqA = jqLite(a);
228+
expect(a.className).toBe('');
229+
230+
a.className = 'foo';
231+
jqA.addClass('foo');
232+
expect(a.className).toBe('foo');
233+
234+
jqA.addClass('bar foo baz');
235+
expect(a.className).toBe('foo bar baz');
236+
});
214237
});
215238

216239

@@ -246,6 +269,7 @@ describe('jqLite', function(){
246269
expect(jqLite(b).hasClass('abc')).toEqual(false);
247270
});
248271

272+
249273
it('should correctly remove middle class', function() {
250274
var element = jqLite('<div class="foo bar baz"></div>');
251275
expect(element.hasClass('bar')).toBe(true);
@@ -256,6 +280,15 @@ describe('jqLite', function(){
256280
expect(element.hasClass('bar')).toBe(false);
257281
expect(element.hasClass('baz')).toBe(true);
258282
});
283+
284+
285+
it('should remove multiple classes specified as one string', function() {
286+
var jqA = jqLite(a);
287+
288+
a.className = 'foo bar baz';
289+
jqA.removeClass('foo baz noexistent');
290+
expect(a.className).toBe('bar');
291+
});
259292
});
260293
});
261294

0 commit comments

Comments
 (0)