Selectors API and classList property

Introduction

The selectors API and classList property were added to the DOM to select html elements and modify css classes assigned to them in a more convinient way. If you used special libraries or frameworks to do these things, you can consider now using only pure JavaScript.

Selecting html elements

Probably every JavaScript developer knows how to select an element or group of elements using jQuery. It’s very simple because jQuery uses standard css selectors which allow exact selecting using complex CSS queries. Before HTML5 there was no adequate approach. Now there is a broad support across all modern browsers, it's called the Selectors API. Before we get familiar with this API let’s take a quick look on ordinary DOM methods for selecting elements:

  • document.getElementById(elementId) - returns a DOM element which is an instance of the HTMLElement whose id attribute is equal to passed value or null if there is no such element.
  • context.getElementsByClassName(className) - returns a list of elements as the HTMLCollection with specified css class assinged. If there is no such element the returned list has length equal to 0.
  • context.getElementsByTagName(tagName) - selects all elements with the specified name element , returns HTMLCollection.
  • document.getElementsByName(paramName) - selects all elements with the specified value of the "name" attribute  , returns NodeList

The context can be any html element, not necessary a document object. However, selecting is restricted only to that element's descendants. You can access elements from the HTMLCollection and NodeList using the item(index) method or square braces like in an array. You can also transform these array-like objects to an array using one of the following snippets:

var arrayOfElements = Array.prototype.slice.apply(HTMLCollection|NodeList)
// or
var arrayOfElements = [].slice.apply(HTMLCollection|NodeList)

The Selectors API adds two new methods which take as a parameter a css selector and can be invoked on any html element:

  • context.querySelector(cssSelector) - returns a html element HTMLElement or null if there is no element matches or selector contains pseudo-element. Parameter can be a single css selector or comma separated list of css selectors. If there is more than one matching elements the first which appears in html document is returned. 
  • context.querySelectorAll(cssSelector) - returns NodeList with all html elements matching passed parameter. Empty list is returned if selector contains pseudo-element.

Important difference is that querySelectorAll returns static list, whereas getElementsByClassName, getElementsByTagName and getElementsByName - live/dynamic. So don't be suprised after some DOM modyfications dynamic list returned by these methods will also change. It's also worth noting that in some cases ordinary methods and simple selectors (id, class name) are much faster.

Adding and removing classes from element

Before HTML5 there was only one element's property which allowed to change CSS classes asigned to it. It's called className and you can assign to it class name or comma separated list of classes.

htmlElement.className = "className" - adds a class to an element.
htmlElement.className = "className1 className2 ..." - adds comma separated list of classes to an element.
htmlElement.className = "" - removes all classes from an element.

Quite new html DOM classList property is an attractive alternative to className because it allows to modify CSS classes assigned to an element in much more convenient way. The property classList is an instance of DOMTokenList and has the following methods:

  • add(className) - adds a class to an element's list of classes. Specific class is not added if it already exists in the list of classes.
  • remove(className) - removes a class from an element's list of classes.
  • toggle(className [, remove]) - removes a class if it exists in an element's list of classes or add class if it doesn't. Returns true if class is added, false if is removed. Second optional parameter if is set to true forces adding class if false forces removing class.
  • contains(className) - returns true if an element's list of classes contains a specific class, false otherwise.
  • item(index) - returns class name from element's list of classes at specific index.

Any changes made in one property (classList or className) are reflected in the other one so you can use both or one of them if you prefer.

Example 1: Selecting methods - differences

In the following code we create hundred list elements with text input and then execute runTest function which gauges execution time of different selecting methods. There are also two buttons: one to run test once again and another to remove list element.

var ul = document.createElement('ul'),
    el, tagList1, tagList2, classList1, classList2, nameList1, nameList2,
    elNum = 100,
    startBtn = document.getElementById('start-btn'),
    modifyBtn = document.getElementById('modify-list-btn'),
    output = document.getElementById('output');
	
startBtn.addEventListener('click',function(evt){
    runTest();
});
	
modifyBtn.addEventListener('click',function(evt){
    if(ul.firstChild) {
        ul.removeChild(ul.firstChild);
    }
    printListsLength();
});
	
for(i=0;i<elNum;++i) {
    el = document.createElement('li');
    el.setAttribute('id','li-'+i);
    el.className = 'element c-'+i;
    el.innerHTML = 'List element No. <input name="index" type="text" value="'+i+'" />';
    ul.appendChild(el);
}
document.body.appendChild(ul);
	
function runTest() {
    console.time('getElementById');
    for(i=0;i<elNum;++i) {
        el = document.getElementById('li-'+i);
    }
    console.timeEnd('getElementById');
		
    console.time('querySelector_id');
    for(i=0;i<elNum;++i) {
        el = document.querySelector('#li-'+i);
    }
    console.timeEnd('querySelector_id');
		
    console.time('getElementsByClassName');
    classList1 = document.getElementsByClassName('element');
    console.timeEnd('getElementsByClassName');
		
    console.time('querySelectorAll_class');
    classList2 = document.querySelectorAll('.element');
    console.timeEnd('querySelectorAll_class');
		
    console.time('getElementsByTagName');
    tagList1 = document.getElementsByTagName('li');
    console.timeEnd('getElementsByTagName');
		
    console.time('querySelectorAll_tag');
    tagList2 = document.querySelectorAll('li');
    console.timeEnd('querySelectorAll_tag');

    console.time('getElementsByName');
    nameList1 = document.getElementsByName('index');
    console.timeEnd('getElementsByName');
				
    console.time('querySelectorAll_name');
    nameList2 = document.querySelectorAll('input[name=index]');
    console.timeEnd('querySelectorAll_name');
		
    printListsLength();			
}
	
function printListsLength() {
    console.log('classList1.length: '+ classList1.length + ', classList2.length: ' + classList2.length);
    console.log('tagList1.length: '+ tagList1.length + ', tagList2.length: ' + tagList2.length);
    console.log('nameList1.length: '+ nameList1.length + ', nameList2.length: ' + nameList2.length);
}
	
runTest();

You can compare times traced in areas marked as 1 and 2. Generally all getElements* methods are faster. However, querySelector with id query performs better in subsequent selects and then is as efficient as getElementById. In the last three lines there are traced length properties of  the list with selected elements after removing one of them. We can see values of length differ. Lists returned by getElements* methods reflect change that was made - there are 99 elements now, not 100. Lists returned by querySelectorAll have still length euqal to 100. As mentioned earlier querySelectorAll return static list which isn't updated when DOM is updated, get* methods dynamic list which reflects DOM updates.

Example 2: Testing CSS queries

In the following html code we include some elements which we try to select using Selectors API.

<h1>What's new in HTML5</h1>
<blockquote>
<h3>Table of Contents</h3>
<ol>
  <li><a href="introduction.html">Introduction: Five Things You Should Know About <abbr>HTML5</abbr></a></li>
  <li><a href="past.html">A Quite Biased History of <abbr>HTML5</abbr></a></li>
  <li><a href="detect.html">Detecting <abbr>HTML5</abbr> Features: It&rsquo;s Elementary, My Dear Watson</a></li>
  <li><a href="http://diveintohtml5.info/">...see more on diveintohtml5.info</a></li>
  <li class="app"><a href="everything.html">The All-In-One Almost-Alphabetical Guide to Detecting Everything</a></li>
  <li class="app"><a href="peeks-pokes-and-pointers.html"><abbr>HTML5</abbr> Peeks, Pokes and Pointers</a></li>
</ol>
</blockquote>

Let's check out Selectors API and examine relation between selectors sequence and what is returned:

console.log(document.querySelector('h1, h3')); // h1
console.log(document.querySelector('h3, h1')); // h1
console.log(document.querySelector('ol, li, h3, h1')); // h1
console.log(document.querySelector('li')); // li with introduction

console.log(document.querySelectorAll('h1, h3')); // h1, h3
console.log(document.querySelectorAll('h3, h1')); // h1, h3
console.log(document.querySelectorAll('ol, li, h3, h1')); // h1, h3, ol#whats-new-list, li, li, li, li, li.app, li.app
console.log(document.querySelectorAll('li')); // li, li, li, li, li.app, li.app

We can see that the only criteria which determines returned html elements and their order is the order of element's occurence in the html document not in the selector parameter.

What about selectors which give the same matches, how mamy elements is returned? In the last line we use two selectors that match the same element, as we can see returned list has only one element.

console.log(document.querySelectorAll('li:first-of-type')); // li with introduction
console.log(document.querySelectorAll('li:first-child')); // li with introduction
console.log(document.querySelectorAll('li:first-child, li:first-of-type').length); // 1

In the following example we use some more complex selectors. We want to fix all links which point to local html files by adding the right base url. First we use selector to find anchor elements which attribute begins with "http". Next we select all anchors which attribute href ends with ".html". We transform returned NodeList to an array and use the forEach method to iterate throughout the array changing href attribute of every selected element.

var baseURL = document.querySelector('a[href^="http"]').getAttribute('href');
[].slice.call(document.querySelectorAll('a[href$=".html"]')).forEach(function(el){
	el.setAttribute('href',baseURL+el.getAttribute('href'));
});

Example 3: Circle preloader

In the following example we use all of described techniques to select and apply styles to the elements of the simple CSS preloader. All files you can find in the project attached to this article.

Circle Preloader

CSS class preloader defines size, shape and color of the preloader background. Class circle creates circle by setting border radius equal to 50%. Classes animation and animation-paused are responsible for starting and pausing animation called circleanimation which is defined using @-webkit-keyframes selector.

* {
    font-family: Lucida Sans, Arial, Helvetica, sans-serif;
}

body {
    margin: 0;
    padding: 0;
}

#main {
    width: 100vw;
    height: 100vh;
    background: url('../images/white_wall.png');	
}

.preloader {
    position: absolute;
    width: 200px;
    height: 200px;
    top:50vh;
    left:50vw;
    margin:-100px 0 0 -100px;	
    background: #000;
    border-radius: 20px;
    box-shadow: 5px 5px 5px 0px rgba(0, 0, 0, 0.5);
    cursor: pointer;	
}

.circle {
    position: absolute;
    border-radius: 50%;
    width: 24px;
    height: 24px;
    background: #FFF;
}
.animation {
    -webkit-animation-name: circleanimation;
    -webkit-animation-duration: 1.8s;
    -webkit-animation-iteration-count: infinite;	
}

.animation-paused {
    -webkit-animation-play-state: paused;
}

@-webkit-keyframes circleanimation {
  0% {
  	-webkit-animation-timing-function: ease-in;
	opacity: 0;
  }
  30% {
  	-webkit-animation-timing-function: ease-in;
	opacity: 1;
  }
  80% {
  	-webkit-animation-timing-function: ease-in;
	opacity: 0;
  }
  100% {
  	-webkit-animation-timing-function: ease-in;
	opacity: 0;
  } 
}

At the first step we create main container for the preloader - empty div element. Then we assign to it preloader class using classList property and its add method. In the preloader class there are some styles related to the size, background color, border and shadow. In the next step, in for loop we create nine elements which are small, animating circles. To each of them we assign at once two classes: circle and animation using className property. Circle class defines shape and color, animation as name suggests animation (fading in and out). To each circle, we also assign specific styles related to it's position and animation delay.

var main = document.querySelector('#main'),
	preloader = document.createElement('div'),
	dotsNumber = 9,
	a = 0,
	da = Math.PI*2/dotsNumber,
	r = 50,
	animTime = 1.8,
	delay = animTime/dotsNumber;
	
for(var i=0;i<dotsNumber;++i) {
	var circle = document.createElement('div');
	circle.className = 'circle animation';
	circle.style['-webkit-animation-delay'] = (i*delay)+'s';
	preloader.appendChild(circle);
	circle.style.top = (88-Math.cos(a)*r) + 'px';
	circle.style.left = (88-Math.sin(a)*r) + 'px';
	a += da;		
}
	
preloader.addEventListener('touchstart', function(evt){
	[].slice.apply(document.querySelectorAll('div.circle')).forEach(function(el,i){
		el.classList.toggle('animation-paused');
		if(i===0) console.log([].slice.apply(el.classList));
		// ["circle", "animation"] or ["circle", "animation", "animation-paused"]
	});
});
	
preloader.classList.add('preloader');	
main.appendChild(preloader);

On touch event, we want to stop all animations. We add listener to preloader and in event handler we get all circle elements using querySelectorAll with div.circle parameter. Next we convert array-like list with selected elements to an array. Later, using the forEach method we toggle animation-paused class on each of them. The animation-paused class just pauses circle opacity animation. You can observe here how that class is toggled, tracing each element's classList property. You can also notice that in the classList property there are other classes assigned using className property.

Summary

Well known jQuery library uses internally CSS selector engine called Sizzle. Sizzle for simple queries (tag name, id, class name) uses appropriate getElements* methods and for the rest querySelectorAll what as we can see in the first example is completely legitimate. Both described features can turn out very useful when you don't want to use any additional library in your project.

File attachments: