Let's Make a Framework: Element Attributes Part 3

19 May 2011 | By Alex Young | Tags frameworks tutorials lmaf documentation dom

Welcome to part 63 of Let’s Make a Framework, the ongoing series about building a JavaScript framework.

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

For the past two weeks I’ve been looking at accessing element attributes. This week I’ll demonstrate how to write element attributes.

API Design

I like the idea of using the same method for both getting and setting element attributes:

// Get
turing('selector').attr('name');

// Set
turing('selector').attr('name', 'value');

It’s fairly easy to remember this and easy to implement.

setAttribute

Remember element.getAttribute? Well, there’s also setAttribute. It was introduced in DOM Level 1, so browser support isn’t terrible.

Most of the cross-browser issues relating to getAttribute should apply to writing attributes because they were almost all related to correcting IE’s interpretation of the attribute’s capitalisation. That means we can use setAttribute in a very similar way.

Null Values

According to MDC:

Even though getAttribute() returns null for missing attributes, you should use removeAttribute() instead of elt.setAttribute(attr, null) to remove the attribute.

This is present in jQuery’s attributes.js implementation:

if ( value === null ) {
  jQuery.removeAttr( elem, name );
  return undefined;

Note that we must distinguish between null and undefined — because we’re using attr to read and write values, undefined is an absence of a value which implies reading the attribute.

That means we need to use element.removeAttribute when null is passed. However, this method has poor browser support. When I tried jQuery’s implementation in IE6 I seemed to get an empty string instead of undefined, so I think a true cross-browser remove attribute method might be out of scope here.

To my knowledge, this is the closest I can get in IE:

function removeAttribute(element, name) {
  if (element.nodeType !== nodeTypes.ELEMENT_NODE) return;
  if (propertyFix[name]) name = propertyFix[name];
  setAttribute(element, name, '');
  element.removeAttributeNode(element.getAttributeNode(name));
}

Tests

I wrote this test to help me implement the core functionality for writing attributes:

'test setting attributes': function() {
  var element = turing.dom.get('#attr-write')[0],
      link = turing.dom.get('#attr-write a')[0],
      input = turing.dom.get('#attr-write form input')[0],
      button = turing.dom.get('#attr-write form button')[0];

  turing.dom.attr(element, 'id', 'attr-test2');
  assert.equal(turing.dom.attr(element, 'id'), 'attr-test2');

  turing.dom.attr(element, 'class', 'example2');
  assert.equal(turing.dom.attr(element, 'class'), 'example2');
  
  turing.dom.attr(element, 'tabindex', 1);
  assert.equal(turing.dom.attr(element, 'tabindex'), 1);

  turing.dom.attr(link, 'href', '/somewhere');
  assert.equal(turing.dom.attr(link, 'href'), '/somewhere');

  // Forms
  turing.dom.attr(input, 'value', 'changed-value');
  assert.equal(turing.dom.attr(input, 'value'), 'changed-value');

  turing.dom.attr(input, 'name', 'changed-name');
  assert.equal(turing.dom.attr(input, 'name'), 'changed-name');
  
  turing.dom.attr(button, 'name', 'changed-button-name');
  assert.equal(turing.dom.attr(button, 'name'), 'changed-button-name');
  
  turing.dom.attr(button, 'value', 'changed-button-value');
  assert.equal(turing.dom.attr(button, 'value'), 'changed-button-value');
}

Implementation

Building on last week’s code, I added a check to see if the attribute value is null or undefined:

/**
 * Get or set attributes.
 *
 * @param {Object} element A DOM element
 * @param {String} attribute The attribute name
 * @param {String} value The attribute value
 */
dom.attr = function(element, attribute, value) {
  if (typeof value === 'undefined') {
    return turing.detect('getAttribute') ?
      element.getAttribute(attribute) : getAttribute(element, attribute);
  } else {
    if (value === null) {
      return dom.removeAttr(element, attribute);
    } else {
      return turing.detect('getAttribute') ?
        element.setAttribute(attribute, value) : setAttribute(element, attribute, value);
    }
  }
};

I reused the getAttribute capability test for writing attributes. I started off with a simple attribute setter for IE:

function setAttribute(element, name, value) {
  if (propertyFix[name]) {
    name = propertyFix[name];
  }

  return element.setAttribute(name, value);
}

But the button test failed so I had to add similar code from getAttribute:

if (name === 'value' && element.nodeName === 'BUTTON') {
  return element.getAttributeNode(name).nodeValue = value;
}

Conclusion

Getting and setting attributes is extremely similar, and it just goes to show how frustratingly similar browser implementations have been. The strange case of IE’s removeAttribute behaviour still baffles me, but if I find a good solution I’ll write an update.

This week’s code is in commit c5625f8.


blog comments powered by Disqus