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

2

8

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?

Cyanotype answered 22/11, 2011 at 0:16 Comment(2)
I think it greatly depends on what you are doing with the nodes.Cimbalom
@Cimbalom My plugin's needs are actually relatively simple--I want a fast way to enumerate them all. Clients call $(element).myPlugIn() on some number of elements over the course of the run to give those elements some "magic"...and as part of the process it is important to be able to enumerate all the nodes this "magic" has been applied to. But I feel uncomfortable just holding these in a global list, in case the client of the plugin is modifying the document, and possibly removing some nodes without explicitly calling the plugin to tell it about that...Cyanotype
C
5

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.

Cimbalom answered 25/11, 2011 at 16:45 Comment(7)
Looking into jQuery UI is a good idea. So you are referring to the handling of $.ui.ddmanager.droppables? If so, where is that hook to the remove() made? github.com/jquery/jquery-ui/blob/master/ui/…Cyanotype
Also I notice in the droppables source there's a selector behind an each() that says find(":data(droppable)").not(".ui-draggable-dragging"). Does this suggest you can select based on nodes having a given $.data() field? That's really what I want, but I imagine that's as slow as looking by class if not slower: github.com/jquery/jquery-ui/blob/master/ui/…Cyanotype
It is made in the widget code very top, github.com/jquery/jquery-ui/blob/master/ui/jquery.ui.widget.jsCimbalom
@HostileFork The find is not so slow because the scope is limited to the drop target and children. Using classes would be much faster, that data selector is actually custom to UI, it's defined in core. A custom selector like that will be MUCH slower.Cimbalom
Custom selector, ah. Well no one is having much more to say as of a close bounty deadline. Taking your point about jQuery UI as probably the canon for jQuery users (what higher authority?). But I'm curious about the good/bad/ugly of their technique...does this destroy "walk the tree" or only catch a removal of the specific root? What kind of guarantee are they getting with their method?Cyanotype
I'm not sure what you mean when you say 'walk the tree', but I will say, I think a best practice would be to, 1. provide a way to remove the plugin from the object and 2. Just check before using it. There isn't really anything else you can, or should worry about doing. That being said, I think in MOST cases, an internal list should not be necessary.Cimbalom
I mean to say: the remove() is a hook which is dispatched to a specific droppable that is being removed. In the jQuery UI model, if I remove a node of which a droppable is a child--as opposed to a remove called directly on the node corresponding to the droppable--will this notification get called? (I know I could install jQuery UI and find out, but I'm asking more as a way of point toward this theory of what is needed to maintain a "correct" internal list in the face of DOM churn...if this is ever addressed in JavaScript at all...)Cyanotype
P
1

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.

Professionalism answered 2/12, 2011 at 1:7 Comment(5)
That's useful stuff, thanks--esp for pointing out the NodeList distinction! While I'm certainly aware that jQuery is a layer above JavaScript, I generally have taken it on faith that this layer is worthwhile. My goal for some time was to remain blissfully ignorant of the mire of trying to do GUI application development in a cross-browser fashion. But I've apparently failed at that (or changed goals), so I'll review this and try to pick an acceptable solution.Cyanotype
Awarding you bounty and +1 for the insights, but to Andrew for the answer in the context of the jQuery universe...Cyanotype
I NodeList is NOT the answer to this question though. The OP needs a list of Nodes that is populated by his plugin only. If you tie it to a class or some other selector that returns a NodeList, the plugin will lose control of the list. i.e. If a user has a list of divs with the plugins class, but only calls the plugin on half the list. The plugin will erroneously work on all the divs.Cimbalom
Conversely, you also don't want items to be removed from the list if they are still in he dom. i.e a user changing a nodes class to style it differently will inadvertently remove it from the plugin. The plugin must retain control of the list, that is why an array must be used.Cimbalom
That e. g. a set of specific class attribute value components defines which elements are considered by the plugin is exactly my point. What would be different with an Array-like object is only that you will have to determine that set of elements repeatedly, while the NodeList would be updated automatically. It is not trivial to find out which elements have vanished from an Array-like object, and rather expensive to find out which have vanished from the document tree, while with a NodeList you do not have to bother checking in the first place. Please consider the original question.Professionalism

© 2022 - 2024 — McMap. All rights reserved.