Let's say for you example you have this array
//Defined at the top of your class
myArray = [];
//Then somewhere else in the code
myArray["unique_id"] = {example: true, example2: false, etc..};
//somewhere else
delete myArray["unique_id"];
Could it be possible to have something like this:
//when adding
myArray["unique_id"] = {example: true, example2: false, etc..};
myArray.trigger("array:update");
//when deleting
delete myArray["unique_id"];
myArray.trigger("array:delete");
//and in a different file or somewhere else down the path
myArray.on("array:update", function(){
//do stuff
});
//and in a different file or somewhere else down the path
myArray.on("array:delete", function(){
//do stuff
});
I like the custom event system and I was wondering if thats something possible. Didn't find examples on the documentation of this specific application.
Basically attaching custom events to array/objects and not necessarily dom elements.
This is possible using a Proxy, but you'll have to make the object emulate an EventEmitter first:
//Defined at the top of your class
let myObject = {};
//Emulate EventEmitter
myObject._events = {};
myObject.emit = function emit(event, ...args) {
(this._events[event] || []).forEach(callback => {
callback.apply(this, args);
});
};
myObject.on = function on(event, callback) {
if (!this._events[event]) {
this._events[event] = [];
}
this._events[event].push(callback);
};
//Define Proxy
let myProxy = new Proxy(myObject, {
set(target, name, value) {
target.emit('update', name, value);
return (target[name] = value);
},
deleteProperty(target, name) {
target.emit('delete', name);
return (delete target[name]);
}
});
//and in a different file or somewhere else down the path
myObject.on('update', function(name, value) {
console.log('update', this, name, value);
});
//and in a different file or somewhere else down the path
myObject.on('delete', function(name) {
console.log('delete', this, name);
});
//Then somewhere else in the code
myProxy["unique_id"] = {
example: true,
example2: false
};
//somewhere else
delete myProxy["unique_id"];
Related
I'm looking into a way to change a native JS function body, while making it not possible to see that it has been changed. Let's take an example with document.hasFocus():
document.hasFocus = ()=>true;
This method works well to spoof focus, but it can be easily detected that it was modified:
document.hasFocus.toString() // -> "()=>true"
Is there any way, in which I can modify such a function while making it impossible to see it has been tampered with?
You can overwrite toString method in Function prototype, and do something like that:
// https://stackoverflow.com/questions/1833588/javascript-clone-a-function
Function.prototype.clone = function() {
var that = this;
var temp = function temporary() {
return that.apply(this, arguments);
};
for (var key in this) {
if (this.hasOwnProperty(key)) {
temp[key] = this[key];
}
}
return temp;
};
Function.prototype.__oldToString = Function.prototype.toString.clone();
function __toStringHooked() {
if ((this.name == "")||(this.name == "hasFocus")) // on Firefox, hasFocus doesn't have any name
{
return eval+"" // this matches regexp
} else {
return this.__oldToString(); // we're returning default value
}
}
Function.prototype.toString = __toStringHooked
document.hasFocus = () => true
The code above is from Th3B0r3dD3v3l0p3r's GitHub repo, you can check it if you want: https://github.com/Th3B0r3dD3v3l0p3r/focus-spoofer/
I'm writing a simple function in JavaScript:
function waitThenRun(objectToWaitFor, callback) {
// logic to periodically check for objectToWaitFor to become something
// then call the callback
}
And I intend to use it as:
waitThenRun(someObjectThatWillBeDefinedLater, function() {
// Doing something with someObjectThatWillBeDefinedLater
});
Yet I get this error:
Uncaught ReferenceError: someObjectThatWillBeDefinedLater is not
defined
How do you handle this?
In simpler terms, I want to run a piece of code only when a global JSON variable is defined, and I can't determine when and where it gets defined. Sometimes it gets defined after my code, sometimes before my code, sometimes a second later, sometimes 5 seconds later. It's non-deterministic.
You can dispatch an event on the window when you define your JSON Object and add an event listener for that event on the window. This way, you will not need to use a setInterval to continuously check if a global variable is defined.
function defineJSON(){
window.JSONObj = {x: "something"};
triggerEvent("JSONDefined", window, {JSONObjName: "JSONObj"});//fire a custom event on the window when you define the JSON Object
}
function triggerEvent(type, obj, data){
var ev;
if (document.createEvent) {
ev = document.createEvent("HTMLEvents");
ev.initEvent(type, true, true);
} else {
ev = document.createEventObject();
ev.eventType = type;
}
ev.eventName = type;
if(data){
for(var key in data){
if(data.hasOwnProperty(key)){
ev[key] = data[key];
}
}
}
if (document.createEvent) {
obj.dispatchEvent(ev);
} else {
obj.fireEvent("on" + ev.eventType, ev);//for < IE8
}
}
window.addEventListener("JSONDefined", function(ev){
//this event handler will be called when the JSON Object is defined
console.log("JSON defined:", window[ev.JSONObjName]);
});
setTimeout(function(){
defineJSON();
}, 2000);//define JSON Object after 2 seconds (just for this example)
If it's a global variable, you can pass the variable name as a string. Since global variables become properties of the window object, the function can then check whether window[objectToWaitFor] is defined.
function waitThenRun(objectToWaitFor, callback) {
var interval = setInterval(function() {
if (window[objectToWaitFor] !== undefined) {
clearInterval(interval);
callback();
}
}
}
waitThenRun("someObjectThatWillBeDefinedLater", function() {
// do something with someObjectThatWillBeDefinedLater
});
All global variables are attached to the window object so can pass the variable name to your function as a string and check if it exists on the window and then run your callback if it does. Snippet:
setTimeout(function() {
window.someObjectThatWillBeDefinedLater = {hello: 'world'};
}, 1000);
function waitThenRun(globalObj, callback) {
let intervalId = setInterval(function() {
if (window[globalObj] !== undefined) {
callback(window[globalObj]);
clearInterval(intervalId);
}
}, 200);
}
waitThenRun('someObjectThatWillBeDefinedLater', console.log.bind(console));
you can do this by using
var checkExist = setInterval(function() {
if (JsonObject) {
//execute your method here
console.log("Exists!");
clearInterval(checkExist);
}
}, 100); // check every 100ms
You can try to use metaprogramming like JS proxies to intercept the set method of an objects property. Something like this:
function tracePropAccess(obj, propKeys) {
const propKeySet = new Set(propKeys);
return new Proxy(obj, {
get(target, propKey, receiver) {
if (propKeySet.has(propKey)) {
console.log('GET '+propKey);
}
return Reflect.get(target, propKey, receiver);
},
set(target, propKey, value, receiver) {
if (propKeySet.has(propKey)) {
console.log('SET '+propKey+'='+value);
}
return Reflect.set(target, propKey, value, receiver);
},
});
}
the snippet was taken from here
In your example, when the set property gets called you can check if the value meets your expected value and do as you wish with it.
If you create an object with functions that return this, you can create a chain of functions that were called previously and create a cascade of function calls:
var i = {
foo: function () {
// Something here...
return this;
},
bar: function () {
// Something here...
return this;
}
};
i.foo().bar().foo()
But what if an error occurs in bar and I don't want to call foo in that case? How do I break the cascade?
If it is at all possible, I would like to avoid try/catch statements.
Ok, so one straightforward thing is that if you want to handle the case without try/catch you have to put if condition in your functions and obviously you have to return something, so that you can execute further functions on that context instead of exception. So try creating all functionality in a object, and allow execution of your function logic only if someone extends. and on your failure return the base, otherwise return current object. In this was you can avoid creating objects every time.
Example:
Lets Consider You have BaseService where all functionalities are defined, and putting a Layer over it to just extend further, so you can go for this pattern:
foo: function() {
if(<function foo does not belongs to this>) {
.......
.......
if(<on logical failure>) {
return this.__proto__;
}
.......
.......
}
return this;
}
Here is a working snippet:
function BaseService() {
var dataBucket = [13, 50, 45, 57, 95];
this.foo = function() {
if (Object.values(this).indexOf(arguments.callee) === -1) {
console.log('processing foo');
}
return this;
}
this.bar = function() {
if (Object.values(this).indexOf(arguments.callee) === -1) {
console.log('processing bar');
}
return this;
}
this.processValIfExists = function(val) {
if (Object.values(this).indexOf(arguments.callee) === -1) {
console.log('processing processValIfExists');
if (dataBucket.indexOf(val) > -1) {
//process the value further
} else {
return this.__proto__;
}
}
return this;
}
};
//define the extended service, you can add further
//functionalities here. eg: createing the dataBucket here from args
function WrapperService() {};
WrapperService.prototype = new BaseService(); // just extend from any service and use
var svc = new WrapperService();
console.log('----------Executes All-----------');
svc.foo().bar().processValIfExists(95).foo().bar();
console.log('----------Executes upto processValIfExists-----------');
svc.foo().bar().processValIfExists(100).foo().bar();
Note that its just a different approach that came to my mind, and to check for the current invoking method I tried to make the code generic instead of checking if its instance of WrapperService in a BaseService function in order to avoid code coupling and can be extend from any other Service as well.
One could return a new empty instance of i, then on error return an empty obect:
class Chainable {
constructor(data){
this.data = data;
}
foo (foo) {
//if an error occured
if(foo == 10) return new Chainable();
// Something here...
return new Chainable(this.data + foo);
},
bar () {
//if were in an error, fail silently
if(!this.data) return this;
// Something here...
return this;
}
}
(new Chainable(1))
.foo(5).bar().bar().data // 6
(new Chainable(1))
.foo(10).bar().bar().data //undefined
Maybe not what you wanted, but Promises are great for chaining and for error handling:
var i = {
count: 0,
foo: function () {
// Something here...
this.count++;
console.log('foo');
return this.count < 4 ?
Promise.resolve(this) :
Promise.reject(new Error('Count too high!'));
},
bar: function () {
// Something here...
this.count++;
console.log('bar');
return this.count < 4 ?
Promise.resolve(this) :
Promise.reject(new Error('Count too high! Count is ' + this.count));
}
};
i.foo() // first
.then(i => i.bar()) // second
.then(i => i.foo()) // third
.then(i => i.bar()) // too much!
.then(i => i.whatIsThis()) // will not run
.then(i => i.whatIsThis()) // will not run
.catch(error => console.log(error.message));
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.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'
You can get an instance of DOMTokenList with this function:
function newDOMTokenList(initialTokens) {
const tmp = document.createElement(`div`);
const classList = tmp.classList;
if (initialTokens) {
initialTokens.forEach(token => {
classList.add(token);
});
}
return classList;
}
We can 'steal' the DOMTokenList from a div, since it does not affect the current document until you choose to insert the element (for example by using insertAdjacentElement) and it will be garbage collected since we do not keep any references to the variable tmp.
Then you can use your list:
var list = newDOMTokenList(['a', 'b']);
list.add('c');
list.contains('d'); // false
list.contains('b'); // true
list.item(1) // 'b'
list instanceof DOMTokenList // true
// etc...
// render it to a string
var soUsers = document.querySelector('so-users');
soUsers.setAttribute('ids', list.toString());
You can even add a MutationObserver to the tmp element and get callbacks whenever the classList changes:
function newDOMTokenList(initialTokens, changed) {
const tmp = document.createElement('div');
const classList = tmp.classList;
if (initialTokens) {
initialTokens.forEach(token => {
classList.add(token);
});
}
if (changed) {
const observer = new MutationObserver((mutationList, observer) => {
for (const mutation of mutationList) {
if (mutation.attributeName === 'class') {
changed();
}
}
});
observer.observe(tmp, {attributes: true});
}
return classList;
}
This, however, will cause the tmp div to never be garbage collected, since the MutationObserver needs to keep a reference to it.
Utilizing Custom Elements - Adding JS properties and methods initialization approach , HTMLElement.dataset
, try
var XFooProto = Object.create(HTMLElement.prototype);
// 1. Give x-foo a foo() method.
XFooProto.contains = function(id) {
var data = JSON.parse(this.dataset.ids);
return data.some(function(_id) {
return id == _id
})
};
XFooProto.add = function(id) {
var data = JSON.parse(this.dataset.ids);
if (!this.contains(id)) {
data.push(id);
};
return data
};
XFooProto.remove = function(id) {
var data = JSON.parse(this.dataset.ids);
if (this.contains(id)) {
for (var _id in data) {
if (data[_id] === id) {
data.splice(_id, 1)
}
};
};
return data
};
XFooProto.ids = function() {
return this.dataset.ids
};
// 2. Define a property read-only "bar".
// Object.defineProperty(XFooProto, "ids", {value: this});
// 3. Register x-foo's definition.
var XFoo = document.registerElement('x-foo', {prototype: XFooProto});
// 4. Instantiate an x-foo.
var xfoo = document.createElement('x-foo');
xfoo.dataset.ids = '["23354", "115866"]';
// 5. Add it to the page.
document.body.appendChild(xfoo);
console.log(xfoo.add("123")); // `["23354", "115866", "123"]`
console.log(xfoo.remove("123")); // `["23354", "115866"]`
console.log(xfoo.contains("123")); // `false`
console.log(xfoo.contains("23354")); // `true`
console.log(xfoo.ids()); // `["23354", "115866"]` , type : `String`
var pre = document.getElementsByTagName("pre")[0]
pre.innerText = JSON.stringify(JSON.parse(xfoo.dataset.ids), null, 4);
<pre></pre>
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.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'
You can get an instance of DOMTokenList with this function:
function newDOMTokenList(initialTokens) {
const tmp = document.createElement(`div`);
const classList = tmp.classList;
if (initialTokens) {
initialTokens.forEach(token => {
classList.add(token);
});
}
return classList;
}
We can 'steal' the DOMTokenList from a div, since it does not affect the current document until you choose to insert the element (for example by using insertAdjacentElement) and it will be garbage collected since we do not keep any references to the variable tmp.
Then you can use your list:
var list = newDOMTokenList(['a', 'b']);
list.add('c');
list.contains('d'); // false
list.contains('b'); // true
list.item(1) // 'b'
list instanceof DOMTokenList // true
// etc...
// render it to a string
var soUsers = document.querySelector('so-users');
soUsers.setAttribute('ids', list.toString());
You can even add a MutationObserver to the tmp element and get callbacks whenever the classList changes:
function newDOMTokenList(initialTokens, changed) {
const tmp = document.createElement('div');
const classList = tmp.classList;
if (initialTokens) {
initialTokens.forEach(token => {
classList.add(token);
});
}
if (changed) {
const observer = new MutationObserver((mutationList, observer) => {
for (const mutation of mutationList) {
if (mutation.attributeName === 'class') {
changed();
}
}
});
observer.observe(tmp, {attributes: true});
}
return classList;
}
This, however, will cause the tmp div to never be garbage collected, since the MutationObserver needs to keep a reference to it.
Utilizing Custom Elements - Adding JS properties and methods initialization approach , HTMLElement.dataset
, try
var XFooProto = Object.create(HTMLElement.prototype);
// 1. Give x-foo a foo() method.
XFooProto.contains = function(id) {
var data = JSON.parse(this.dataset.ids);
return data.some(function(_id) {
return id == _id
})
};
XFooProto.add = function(id) {
var data = JSON.parse(this.dataset.ids);
if (!this.contains(id)) {
data.push(id);
};
return data
};
XFooProto.remove = function(id) {
var data = JSON.parse(this.dataset.ids);
if (this.contains(id)) {
for (var _id in data) {
if (data[_id] === id) {
data.splice(_id, 1)
}
};
};
return data
};
XFooProto.ids = function() {
return this.dataset.ids
};
// 2. Define a property read-only "bar".
// Object.defineProperty(XFooProto, "ids", {value: this});
// 3. Register x-foo's definition.
var XFoo = document.registerElement('x-foo', {prototype: XFooProto});
// 4. Instantiate an x-foo.
var xfoo = document.createElement('x-foo');
xfoo.dataset.ids = '["23354", "115866"]';
// 5. Add it to the page.
document.body.appendChild(xfoo);
console.log(xfoo.add("123")); // `["23354", "115866", "123"]`
console.log(xfoo.remove("123")); // `["23354", "115866"]`
console.log(xfoo.contains("123")); // `false`
console.log(xfoo.contains("23354")); // `true`
console.log(xfoo.ids()); // `["23354", "115866"]` , type : `String`
var pre = document.getElementsByTagName("pre")[0]
pre.innerText = JSON.stringify(JSON.parse(xfoo.dataset.ids), null, 4);
<pre></pre>