DailyJS

Let's Make a Framework: Element Attributes Part 2

Alex R. Young

Subscribe

@dailyjs

Facebook

Google+

tutorials frameworks lmaf dom documentation

Let's Make a Framework: Element Attributes Part 2

Posted by Alex R. Young on .
Featured

tutorials frameworks lmaf dom documentation

Let's Make a Framework: Element Attributes Part 2

Posted by Alex R. Young on .

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 = '';

  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
.