Let's Make a Framework: hasClass

18 Aug 2011 | By Alex Young | Tags frameworks tutorials lmaf css dom

Let’s Make a Framework is an ongoing series about building a JavaScript framework from the ground up.

These articles are tagged with lmaf. The project we’re creating is called Turing. Documentation is available at turingjs.com.

Using Turing’s DOM module quickly reveals the lack of hasClass. This is a handy method that most frameworks provide to detect if a node has a given class name. We’ve already implemented the class manipulation functionality in Part 60: CSS Classes.

jQuery hasClass

jQuery uses a combination of a regular expression and indexOf to detect if classes are present:

hasClass: function( selector ) {
  var className = " " + selector + " ";
  for ( var i = 0, l = this.length; i < l; i++ ) {
    if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) {
      return true;
    }
  }

  return false;
},

A nice touch here is every element in the current internal stack is searched for the class name. If a result is found the method will return straight away.

Zepto

I also looked at how Zepto does this, because Zepto is usually very concise and easy to follow. The hasClass implementation is in zepto.js. Zepto includes caching, but the real work is a regular expression:

function classRE(name){
  return name in classCache ?
    classCache[name] : (classCache[name] = new RegExp('(^|\\s)' + name + '(\\s|$)'));
}

I actually thought of just using new RegExp('\\b' + className + '\\b').test(element.className), but looking at Zepto made me realise that expression might not match all valid class names.

Regular Expression Matching

The reason using \b isn’t sufficient is because CSS identifiers can contain hyphen and underscore (from CSS Level 2: Characters and case), while \b would only match [a-zA-Z0-9_] (which are considered word characters by the regular expression engine). Zepto’s regular expression uses \s and the line ending characters.

Tests

To make sure Turing’s implementation worked along these lines, I wrote some tests:

'test hasClass': function() {
  assert.ok(turing('#attr-test').hasClass('example'));
  assert.ok(turing('#attr-test').hasClass('example-2'));
  assert.ok(turing('#attr-test').hasClass('example_3'));
  assert.ok(!turing('#attr-test').hasClass('example_'));
},

'test nested hasClass': function() {
  assert.ok(turing('#nested-hasClass-test div').hasClass('find-me'));
  assert.ok(!turing('#nested-hasClass-test div').hasClass('aaa'));
},

I wrote this feature test first — this approach has always worked well for me when researching and writing this series. Notice that I’ve also included a “nested test” which is intended to test Turing’s chained hasClass behaviour which searches through every matching element just like jQuery.

Implementation

I used a similar regular expression to Zepto and added some sanity checking for node type and inputs:

  /**
   * Detects if a class is present.
   *
   * @param {Object} element A DOM element
   * @param {String} className The class name
   * @return {Boolean}
   */
  dom.hasClass = function(element, className) {
    if (!className || typeof className !== 'string') return false;
    if (element.nodeType !== nodeTypes.ELEMENT_NODE) return false;
    if (element.className && element.className.length) {
      return new RegExp('(^|\\s)' + className + '($|\\s)').test(element.className);
    } else {
      return false;
    }
  };

To make this work through a chain, it just needs to be put in a loop:

/**
 * Detects if a class is present.
 *
 * @param {String} className A class name
 * @returns {Boolean}
 */
hasClass: function(className) {
  for (var i = 0; i < this.length; i++) {
    if (dom.hasClass(this[i], className)) {
      return true;
    }
  }
  return false;
},

This passes the tests in IE6, Firefox, Chrome, Safari, etc.

To get the version of Turing in this tutorial, checkout commit b169bd4.

References


blog comments powered by Disqus