Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to track lists of nodes in a Jquery plugin (if clients may remove those DOM nodes)

I'm writing a jQuery plugin that "points at" a certain number of nodes in the DOM. Yet if I try something like having my plugin hold references to a set of nodes, I worry about them going "stale".

(While I realize that JavaScript is garbage collected and won't crash, I'd like to be able to keep my lists up to date, and not hold on to things that should be GC'd.)

The first thing that occurred to me was that there might be some sort of hook. But there does not seem to be a standard that looks trustworthy:

With JQuery, is it possible to have a function run when a DOM element calls .remove()?

Callback on the removal of an element from the DOM tree?

jQuery remove() callback?

This made me wonder about maintaining lists of nodes by putting a class attribute on them. That way, their membership in the list would travel with them. But as one might fear, this can be pathologically slow to enumerate, and is why you are supposed to formulate your query as tags before classes.

In addition to potential performance concerns, I wonder if it's considered poor form for a plugin to poke classes onto DOM nodes for this kind of purpose, which is not related to styling. (One of the better things about .data() is that it's relatively out-of-band, and with the exception of this list issue that's what I'm using.)

This seems like a common enough problem to have been addressed by other plugins. I'm tempted to use the class solution for it's "correctness" properties, even though it's slower. But is there a faster and more canonical way that gives the best of both worlds?

like image 239
HostileFork says dont trust SE Avatar asked Nov 22 '11 00:11

HostileFork says dont trust SE


2 Answers

I would trust in how jQuery UI does it. Specifically Droppables, they maintain a list internally of jQuery objects that they iterate over when something hovers over it. They manage the list in 2 ways,

  1. Add a destroy handler that is fired when jQuery.remove() is run. (They hook into jQuerys remove functions to do this) This won't handle plain javascript removes though so,
  2. Just check if it exists before doing anything.

They don't remove it, I assume, because its possible for it to be removed from the DOM then put back in.

like image 133
Andrew Avatar answered Sep 22 '22 10:09

Andrew


Sorry to break this to you, but "trustworthy jQuery-based code" is a contradiction in terms. FWIW:

jQuery, like several other currently popular scripting frameworks, creates an Array-like object to hold the query result, not an object implementing the NodeList interface of W3C DOM Level 2+ Core (NodeList for short). A NodeList is live; Array-like objects, which are native ECMAScript objects, not host objects, are (usually) not.

If you do not use such frameworks for this, you will not have this problem, as all (quasi-)standard DOM methods (like getElementsByClassName()) and properties (like document.forms) return/yield a NodeList (sometimes even an HTMLCollection).

DOM Level 2 Events specified mutation event types, but as of DOM Level 3 Events (Working Draft) they are deprecated, and any hooks on that are unreliable now and probably not even interoperable.

So you should use (quasi-)standard methods for this, not a jQuery result and not mutation events.

However, if you still want to go the jQuery(-ish) way (with all its other non-apparent shortcomings), you should look at a Node's parentNode property. Nodes that are not a Document node but still are in a document tree have a non-null parentNode property value (whereas null is to be understood as the W3C DOM Level 2+ Core Specification's language-independent null, so you should look for all values that type-convert to true in an ECMAScript implementation). Accordingly, all other Nodes have a parentNode property value of null, so something that type-converts to false in an ECMAScript implementation.

Assuming that myNodes refers to an Array instance which elements are Nodes, and myNode refers to one such node object, the following should work:

var i = 0;

/* … */

var myNode = myNodes[i];
if (!myNode.parentNode)
{
  myNodes.splice(i, 1);
}

You could then iterate over myNodes occasionally, say in a function called through window.setInterval(), and remove the "stale" nodes from it. It is certainly not as nice as with a NodeList, but it works even if nodes are removed without using jQuery, and even if mutation event types are not supported by the runtime environment.

Unfortunately, jQuery() does not return a reference to an Array instance. So you will have to make one first, like so:

var jqObj = jQuery("…");

/* or jQuery.makeArray(jqObj) */
var a = Array.prototype.slice.call(jqObj, 0);

(We are using the fact here that the object referred to by jqObj does have a length property.)

I cannot say what would be good style for a jQuery plugin, as for reasons that should be obvious by now I try to avoid using jQuery in the first place.

You should also learn to differentiate between the programming language*s*, like JavaScript, and the (usually language-independent) APIs that can be used with them, like the DOM.

HTH.

like image 21
PointedEars Avatar answered Sep 23 '22 10:09

PointedEars