Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Constructing a DOMTokenList/DOMSettableTokenList instance

Tags:

javascript

dom

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?

like image 746
Jeremy Avatar asked Mar 20 '15 17:03

Jeremy


People also ask

How do you make 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.

What is a DOMTokenList?

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.


1 Answers

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.

  • item()
  • contains()
  • add()
  • remove()
  • toggle()

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'

like image 125
devconcept Avatar answered Sep 22 '22 23:09

devconcept