Direct reference from DOM object to my object / data? - javascript

// 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.

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");

save element nodechilds in the same type as element in javascript

var old_element=document.getElementById('dhtml_menu-1895');
var old_element_child=old_element.children;
console.log(old_element_child);
output a nodelist object
i would like to have old_element_child in the same type as old_element to be able to do
var new_element = old_element.cloneNode(true);
var new_element_child=new_element.children;
old_element.parentNode.replaceChild(new_element, old_element);
old_element.replaceChild(old_element_child, new_element_child);
The children property returns a nodelist of all the children of a given element (even if it's only one, which seems to be the case). What you want is an specific element, for that you could either use
var old_element_child = old_element.children[n];
n being the index of the desired child inside the parent (0 based). So if it's the first child it would be
var old_element_child = old_element.children[0];
You can try copying your object in this way:
The Object.assign() method is used to copy the values of all enumerable own properties from one or more source objects to a target object. It will return the target object.
var old_element=document.getElementById('dhtml_menu-1895');
var copy = Object.assign({}, old_element);
console.log(copy); // here is your object copyed

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

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.

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)];
}
}
}

Appending elements to DOM in a loop structure

Once the page has been loaded, I would like to append an additional element for each existing elements on the page.
I tried something like this:
var divs=document.getElementsByTagName('div');
for(i=0;i<divs.length;i++){
newDiv=document.createElement('div');
divs[i].appendChild(newDiv);
}
Just a warning this will actually freezes the browser because the divs variable is dynamic and divs.length just gets larger and larger each time the loop goes.
Is there a way to determine the number of tags when the DOM is normally loaded for the first time and have a chance to work with the elements statically.
I can't there of another solution so far.
Thanks so much.
Dennis!
The problem is that DOM collections are live, and when the underlying document structure is changed, it will be reflected automatically on the collection, that's why when the length property is accessed it will contain a new length, a common approach is to cache the length before starting the loop:
var divs=document.getElementsByTagName('div');
for(var i = 0, len = divs.length;i<len;i++){
var newDiv = document.createElement('div');
divs[i].appendChild(newDiv);
}
Also notice that you should declare all your variables with the var statement, otherwise it might become global.
Edit: In this case, since you are appending child nodes of the same tagName, the collection will be modified, and the indexes will no longer match, after the first iteration, the index 1 will refer to the newDiv object from the previous iteration, as #Casey recommends it will be safer to convert the collection to a plain array before traversing it.
I use the following function:
function toArray(obj) {
var array = [];
// iterate backwards ensuring that length is an UInt32
for (var i = obj.length >>> 0; i--;) {
array[i] = obj[i];
}
return array;
}
//...
var divs = toArray(document.getElementsByTagName('div'));
//...
Like you said, the divs variable is dynamic, so you have to convert it into an array (which is static) before you use it.
var nodeList = document.getElementsByTagName('div');
var divs = [];
for (var i = 0; i < nodeList.length; i++)
divs.push(nodeList[i]);
// loop again and append the other divs
Another (more elegant) way to do this is:
var divs = Array.prototype.slice.call(document.getElementsByTagName('div'));
But alas, this method does not work in IE.
Using jQuery, this is pretty straight forward. You can get a reference to all the existing divs or any other element on the page and then append a new element very easily without needing to create an explicit loop. Hope this help.
$('div').each(function(){
var newDiv = document.createElement('div');
$(this).append(newDiv);
});
document.getElementsByTagName() does NOT return a plain array, but an instance of HtmlCollection, which behaves like an array, but in fact presents some kind of view to all elements with the given element name in the document.
So, whenever you insert something into the DOM, the length property of divs will be updated too - of course.
So, besides other answers here, this behaviour should make sense now ;-)

Categories