The DOMTokenList and DOMSettableTokenList interfaces (MDN, WHATWG) provide methods for manipulating ordered sets of string tokens represented by space-delimited strings. They are most commonly used in the form of the Element.prototype.classList property, a DOMTokenList which reflects the class
attribute of an associated element.
var div = document.createElement('div'); div.setAttribute('class', 'hello world goodnight moon'); var list = div.classList; console.assert(list.length === 4); console.assert(list[0] === 'hello'); console.assert(list.item(1) === 'world'); console.assert(list.contains('moon') === true); console.assert(list.contains('mars') === false); list.remove('world', 'earth', 'dirt', 'sand'); list.add('hello', 'mars'); list.toggle('goodnight'); console.assert(div.getAttribute('class') === 'hello moon mars');
I'm working on a custom element (HTML5Rocks, W3C Draft) which displays a real-time feed of the activity of specified Stack Overflow users. This list of users is specified in an ids
attribute, and may be updated at any time.
<so-users ids="1114 22656 106224"></so-users>
document.querySelector('so-users').setAttribute('ids', '23354 115866');
Instead of requiring users to manipulate this attribute directly, I would like to have an .ids
property providing a DOMTokenList that they can use instead. Ideally this would be directly associated with the attribute, but an unbound DOMSettableTokenList instance that I have to manually bind would also be fine.
document.querySelector('so-users').ids.add('17174');
Unfortunately, I have been unable to find any way to create a DOMTokenList instance. The definition is not a constructor, and directly creating an object using its prototype results in errors when I call any associated methods:
new DOMTokenList; // TypeError: Illegal constructor new DOMSettableTokenList; // TypeError: Illegal constructor
var list = Object.create(DOMSettableTokenList.prototype, { value: { value: 'hello world' } }); console.assert(list instanceof DOMTokenList); console.assert(list instanceof DOMSettableTokenList); list.item(0); // TypeError: Illegal invocation
function TokenListConstructor() { this.value = 'hello world'; } TokenListConstructor.prototype = DOMSettableTokenList.prototype; var list = new TokenListConstructor; console.assert(list instanceof DOMTokenList); console.assert(list instanceof DOMSettableTokenList); list.add('moon'); // TypeError: Illegal invocation
How can I construct a new DOMTokenList
or DOMSettableTokenList
instance?
You cannot create an DOMTokenList or an DOMSettableTokenList directly. Instead you should use the class attribute to store and retrieve your data and perhaps map an ids attribute of your DOM element to the classList property. var element = document.
A DOMTokenList is a set of space separated tokens. A DOMTokenList can be accessed by index (starts at 0). The length Property returns the number of tokens in a DOMTokenList.
You cannot create an DOMTokenList or an DOMSettableTokenList directly. Instead you should use the class attribute to store and retrieve your data and perhaps map an ids attribute of your DOM element to the classList property.
var element = document.querySelector('so-users'); element.ids = element.classList;
You can use relList according to the documentation but classList is more supported, the only drawback is that you might run into issues if one of your ids matches a class name so set an inline style to hide the element just in case.
For a custom component compatibility should be a concern (classList is present in IE>=10, Firefox 3.6, Chrome 8, Opera 11.5 and Safari 5.1, see http://caniuse.com/#feat=classlist) so if compatibility is in your requirements use the another solution posted below.
If you cannot use clases or classList and/or must use the ids attribute you should implement a custom function according to the spec with the following properties as functions.
This is an example implementation of such functionality.
var TokenList = function (ids) { 'use strict'; var idsArray = [], self = this, parse = function (id, functionName, cb) { var search = id.toString(); if (search.split(' ').length > 1) { throw new Error("Failed to execute '" + functionName + "' on 'TokenList': The token provided ('" + search + "') contains HTML space characters, which are not valid in tokens.');"); } else { cb(search); } }; function triggerAttributeChange() { if (self.tokenChanged && typeof self.tokenChanged === 'function') { self.tokenChanged(idsArray.toString()); } } if (ids && typeof ids === 'string') { idsArray = ids.split(' '); } self.item = function (index) { return idsArray[index]; }; self.contains = function (id) { parse(id, 'contains', function (search) { return idsArray.indexOf(search) !== -1; }); }; self.add = function (id) { parse(id, 'add', function (search) { if (idsArray.indexOf(search) === -1) { idsArray.push(search); } triggerAttributeChange(); }); }; self.remove = function (id) { parse(id, 'remove', function (search) { idsArray = idsArray.filter(function (item) { return item !== id; }); triggerAttributeChange(); }); }; self.toggle = function (id) { parse(id, 'toggle', function (search) { if (!self.contains(search)) { self.add(search); } else { self.remove(search); } }); }; self.tokenChanged = null; self.toString = function () { var tokens = '', i; if (idsArray.length > 0) { for (i = 0; i < idsArray.length; i = i + 1) { tokens = tokens + idsArray[i] + ' '; } tokens = tokens.slice(0, tokens.length - 1); } return tokens; }; };
Set an 'ids' property in your element with a new instance of this function and finally you must bound the targeted attribute to the property listening to changes to the element and updating the property o viceversa. You can do that with a mutation observer.
See firing event on DOM attribute change and https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
var attachTokenList = function (element, prop, initialValues) { 'use strict'; var initValues = initialValues || element.getAttribute(prop), MutationObserver = window.MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver, observer, config, cancelMutation = false; function createTokenList(values) { var tList = new TokenList(values); tList.tokenChanged = function () { element.setAttribute(prop, element[prop].toString()); cancelMutation = true; }; element[prop] = tList; } createTokenList(initValues); observer = new MutationObserver(function (mutation) { var i, mutationrec, newAttr; if (mutation.length > 0 && !cancelMutation) { for (i = 0; i < mutation.length; i = i + 1) { mutationrec = mutation[i]; if (mutationrec.attributeName === prop && element[prop]) { newAttr = element.getAttribute(prop); createTokenList(newAttr); } } } cancelMutation = false; }); config = { attributes: true }; observer.observe(element, config); };
Testing to see if it works
<so-users ids="1234 5678"></so-users> <button onclick="clickButton1()">Add 7890</button> <button onclick="clickButton2()">Set to 3456</button> <button onclick="clickButton3()">Add 9876</button>
Inside a script tag
var elem = document.querySelector('so-users'); attachTokenList(elem, 'ids') function clickButton1 () { elem.ids.add('7890'); } function clickButton2 () { elem.setAttribute('ids', '3456'); } function clickButton3 () { elem.ids.add('9876'); }
Clicking the buttons in sequence set the ids attribute to '3456 9876'
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With