Using DOM elements as keys to javascript map - javascript

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

Related

JavaScript - anyway to remove all base methods and attributes of an Object but keep one?

I'm building some app which scans the DOM for elements and their children. Right now, I can get the data I need with the following line of code:
var bodyObj = document.getElementsByTagName("BODY")[0].children;
The problem, that the bodyObj object is spammed with unnecessary methods and attributes, I only want to keep the children method and clean the rest. Any way of achieving this?
PS
Pure Js.
Body object with only the children property
You might think to try something like:
var bodyObj = document.getElementsByTagName("BODY")[0];
for (var key in bodyObj){
if (bodyObj.hasOwnProperty(key) && key != 'children'){
delete bodyObj[key];
}
}
...like Florian Cabirol suggested in their answer. However, the <body> object's properties are non-enumerable, meaning they won't show up in the loop. To get the non-enumerable properties, you might try (in ES5+):
Object.getOwnPropertyNames(bodyObj);
But it still won't get them. Next, you might think, "I'll just make an array of all possible property/method names that are in HTMLBodyElements, and loop through them, removing them if they exist." However, if you do something like:
delete bodyObj.baseURI;
console.log(bodyObj.baseURI);
You'll notice that it didn't delete it. That's because you can't remove properties from a DOM object. Attempts to redefine them or to delete them will silently fail.
Body object's children's HTMLElements
To get document.getElementsByTagName("BODY")[0].children without any of its properties/methods, you could do:
var bodyObj = document.getElementsByTagName("BODY")[0].children;
bodyObj = Object.keys(bodyObj).map(function(key) { return bodyObj[key]; });
This would convert it from an HTMLCollection to an array, if you're okay with that. You could always wrap it in an object: bodyObj = {children: bodyObj};.
You can do something like this :
for (var key in bodyObj){
if (bodyObj.hasOwnProperty(key) && key != 'children'){
delete bodyObj[key];
}
}

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.

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.

Delete property from object without knowing depth or parent properties

I currently have a nested javascript object of unknown depth. The object is generated purely dynamically, so I don't know the names of the parents of the property I want to delete ( I could rework some stuff to get them If I Have To, but I'm trying to avoid that).
I currently have a function called search_tree(name) that searches through all the properties of the object until it finds a property name : "name" , and then returns that object for adding data at that location of the tree.
However, I now need to delete that object from the tree, and have not yet gotten it to work. I have tried:
obj = search_tree(name);
delete obj;
and that doesn't work, I'm assuming because obj isn't actually the object in the tree, but a reference to it?
delete search_tree(name);
also yielded no results. Can this be done in this way, or will I need to alter the search_tree function to somehow return the heritage as well (or just make a different one)? Thanks
The code from search_tree
function search_tree(element, matchingName){
if(element.name == matchingName){
return element;
}else if (element.children != null){
var result = null;
for(var child in element.children){
result = searchTree(element.children[child], matchingName);
}
return result;
} else {
return null;
}
}
Just realized this function may be a bit unclear without explanation. Each object in the tree has a child object called "children," in which any number of other objects will be stored. I added the extra "children" layer because the objects often have child objects that I do not want to be searched through as part of the tree. Element is the object being searched through
Are you looking to remove the object from the tree? And that's all you're looking to do?
If so, then store the "parent" node of the tree inside of your search.
Write a second function -- maybe prune_tree, where you pass in the parent and the object (or an object with both as properties), and then do a for ... in search of parent. If parent[key] === object, delete parent[key];
You now have a full tree where that particular parent no longer contains that object (or you should).
Given that search_tree should be recursive, give it one more parameter (parent), which you feed it once for every level of depth you hit (each child will have the same parent). Be sure to account for the parent being the root (and thus not having a parent).
When you find the object you want to kill, return { object : objectNode, parent : parentNode };
Put that into your prune function.
The reference to parentNode means that when you delete parentNode.objectNode, it will also be deleted from the tree (because it's just a reference, afterall).
EDIT:
Based on the above:
function prune_tree (parent, child) {
var key = "";
for (key in parent) { if (parent.hasOwnProperty(key) && parent[key] === child) {
delete parent[key];
}
}
function search_tree (name, element, parent) {
var key = "";
if (element.name === name) {
return prune_tree(parent, element);
} else if (!element.children) {
return null;
} else {
parent = element.children;
for (key in children) { if (children.hasOwnProperty(key) {
return search_tree(name, children[key], parent);
}}
}
}
I'm not 100% sure of what you're actually doing when you're recursing (like whether you're depending on a specific return value, or whatever... I don't even know if there are multiple objects which might have that same name, on different branches -- including the root node).
But something like what I've got there should recurse your tree.
It's setting parent to element.children (the place where the children are stored), and then looping through each object in children to call the function over again, passing in parent to the next set. So element is going to be a child element, and parent is going to be the children object which holds it.
If element.name is an exact match to name, then call the separate function prune_tree, and pass it the saved parent element, and the current child element.
Inside of prune_tree, just iterate through the keys of parent, until you find the child element you're looking for. Then delete it off of the parent.
There shouldn't really be any surprises here, and this particular set of functions will likely keep on running until every single node on every single branch has been visited... ...so if you've got more than, say, 2000 nodes, you might want to consider breaking this up into chunks, or else it's going to break the call stack of some browsers.
Assuming you've got only 1000 nodes or less, or you're only targeting browsers with bigger stacks, this should prune everything with the same name.
Again, this all comes down to whether this is your intended outcome, or if you're depending on getting return values, to do something with them, or if you just expect to fire this, pass it the root of a tree, and expect the function to purify the branches.
I'm also not sure whether you want to do something with the pruned objects, or if the point is just to have a tree clean from the tyranny of whatever is named "name".

How do I add objects to the jquery result object?

I'm not sure if I'm doing something wrong here or not, but I need to base the status of several objects off of the status of another. So I'm basically iterating over the parents, then getting their children and iterating over them to get their children etc. However, I don't know how to add jquery objects on to a jquery result object.
My code looks something like this (where bubbleDown is said result object):
while (i < bubbleDown.length) {
var curRow = bubbleDown[i];
child = curRow.getAttribute('class') //etc
parent = curRow.getAttribute('class') //etc
bubbleDown.add($('.' + child + ':not(.' + parent.replace(' ', '.') + ')'));
i++;
}
I've hidden some logic due to it not being applicable to my question. Anyway, it doesn't work, and not because the jquery fails to return objects. Can anyone help?
The simplest approach is that .add() takes a selector already, just use that overload and keep updating the reference to the object to use the one .add() returns:
var newBubbleDown = $(bubbleDown);
while (i < bubbleDown.length) {
var curRow = bubbleDown[i];
child = curRow.getAttribute('class') //etc
parent = curRow.getAttribute('class') //etc
newBubbleDown = newBubbleDown.add('.' + child + ':not(.' + parent.replace(' ', '.') + ')');
i++;
}
//use newBubbleDown
I can't simplify it any further since I'm not sure of your logic outside, e.g. where i comes from, what child and parent are used for, etc. But just call .add(selector), without feeding it a jQuery object and you're all set.
We're using a new object here since .add() returns a reference to a new jQuery object (one you're not using), so each .add() adds elements, creates a jQuery object containing them, then it's thrown away on the next loop. Instead you need to update the reference so it keeps accumulating elements like you want. I tend to change the .add() implementation to act on the current array in most of my projects because it's more useful that way which would make your original code work, for example).
Note: this will only add elements to the newBubbleDown jQuery object, but your :not() use makes me wonder if this will suit your needs, it wouldn't remove any elements in any case, it'll only find the elements matching that selector and add them to the array. If you need to exclude elements already in the array, you'll need .filter().
you have to do this on a jquery-object item:
var curRow = $(bubbleDown[i]);
or you can use the each() method: (while index holds the current item index)
bubbleDown.each(function(index){
var child = $(this).attr('class')...
...
bubbleDown.add(...);
});
use var to init your variables to avoid problems with IE for example.

Categories