Everyone is already familiar with document.getElementById(..). Sometimes though, you don't want to (or can't) retrieve an element by its id. What would be nice was if there was some way to "query" for all elements of a certain CSS class name and then return them all in an array. Well there is no such function, but there's not reason we can't write one!
Read on to learn more.
I'm going to call my function getElementsByClassName(..). In order to write this function, we'll need to leverage another, native ECMAScript function: .getElementsByTagName(..). I trust everyone is already familiar with how that function works. If not, there are plenty of resources on the web that can help you — I'm not going to get into that here.
The getElementsByClassName(..) function will take three parameters:
- _className: (required) A regular expression (or a string) used to match the desired class name(s).
- _startElem: (optional) The element at which you wish to begin the search; default = 'document'.
- _filterTag: (optional) The tag name you wish to limit searches to; default = '*'.
The decision to use a regular expression instead of just a simple string might not be obvious; let me explain.
Since DOM elements can have multiple classes specified (simply separate them by a space), a direct string-to-.className comparison would not be very flexible. Suppose, for example, I want to retrieve all elements that have the className: 'animal' and 'dog'. You might think would be as simple as passing in the string 'animal dog', but that may not work. The element's .className property could very well be 'animal poodle dog', in which case that string comparison would fail, but the element does indeed have the classes 'animal' and 'dog'.
Clearly we need something more robust. Enter regular expressions!
If our getElementsByClassName(..) takes a regular expression, we now have a great deal more flexibility. We can query all elements that have classes 'animal' and 'dog' but not 'poodle' (something that would be darn near impossible with a straight string comparison).
Still, we don't want to force the average person to have to write a regular expression just to use this function, so we'll also allow plain old strings to be passed in, and when that happens, we'll simply generate a stock regex that works for 99% of cases.
The second parameter, _startElem, is to increase efficiency. There is a good chance that you don't want to query the entire document for elements with a particular class name, but that you want to confine your query to some parent element. This optional parameter allows you to do that. If this parameter is not specified, it will default to the entire document.
The third parameter, _filterTag, is also to improve efficiency. You may only be interested in querying a elements of a specific tag name. It would be silly then to query all elements if you're only interested in say, <span> elements. If this parameter is not specified, it will default to all elements ('*').
Now that we have the basic design for this function, we can go ahead and write it.
Here is the full code for getElementsByClassName(..) with the explanation of any tricky code to follow.
{
if (typeof _className === 'string')
{ _className = new RegExp('(^| )' + _className + '( |$)'); }
// default to the document element if no _startElem is specified
_startElem = _startElem || document;
// default to all elements if no _filterTag is specified
_filterTag = _filterTag || '*';
var arr = []; // the array of matched elements that will be returned
var tags; // array of all tags to check for class name matches
/* If the browser supports [DOMElement].all, we'll use that. Otherwise
we'll use .getElementsByTagName(..), which is really the preferred method. */
if (typeof _startElem.all != 'undefined' && _filterTag == '*')
{
tags = _startElem.all;
}
else
{
// the W3C way (but won't work for IE less than 6)
tags = _startElem.getElementsByTagName(_filterTag);
}
// loop through the tags array checking for .className matches
var i, len = tags.length;
for (i = 0; i < len; i++)
{
var elem = tags[i];
if (_className.test(elem.className))
{ arr.push(elem); }
}
return arr;
}
Most of this function is pretty self-explanatory, but two things require explanation.
_startElem = _startElem || document;
// default to all elements if no _filterTag is specified
_filterTag = _filterTag || '*';
The above code ensures default values for _startElem and _filterTag. It does this by leveraging the fact that, in ECMAScript, all variables are variant type, and that ECMAScript uses short-circuit evaluation.
It boils down to this: when assigning a variable using logical OR ( || ), the variable's value will be equal to the value of the first variable that evaluates to true in an if conditional. Since this is going to be the subject of a future blog entry, I'm not going to go into any additional details here. All you need to know for now, is that if either _startElem or _filterTag is unspecified, each will receive its proper default value.
The other piece of code that requires some explanation is:
{
tags = _startElem.all;
}
else
{
tags = _startElem.getElementsByTagName(_filterTag);
}
The really tricky part about this is that it's not obvious why we need to do it.
The problem is, IE (less than 6) supports .getElementsByTagName(..), but it doesn't support passing in the universal selector to that function (the value '*'). Since all modern versions of IE support .getElementsByTagName(..), but they don't all support the universal selector, we have something of a problems here.
We can use [DOMElement].all in place of .getElementsByTagName('*'), for IE, but the fact that .all exists doesn't necessarily tell us we're in IE (Opera supports .all too).
Fortunately, .all is sufficient for what we're trying to do, so it doesn't really matter if we're using IE5, IE6, IE7, or Opera (or even some other browser that supports .all that I don't even know about).
So, if the browser supports .all we'll use that. Otherwise, we'll use the W3C standard's .getElementsByTagName('*') instead.
With all of that sorted out, our tags variable should now be an array, appropriately populated with DOM elements that the rest of the code will filter based on class name.
Everything else in the function (including the filtering loop) should be fairly easy to understand. The only thing left to do now is show a few samples of how to utilize it properly.
<html>
<head>
<title>getElementsByClassName(..) samples</title>
<script type = "text/javascript">
/* include getElementsByClassName(..) function */
function init()
{
var dogHouse = document.getElementById('theHouse');
var animals = getElementsByClassName('animal');
var animalsWithHomes = getElementsByClassName('animal', dogHouse);
var dogs = getElementsByClassName('dog');
var poodles = getElementsByClassName('poodle');
var divDogs = getElementsByClassName('dog', null, 'div');
var cats = getElementsByClassName('cat');
alert('Total animals: ' + animals.length);
alert('Animals with homes: ' + animalsWithHomes.length);
alert('Total Dogs: ' + dogs.length);
alert('Number of Poodles: ' + poodles.length);
alert('DIV Dogs: ' + divDogs.length);
alert('Number of Cats: ' + cats.length);
}
</script>
<style type = "text/css">
#theHouse, #theStreets { border: 1px solid #000; }
</style>
</head>
<body onload = "init()">
The House:
<div id = "theHouse">
<div class = "animal dog poodle">Poodle 1 (div)</div>
<span class = "animal dog chocolateLab">Chocolate Lab 1 (span)</span>
<div class = "animal dog poodle">Poodle 2 (div)</div>
</div>
<br /><br />
The Streets:
<div id = "theStreets">
<div class = "animal dog germanShepherd">Homeless Shepherd (div)</div>
<span class = "animal cat calico">Calico Cat 1 (span)</span>
</div>
</body>
</html>
I don't think this requires much explanation. It's basically just some samples of .getElementsByClassName(..) in action.
One thing you'll note is that I didn't bother to use any regular expressions here. I simply pass in strings and let our function use its stock regex for me. I could have however, passed in a more complex regular expression if I really wanted to.
If you're not too familiar with regular expressions, that's okay, but if you are, then that extra knowledge may go a long way towards helping you get the most out of the .getElementsByClasName(..) function.
Well that wraps up this entry. It was long I know, but it's a super-useful function and a great addition to any ECMAScript library.
Comments welcome.