Can the HTML data attribute hold a reference to a DOM element? - javascript

Is it possible with the HTML data- attributes to hold a reference to another DOM element? For example, I could do this with jQuery:
var domel1 = document.getElementById("#mydiv")
var domel2 = document.getElementById("#mydiv2")
$(domEl1).attr('data-domel', domel2)
Then later on, with jQuery I would do:
var domel1 = document.getElementById("#mydiv")
var domel2 = $(domel2).data('domel')
$(domel2).html("blahblahblah")
This might seem like a trivial example because I could just reference domel2 with the same id like I did at first, but there are cases where this could be useful for representing relationships between <div>s.

Yes and no. You cannot store a reference to a DOM element in a data- attribute. However, you can associated a reference to a DOM element to another element using jQuery .data(), which are already using:
$someElement.data('name', someOtherElement);
From the jQuery documentation:
The .data() method allows us to attach data of any type to DOM
elements in a way that is safe from circular references and therefore
from memory leaks.
Note that using .data() to set data will add it to the data store but not add it as a data- attribute in the DOM. However, using .data() to read data will check the data store as well as the data- attribute (if one exists and there's no data store value with the given key).

Not directly. data-* attributes are just attributes, so you can only store a string in them.
But, of course, you can store the id or class of your target element, in order to retrieve it later.
Or you could also store a reference to the element in a property, since properties can have any value.

Not legal, since attributes should be text strings. But since you're using jQuery you could use the .data() method instead.

jQuery .data() basically does everything for you.
But if you cannot use jQuery, or have to implement something more case-specific, you can hold indexes to a global object that holds the actual data.
This way you can support any type of data you need, references, objects, functions (even binded functions).
Here's a vanilla implementation of data, though I'm not sure what are your limitations - you will probably want to change some bits of the code below.
Note that elements in this code are identified by using their id.
// Set data with: elem.data('key',anything)
// Get data with: elem.data('key')
// Remove data (kind of) with: elem.data('key',undefined)
// This will generate random id on element if id is missing
Node.prototype.force_id = function() {
return this.id || (this.id = ('' + Math.random()).replace('0.', 'id-'));
}
// Our own data implementation
window.DATAOFDOM = {}; // I like naming globals as ALLCAPS
Node.prototype.data = function(k, v) {
if (arguments.length == 1) {
// getter
if (window.DATAOFDOM[this.id]) {
return window.DATAOFDOM[this.id][k]; // returns undefined when k isn't there
}
// else: implicitly returns undefined when there's no data for this element
} else {
// setter
this.force_id();
if (!window.DATAOFDOM[this.id])
window.DATAOFDOM[this.id] = {};
return window.DATAOFDOM[this.id][k] = v;
}
}
https://jsfiddle.net/oriadam/63zn9qtd/

This is not an answer because no element can store DOM element as an attribute value. Here is a small polyfill to do the same
If I have the same requirement I would follow this approach,
var DataDOM = (function(){
function DataDOM(){}
var elements = {}, counter = 0;
DataDOM.prototype.set = function(ele){
elements['ele' + counter] = ele;
counter += 1;
return ele + (counter - 1);
}
DataDOM.prototype.get = function(eleRef){
return elements[eleRef];
}
return DataDOM;
})();
Use like below
var dDOM = new DataDOM();
For example if if I want to set DOM reference to a element data attribute
var div = document.getElementById('someId');
var attr = dDOM.set(div);
Then set attr as data to some element
then while retrieving use below method to get it back
var referedElement = dDOM.get(someElement.attr('data'));
Because there is no direct way to store elements as Data AFAIK.

Related

Getting jQuery.data functionality without jQuery

You can quite easily set a data attribute of any element with jquery using $('#elid').data('key', value). You can do the same using document.querySelector('#elid').setAttribute('data-key', value)
However, jQuery gives you a special ability that querySelector doesn't - the ability to add attributes of an arbitrary type (including functions, and I think promises, which is what I need).
So if you were to do $('#elid').data('key', function(){console.log('yes')}) with jQuery, and then $('#elid').data('key')(), it would log 'yes' to the console -- we can just assign a function to the element as a data attribute and run it whenever.
But we can't do the same with 'setAttribute' -- when we do it, it apparently just assigns a stringified form of the function to the data attribute, rather than the actual function.
I've provided example code here:
https://jsfiddle.net/8e1wyL41/
So how can I apply data to elements with plain javascript, just like jQuery, including the ability to have arbitrary functions or javascript objects as data attribute values?
jQuery#data() uses an internal object to keep track of data values. It does not update the element to have new or changed data-* attributes when setting data values. When retrieving a data value, if the internal object does not have a set value it will attempt to get it from the data-* attributes.
A overly simplified way of doing this without jQuery would be to just use an object and store your data on that
var element = document.querySelector("div");
element.customData = {};
//get data example, check if customData has a value first, if not use dataset
var someData = element.customData["somedata"] || element.dataset["somedata"];
//set
element.customData["somedata"] = function(){};
If you don't want to contaminate the element with arbitrary properties you could use a WeakMap, pending on browser support, to associate a data object with the element. This also allows for using a single object to maintain other element data objects as well. The key to the data object is the element object itself. And the data object will get deleted from the map automatically once the element is garbage collected
var dataMap = new WeakMap();
var element = document.querySelector('div');
var elementData = dataMap.get(element);
if(!elementData){
dataMap.set(element, elementData = {});
}
//get data example, check if data object has a value first, if not use dataset
var someData = elementData["somedata"] || element.dataset["somedata"];
//set
elementData["somedata"] = function(){};
.dataset sets or gets a DOMString of HTML data-*, though you can use Function() to call the function stored as string at HTMLElement.dataset
document.documentElement.dataset.fn = function fn(...args) {console.log(args)};
new Function("return " + document.documentElement.dataset.fn)()("yes");

Direct reference from DOM object to my object / data?

// my object
function myObject(id) {
this.id = id;
}
var parent = document.getElementById('parent');
var myObjects = [];
for(i = 0; i < someCount; i++) {
var id = someData[i].id; // unique value
var oobject = new myObject(id);
myObjects.push(oobject);
// div element
var div = document.createElement('div');
div.id = id; // note how div id is same as id property - these two are in "pairs" a.k.a the data from that object is meant for that div
div.className = 'my-div';
parent.appendChild(div);
}
What are my options? How to make a direct reference from DOM object to my object?
This doesn't even have to be a separate object, I just need to store some data that I could access directly / O(1) via DOM element.
Data is changed dynamically and often which rules out localStorage in my opinion
There's too much data for data- attribute and I'm worried that it causes some issues
Works in all modern browsers (also stealth mode) and mobiles - not a big plus for localStorage
I need to be able to get/set my data when DOM element is clicked without any nasty array iterations or searching for the correct object using only some property's value (like id in the example above).
I looked into adding my own properties/data to DOM object itself
but all I could find was that this is a very bad idea. These posts were old (between 2008 and 2011), how are things in 2016? There is extremely little information about this. This would be the easiest and I would only need 1 line: myDiv.addEventListener('click', function(e){ var id = e.currentTarget.myObject.id; }
I could also assign DOM object to my own object but as far as I know and I couldn't find any information stating otherwise: there's no way to directly get an object by its property a.k.a move up in the object's hierarchy.
One way to do this in O(1) and without altering DOM nodes is to use an object literal as a Map (hash) linking the node to an object.
You are already setting an id attribute on your DOM elements, so you can use each node's id as the key to connect to the appropriate myObject:
var map = {}; // create the map. map[domNodeID] = appropriate myObject
for(i = 0; i < someCount; i++) {
var id = someData[i].id;
var oobject = new myObject(id);
// div element
var div = document.createElement('div');
div.id = id;
div.className = 'my-div';
parent.appendChild(div);
// link node and object through the map
map[id] = oobject;
}
Then you can just reference the object using the map:
myDiv.addEventListener('click', function(e) {
var oobject = map[this.id]; // O(1) lookup
// use your object
}
There is also a built-in object for this purpose called Map, which might prove better than using an object literal but it depends on you needed browser support. Map does allow using objects as keys as well as strings so you can technically link a node to an object directly, without the need for an id:
map.set(node, oobject)
Additionally, instead of using direct access like map[key] = value, perhaps you can add methods to your custom map that use the same names as Map methods, such as map.set(key, value) to ensure an easy switch to a Map later on.

localStorage methods

function Todo(id, task, who, dueDate) {
this.id = id;
this.task = task;
this.who = who;
this.dueDate = dueDate;
this.done = false;
}
function updateDone(e) {
var spanClicked = e.target;
var id = spanClicked.parentElement.id;
var done = spanClicked.parentElement.done;
spanClicked.innerHTML = " ✔ ";
spanClicked.setAttribute("class", "done");
var key = "todos" + done;
localStorage.setItem(key, "true");
console.log(key);
}
In the second part of the last function I'm trying to change the value of the done property to "true" when the object I'm targeting is clicked for localStorage. What happens instead though is when I check localStorage in the web console the value of that object doesn't change and instead another object is added to the localStorage called "todosundefined: 'true'". I don't understand why when I click an object something is added to the localStorage as a separate, undefined object instead of changing the value of the object I clicked on. Any suggestions to modify it so that I change the done value of the object I click on for localStorage?
As mentioned in the comments above, there's no done property on DOM objects. I'm not entirely sure what you're trying to do, but in order to get the contents of the class attribute of a DOM object, you should use the className property. E.g:
var done = spanClicked.parentElement.className;
Then again, that doesn't appear to make any sense based on the code you posted. I'm assuming you want to use local storage to keep track which items on a list of todo's are done. In that case, wouldn't it be better to just store an array of id's of all items that are done (or the reverse)? You can't store an array directly in local storage, but you can use, for example, JSON.stringify() to get a string representation of the array that you can then store.

Using DOM elements as keys to javascript map

I am trying to associate some "private" data with DOM elements. Rather than adding that data to the DOM element itself (I'd like to avoid changing the DOM element), I have a separate data object that I want to use as a map.
Rather than:
document.GetElementById('someElementId').privateData = {};
I want to do
internalPrivateDataMap[document.GetElementById('someElementId')].privateData = {};
Not all the elements have an id field, and some are created dynamically, so I can't use the id as the key.
This works fine for most elements, but for "a" elements, the key being used seems to be the href of the element, I think because the DOM defines a toString() function for a elements.
The result of this is that if I have two "a" elements with the same href, they are sharing privateData, which I don't want.
My current workaround is to generate an internal uniqueID I can use as a key, but that requires me to modify the DOM element, which I am trying to avoid.
As you noticed, this doesn't work reliably and I know no way to make it work without either giving every element a (generated) ID or at least assign a unique ID to a new custom element field; DOM nodes simply don't have the necessary properties to work as keys in a map.
So you really have these solutions left:
Assign each element a generated ID unless it already has one
Assign a unique ID to a new private field. That way, you can keep the memory impact per DOM node small and still keep your private data in a different place. Don't forget that you need to clean the private data somehow when the DOM elements are deleted.
Use something like jQuery which has element.data() to read and put private data into a DOM element
Use your own element.privateData = {}; Note that you still need cleanup for event handlers which keep references to the element or you will have unexpected memory leaks.
For anyone who's ok with an inefficient solution, you could create a custom map class that leverages node equality. Here's the basic idea, extend as needed:
// Map that has dom elements as keys. Note: O(N) lookup, insertion and deletion
class ElementMap {
constructor() {
this.pairs = [];
}
set(element, value) {
const pair = this.pairs.find(p => p.element === element);
if (pair) {
pair.value = value;
} else {
this.pairs.push({ element, value });
}
}
get(element) {
return this.pairs.find(p => p.element === element)?.value || null;
}
delete(element) {
const idx = this.pairs.findIndex(p => p.element === element);
if (idx >= 0) {
this.pairs = [...this.pairs.slice(0, idx), ...this.pairs.slice(idx + 1)];
}
}
}

call a function with a specific index value of nodelist

Is it possible to call a function on specific index value of nodelist which is storing div like following :
var txtElem = txtdiv.getElementsByTagName("div");
the thing i want is that i am storing list of divisions in txtElem nodelist now i want to call a function on click event of the 3rd div stored in nodelist. The divisions are created dynamically and they don't have any id so they are not accessible by id.
from what you asked, it seems like this will do:
function toPseudoArray(nodeList) {
var ar = [];
for(var i in nodeList)
if(nodeList[i].nextSibling) // or for that case any other way to find if this is an element
ar.push(nodeList[i]);
return ar;
}
Pass your nodeList to this function, use what it returns as an array that contains your elements, and only your elements.
By the way, you could directly call function on a specific element simply using my_fab_function(txtElem[0]); -- of course, until you don't exceed the count.
The question is quite unclear. Seeing the jQuery tag, these come to my mind:
A way to call a jQuery function on a specified index using .eq():
var n = 1; //the index you need
$(txtElem).eq(n).css('color', 'red');
Simple Javascript to get the DOM element:
var n = 1; //the index you need
var elem = txtElem[n]; //elem will hold the DOM element
//call simple DOM methods on it:
var s = elem.innerHTML;
//you can also call jQuery functions on it:
$(elem).css('color', 'red');
By the way txtElem is not an object, it is a NodeList, an "array-like object".

Categories