44

I'm writing a GreaseMonkey script where I'm iterating through a bunch of elements. For each element, I need a string ID that I can use to reference that element later. The element itself doesn't have an id attribute, and I can't modify the original document to give it one (although I can make DOM changes in my script). I can't store the references in my script because when I need them, the GreaseMonkey script itself will have gone out of scope. Is there some way to get at an "internal" ID that the browser uses, for example? A Firefox-only solution is fine; a cross-browser solution that could be applied in other scenarios would be awesome.

Edit:

  • If the GreaseMonkey script is out of scope, how are you referencing the elements later? They GreaseMonkey script is adding events to DOM objects. I can't store the references in an array or some other similar mechanism because when the event fires, the array will be gone because the GreaseMonkey script will have gone out of scope. So the event needs some way to know about the element reference that the script had when the event was attached. And the element in question is not the one to which it is attached.

  • Can't you just use a custom property on the element? Yes, but the problem is on the lookup. I'd have to resort to iterating through all the elements looking for the one that has that custom property set to the desired id. That would work, sure, but in large documents it could be very time consuming. I'm looking for something where the browser can do the lookup grunt work.

  • Wait, can you or can you not modify the document? I can't modify the source document, but I can make DOM changes in the script. I'll clarify in the question.

  • Can you not use closures? Closuses did turn out to work, although I initially thought they wouldn't. See my later post.

It sounds like the answer to the question: "Is there some internal browser ID I could use?" is "No."

Robert J. Walker
  • 10,027
  • 5
  • 46
  • 65
  • 2
    Very interesting question. I would actually like to know how do various browsers implement this. Perhaps some browsers assign a hidden index to all the elements so they can access them internally without the need of walking through the entire DOM tree. It would make sense. The question is if we can access the index from JavaScript. – Petr Peller Jan 30 '14 at 17:37
  • @AsGoodAsItGets Actually, it's the other way around, as this question was created first. :) – Robert J. Walker Jul 07 '17 at 15:05
  • @AsGoodAsItGets: *shrug* Nobody will force you to; do so if you feel like you ought to. – Robert J. Walker Sep 12 '17 at 18:05
  • In database design, we usually create a unique ID field to lookup the record (row) by a number that is guaranteed never to change, even when we mostly do lookups by another field, for the sake of extendibility. It seems strange that the DOM designers would not put a unique integer into each Node that is created, or at least each Node of type Element. The Node objects are on the other hand just littered with function values that could have been stored in class static or elsewhere out of the actual objects. Just saying. – David Spector Dec 03 '18 at 00:08

13 Answers13

11

The answer is no, there isn't an internal id you can access. Opera and IE (maybe Safari?) support .sourceIndex (which changes if DOM does) but Firefox has nothing of this sort.

You can simulate source-index by generating Xpath to a given node or finding the index of the node from document.getElementsByTagName('*') which will always return elements in source order.

All of this requires a completely static file of course. Changes to DOM will break the lookup.

What I don't understand is how you can loose references to nodes but not to (theoretical) internal id's? Either closures and assignments work or they don't. Or am I missing something?

Borgar
  • 37,817
  • 5
  • 41
  • 42
11

Closure is the way to go. This way you'll have exact reference to the element that even will survive some shuffling of DOM.

Example for those who don't know closures:

var saved_element = findThatDOMNode();

document.body.onclick = function() 
{
   alert(saved_element); // it's still there!
}

If you had to store it in a cookie, then I recommend computing XPath for it (e.g. walk up the DOM counting previous siblings until you find element with an ID and you'll end up with something like [@id=foo]/div[4]/p[2]/a).

XPointer is W3C's solution to that problem.

Kornel
  • 97,764
  • 37
  • 219
  • 309
7

A bit confused by the wording of your question - you say that you "need a string ID that [you] can use to reference that element later, " but that you "can't store the references in [your] script because when [you] need them, the GreaseMonkey script itself will have gone out of scope."

If the script will have gone out of scope, then how are you referencing them later?!

I am going to ignore the fact that I am confused by what you are getting at and tell you that I write Greasemonkey scripts quite often and can modify the DOM elements I access to give them an ID property. This is code you can use to get a pseudo-unique value for temporary use:

var PseudoGuid = new (function() {
    this.empty = "00000000-0000-0000-0000-000000000000";
    this.GetNew = function() {
        var fourChars = function() {
            return (((1 + Math.random()) * 0x10000)|0).toString(16).substring(1).toUpperCase();
        }
        return (fourChars() + fourChars() + "-" + fourChars() + "-" + fourChars() + "-" + fourChars() + "-" + fourChars() + fourChars() + fourChars());
    };
})();

// usage example:
var tempId = PseudoGuid.GetNew();
someDomElement.id = tempId;

That works for me, I just tested it in a Greasemonkey script myself.


UPDATE: Closures are the way to go - personally, as a hard-core JavaScript developer, I don't know how you didn't think of those immediately. :)

myDomElement; // some DOM element we want later reference to

someOtherDomElement.addEventListener("click", function(e) {
   // because of the closure, here we have a reference to myDomElement
   doSomething(myDomElement);
}, false);

Now, myDomElement is one of the elements you apparently, from your description, already have around (since you were thinking of adding an ID to it, or whatever).

Maybe if you post an example of what you are trying to do, it would be easier to help you, assuming this doesn't.

Jason Bunting
  • 58,249
  • 14
  • 102
  • 93
  • 1
    Great minds think alike. Nitpick: DOM id's must start with a A-Z character so this breaks HTML (works though). :-) – Borgar Oct 22 '08 at 17:58
  • True - I tend to forget some of the particulars of the specs since most browsers are so conveniently forgiving. :) I have used this pattern for quite a while in my own JavaScript code without problem, but now that you mention it, I think I will make changes to ensure validity...maybe. :P – Jason Bunting Oct 22 '08 at 18:06
  • Yeah, I would typically use closures in a scenario like this. I'm not sure why I didn't think of them in this case. I chalk it up to too many nights working late. :) – Robert J. Walker Oct 22 '08 at 20:07
  • Don't closures over DOM elements cause browser leaks, at least in IE? – ErikE Aug 30 '12 at 20:59
  • @ErikE - As far as I recall, and it's been longer than I care to admit, the memory leak issue occurs when you have event handlers attached to DOM elements that are removed from the DOM and no longer referenced by any variables. – Jason Bunting Sep 23 '12 at 06:21
  • @JasonBunting Thanks for replying. I thought about it a little and my memory says that it has to be a two-way reference: a closure or any reference of a DOM element from the JavaScript side; that same JavaScript kept instantiated by being bound to a DOM element (don't think it has to be the same one). Since the DOM and JavaScript are handled in two different object managers, they both get stuck waiting for the other to release the reference. Looking back at your update, it seems to meet these criteria. Your anonymous function is bound to an element, and it has a closure over another element. – ErikE Sep 23 '12 at 06:46
  • You could always test it by setting up a few thousand references on page load, then refresh over and over and see if the memory usage of IE grows or not. Also, to address your point specifically, I don't think removing the element from the DOM is a requirement. Certainly, a reference in JavaScript to a DOM element that is removed will keep that DOM element instantiated so long as the script itself is active, but in that case the reference will be released when the script is destroyed, so (late night brain analysis here) there should be no leak. – ErikE Sep 23 '12 at 06:47
5

UPDATE: Closures are indeed the answer. So after fiddling with it some more, I figured out why closures were initially problematic and how to fix it. The tricky thing with a closure is you have to be careful when iterating through the elements not to end up with all of your closures referencing the same element. For example, this doesn't work:

for (var i = 0; i < elements.length; i++) {
    var element = elements[i];
    var button = document.createElement("button");
    button.addEventListener("click", function(ev) {
        // do something with element here
    }, false)
}

But this does:

var buildListener = function(element) {
    return function(ev) {
        // do something with event here
    };
};

for (var i = 0; i < elements.length; i++) {
    var element = elements[i];
    var button = document.createElement("button");
    button.addEventListener("click", buildListener(element), false)
}

Anyway, I decided not to select one answer because the question had two answers: 1) No, there are no internal IDs you can use; 2) you should use closures for this. So I simply upvoted the first people to say whether there were internal IDs or who recommended generating IDs, plus anyone who mentioned closures. Thanks for the help!

Robert J. Walker
  • 10,027
  • 5
  • 46
  • 65
  • 1
    The function-inside-a-loop problem is described well here: http://jslinterrors.com/dont-make-functions-within-a-loop. Thanks for the heads up; that would've eaten up a few of my hours easily. – Seth Jul 28 '14 at 23:28
4

If you can write to the DOM (I'm sure you can). I would solve this like this:

Have a function return or generate an ID:

//(function () {

  var idCounter = new Date().getTime();
  function getId( node ) {
    return (node.id) ? node.id : (node.id = 'tempIdPrefix_' + idCounter++ );
  }

//})();

Use this to get ID's as needed:

var n = document.getElementById('someid');
getId(n);  // returns "someid"

var n = document.getElementsByTagName('div')[1];
getId(n);  // returns "tempIdPrefix_1224697942198"

This way you don't need to worry about what the HTML looks like when the server hands it to you.

Borgar
  • 37,817
  • 5
  • 41
  • 42
3

If you're not modifying the DOM you can get them all by indexed order:

(Prototype example)

myNodes = document.body.descendants()
alert(document.body.descendants()[1].innerHTML)

You could loop through all of the nodes and give them a unique className that you could later select easily.

Diodeus - James MacFarlane
  • 112,730
  • 33
  • 157
  • 176
  • Such unique names, generated by a walk of the DOM, will change whenever the HTML file changes (just deleting one node could change many assigned names or identifiers). Thus a persistent id can apparently only be assigned by the author of the HTML file! A problem not solved by HTML5. – David Spector Dec 03 '18 at 00:12
2

You can set the id attribute to a computed value. There is a function in the prototype library that can do this for you.

http://www.prototypejs.org/api/element/identify

My favorite javascript library is jQuery. Unfortunately jQuery does not have a function like identify. However, you can still set the id attribute to a value that you generate on your own.

http://docs.jquery.com/Attributes/attr#keyfn

Here is a partial snippet from jQuery docs that sets id for divs based on the position in the page:

  $(document).ready(function(){

    $("div").attr("id", function (arr) {
          return "div-id" + arr;
        });
  });
Michael
  • 111
  • 4
2

You can generate a stable, unique identifier for any given node in a DOM with the following function:

function getUniqueKeyForNode (targetNode) {
    const pieces = ['doc'];
    let node = targetNode;

    while (node && node.parentNode) {
        pieces.push(Array.prototype.indexOf.call(node.parentNode.childNodes, node));
        node = node.parentNode
    }

    return pieces.reverse().join('/');
}

This will create identifiers such as doc/0, doc/0/0, doc/0/1, doc/0/1/0, doc/0/1/1 for a structure like this one:

<div>
    <div />
    <div>
        <div />
        <div />
    </div>
</div>

There are also a few optimisations and changes you can make, for example:

  • In the while loop, break when that node has an attribute you know to be unique, for example @id

  • Not reverse() the pieces, currently it is just there to look more like the DOM structure the ID's are generated from

  • Not include the first piece doc if you don't need an identifier for the document node

  • Save the identifier on the node in some way, and reuse that value for child nodes to avoid having to traverse all the way up the tree again.

  • If you're writing these identifiers back to XML, use another concatenation character if the attribute you're writing is restricted.

wybe
  • 615
  • 5
  • 14
1

Use mouse and/or positional properties of the element to generate a unique ID.

0

In javascript, you could attach a custom ID field to the node

if(node.id) {
  node.myId = node.id;
} else {
  node.myId = createId();
}

// store myId

It's a bit of hack, but it'll give each and every node an id you can use. Of course, document.getElementById() won't pay attention to it.

sblundy
  • 60,628
  • 22
  • 121
  • 123
  • Yes, but the problem is on the lookup end later. The only way I'd have to find that element again is to iterate all elements in the document and compare that custom property. Depending on the document size, that could be really slow. – Robert J. Walker Oct 22 '08 at 17:15
0

You can also use pguid (page-unique identifier) for unique identifier generation:

 pguid = b9j.pguid.next() // A unique id (suitable for a DOM element)
                          // is generated
                          // Something like "b9j-pguid-20a9ff-0"
 ...
 pguid = b9j.pguid.next() // Another unique one... "b9j-pguid-20a9ff-1"

 // Build a custom generator
 var sequence = new b9j.pguid.Sequence({ namespace: "frobozz" })
 pguid = sequence.next() "frobozz-c861e1-0"

http://appengine.bravo9.com/b9j/documentation/pguid.html

Robert Krimen
  • 289
  • 1
  • 5
0

OK, there is no ID associated to DOM element automatically. DOM has a hierarchycal structure of elements which is the main information. From this perspective, you can associate data to DOM elements with jQuery or jQLite. It can solve some issues when you have to bind custom data to elements.

kisp
  • 6,402
  • 3
  • 21
  • 19
0

I 'think' I've just solved a problem similar to this. However, I'm using jQuery in a browser DOM environment.

var objA = $("selector to some dom element"); var objB = $("selector to some other dom element");

if( objA[0] === objB[0]) { //GREAT! the two objects point to exactly the same dom node }

Tom Carnell
  • 585
  • 8
  • 14