Let's Make a Framework: Element Attributes Part 2

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

Welcome to part 62 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.

Last week I discussed retrieving element attributes, which should just involve calling getAttribute but ends up with added complexity to support every browser. This week I’ll look at building a cross browser implementation.

Tests

This test should cover the basics for getting attributes:

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

  assert.equal(turing.dom.attr(element, 'id'), 'attr-test');
  assert.equal(turing.dom.attr(element, 'class'), 'example');
  assert.equal(turing.dom.attr(element, 'tabindex'), 9);
  assert.equal(turing.dom.attr(link, 'href'), '/example');
}

It includes some examples that I know will break in IE: class will have to be mapped to className, href won’t return what we expect, and tabindex requires capitalisation.

Basic Implementation

This will work in WebKit and Firefox:

/**
 * Get or set attributes.
 *
 * @param {Object} element A DOM element
 * @param {String|Object} options The attribute name or a list of attributes to set
 */
dom.attr = function(element, options) {
  if (typeof options === 'string') {
    return element.getAttribute(options);
  }
};

It fails in IE:

AssertionError In "==": Expected: example Found: null

Which is fine, we just need to account for the way class is accessed:

var propertyFix = {
  'class': 'className'
};

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

  return element.getAttribute(name);
}

This is only good for browsers that need it (IE). Let’s use Turing’s capability detection:

turing.addDetectionTest('getAttribute', function() {
  var div = document.createElement('div');
  div.innerHTML = '<a href="/example"></a>';

  if (div.childNodes[0].getAttribute('href') === '/example') {
    return true;
  }

  // Helps IE release memory associated with the div
  div = null;
  return false;
});

This checks the unusual case of the extra parameter required to correct what href represents in IE. Then the main dom.attr method needs to be updated to take this into account:

/**
 * Get or set attributes.
 *
 * @param {Object} element A DOM element
 * @param {String|Object} options The attribute name or a list of attributes to set
 */
dom.attr = function(element, options) {
  if (typeof options === 'string') {
    return turing.detect('getAttribute') ?
      element.getAttribute(options) : getAttribute(element, options);
  }
};

Extra Parameters

As I mentioned last week, IE requires an extra parameter to make href return the value we expect. I’ve created a list of attributes that require this extra parameter:

getAttributeParamFix = {
  width: true,
  height: true,
  src: true,
  href: true
};

Then if the capability test fails, this will run:

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

  if (getAttributeParamFix[name]) {
    return element.getAttribute(name, 2);
  }

  return element.getAttribute(name);
}

The part that reads element.getAttribute(name, 2) makes IE behave like other browsers. Using an object that contains a list of true values is a convenient way of doing this, I instinctively thought of ['width', 'height', 'src', 'href'].indexOf(name) !== -1) but then I remembered that IE doesn’t have indexOf.

Forms

Dealing with button attributes can have strange results in IE. In particular, accessing value can return the inner HTML. I’m still testing this to figure out what versions it affects, and what other attributes are involved, but so far I’ve done this:

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

These are the tests I’m using:

assert.equal(turing.dom.attr(input, 'value'), 'example');
assert.equal(turing.dom.attr(input, 'name'), 'e');
assert.equal(turing.dom.attr(button, 'name'), 'b');
assert.equal(turing.dom.attr(button, 'value'), 'example');

Chains

Supporting the chained API is a case of a quick stub:

/**
 * Get or set attributes.
 *
 * @param {String|Object} options The attribute name or a list of attributes to set
 * @returns {String} The attribute value
 */
attr: function(options) {
  if (this.elements.length > 0) {
    return dom.attr(this[0], options);
  }
}

This is one of those interesting cases where it makes sense to access the first element in the current chain’s stack, and return a value rather than this.

Conclusion

More than anything else, getting attributes is a task in browser support. There are some subtleties to fully supporting IE (and I’m still not convinced this is 100%).

The last commit for this week was commit b16c498.


blog comments powered by Disqus