Note: If you want to skip to the final code (in both MooTools and framework-neutral flavors), it’s at the bottom of this post.
The problem
Readers of this blog know that I enjoy MooTools. Like other JavaScript frameworks, it has many excellent features, including addClass
and removeClass
functions, which I use all the time. However, when I was working on my CustomInput class the other day, I discovered a major shortcoming of MooTools’ removeClass
function — it doesn’t work very well when trying to remove multiple classes (as of MooTools version 1.2.4). In particular, if you specify multiple classes to remove, removeClass
will only work if the classes are listed in that element’s className
property in the order specified.
//Assuming element has className "hello cruel world"
//Single terms work fine
element.removeClass("world"); //becomes "hello world"
element.removeClass("hello"); //becomes " cruel world"
//Multiple terms listed in same order as className works fine
element.removeClass("hello cruel"); //becomes " world"
//Multiple terms NOT listed in same order as className fail
element.removeClass("hello world"); //remains "hello cruel world"
element.removeClass("cruel hello"); //remains "hello cruel world"
Code language: JavaScript (javascript)
The cause
A peek at MooTools’ removeClass code reveals the shortcoming:
removeClass: function(className){
this.className = this.className.replace(new RegExp('(^|s)' + className + '(?:s|$)'), '$1');
return this;
}
Code language: JavaScript (javascript)
The string containing the class names you want to remove is passed as-is; it remains a whole string, and is not broken down into substrings. So "hello world"
remains the single string "hello world"
instead of two separate strings "hello"
and "world"
.
I tested jQuery’s removeClass
function and noticed it doesn’t have the same problem; it will remove each word, no matter what order you specify. Taking a look under the hood reveals a completely different approach to removeClass
:
removeClass: function( value ) {
if ( jQuery.isFunction(value) ) {
return this.each(function(i) {
var self = jQuery(this);
self.removeClass( value.call(this, i, self.attr("class")) );
});
}
if ( (value && typeof value === "string") || value === undefined ) {
var classNames = (value || "").split(rspace);
for ( var i = 0, l = this.length; i < l; i++ ) {
var elem = this[i];
if ( elem.nodeType === 1 && elem.className ) {
if ( value ) {
var className = (" " + elem.className + " ").replace(rclass, " ");
for ( var c = 0, cl = classNames.length; c < cl; c++ ) {
className = className.replace(" " + classNames[c] + " ", " ");
}
elem.className = jQuery.trim( className );
} else {
elem.className = "";
}
}
}
}
return this;
}
Code language: JavaScript (javascript)
The big difference between jQuery and MooTools in this case is that jQuery converts the arguments to an array (split using spaces as a delimiter) then loops through the className
property to search for each word in the array, whereas MooTools performs a simple full-string replace using regular expressions.
The search for a solution
My first reaction was to build a MooTools-flavored variation of jQuery’s removeClass
code.
Element.implement({
removeClasses: function (classNames) {
if(this.className){
var classNameString = this.className;
classNames.split(/s+/).each(function (term){
classNameString = classNameString.replace(term, " ");
});
this.className = classNameString.clean();
}
return this;
}
});
Code language: JavaScript (javascript)
It follows the same basic principles of the jQuery version, but uses MooTools’ Array.each
and String.clean
utility functions. It works well, but I wasn’t thrilled about using a loop. I thought maybe a regular expression would be better suited for the job.
I eventually came up with this:
(purposely verbose to explain what’s happening)
Element.implement({
removeClasses: function (classNames) {
if(classNames && this.className){
//Replace all spaces in classNames with vertical beams
var terms = classNames.replace(/s+/g, "|");
//Create a regular expression using terms variable
var reg = new RegExp('b(' + terms + ')b', 'g');
//Use the new regular expression to replace all specified terms with a space
var newClass = this.className.replace(reg, " ");
//Use MooTools' 'clean' method to remove extraneous spaces
newClass = newClass.clean();
//Set element's classname to new cleaned list of classes
this.className = newClass;
}
return this;
}
});
Code language: JavaScript (javascript)
Notes:
- I’m no expert on JavaScript speed tests, so for all I know a loop might be quicker. However, a regular expression feels more elegant.
- I named my utility removesClasses so it doesn’t overwrite MooTools’ built-in utility.
Final MooTools version
Here’s a more concise version:
Element.implement({
removeClasses: function (classNames) {
this.className = this.className.replace(new RegExp("b(" + classNames.replace(/s+/g, "|") + ")b", "g"), " ").clean();
return this;
}
});
Code language: JavaScript (javascript)
You can see it in action via the MooShell (apparently the MooTools Shell is no longer online)
Standalone (framework-neutral) version
For those of you who don’t use MooTools, a few small edits will allow you to use this code without relying on any outside JavaScript libraries. First, a verbose version explaining what’s happening:
function removeClass(el, classNames) {
//Only run if the element is available and supports the className property
if(el && el.className && classNames){
//Replace all spaces in classNames with vertical beams
var terms = classNames.replace(/s+/g, "|");
//Create a regular expression using terms variable
var reg = new RegExp('b(' + terms + ')b', 'g');
//Use the new regular expression to replace all specified terms with a space
var newClass = el.className.replace(reg, " ");
//Use regular expression to remove extraneous whitespace between class names
newClass = newClass.replace(/s+/g, " ");
//Use regular expression to remove all whitespace at front and end of string
newClass = newClass.replace(/^s+|s+$/g, "");
//Set element's classname to new cleaned list of classes
el.className = newClass;
}
}
Code language: JavaScript (javascript)
The concise version:
function removeClass(el, classNames) {
if(el && el.className && classNames){
el.className = el.className.replace(new RegExp("b(" + classNames.replace(/s+/g, "|") + ")b", "g"), " ").replace(/s+/g, " ").replace(/^s+|s+$/g, "");
}
}
Code language: JavaScript (javascript)
Used as follows:
var myelement = document.getElementById("myelement");
removeClass(myelement, "one three");
//<div id="myelement" class="one two three"></div>
//becomes
//<div id="myelement" class="two"></div>
Code language: HTML, XML (xml)
Enjoy!
Successfully tested in Internet Explorer 6 (WinXP), Firefox 3.5 (WinXP), Firefox 3.6 (OS X 10.6.2), Safari 4 (OS X 10.6.2), Opera 10.1 (OS X 10.6.2), Chrome 5 (OS X 10.6.2)
Comments
Mr Speaker wrote on August 20, 2010 at 6:54 am:
Excellent! if you google around for "removeClass" functions then most of the answers are the only-remove-text-string versions. Thanks!
ttocco wrote on September 16, 2010 at 6:28 am:
Nice addition, thanks for sharing. It also appears that the native 1.2.4 Element.removeClass() does not trim whitespace where multiple, space-delimited classes previously existed. Selectors like div[class=whatever] will fail when the class contains whitespace (class="whatever") because it's not an exact match. It seems to me this should be fixed in core so I reimplemented removeClass() with a call to the .clean() method.
<pre>
Element.implement({
removeClass: function(className){
this.className = this.className.replace(new RegExp('(^|s)' + className + '(?:s|$)'), '$1').clean();
return this;
}
})
</pre>
philip wrote on September 18, 2010 at 1:55 pm:
@ttocco nice, thanks
Excellent! if you google around for “removeClass” functions then most of the answers are the only-remove-text-string versions. Thanks!
Nice addition, thanks for sharing. It also appears that the native 1.2.4 Element.removeClass() does not trim whitespace where multiple, space-delimited classes previously existed. Selectors like div[class=whatever] will fail when the class contains whitespace (class=”whatever”) because it’s not an exact match. It seems to me this should be fixed in core so I reimplemented removeClass() with a call to the .clean() method.
@ttocco nice, thanks