Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In d3.js, how can I check if an element has been removed without doing a new selection?

I am using a d3 timer to animate an element, and I want the timer to stop when the element has been removed. What is a simple way of doing that?

Here's a JS fiddle to illustrate my issue. What should I replace this line with?

  if (rect.empty()) {

I realize I can get it to work by changing it into this:

  if (d3.select("rect").empty()) {

However, doing a new D3 selection based on element names or classes is a problem if I have a lot of rect elements or reuse the same classes a lot. Is it possible to simply refresh an existing D3 selection to see if it has become empty?

like image 575
phraseologist Avatar asked Nov 09 '22 20:11

phraseologist


1 Answers

There are two DOM features available to accomplish what you are looking for, which will require no D3 at all. They will both work for you, but will vary in complexity and flexibility.

1. Use a live HTMLCollection.

Both, the Document as well as the Element interface provide similar methods for retrieving elements based on specified criteria:

  • Document.getElementsByClassName() and Element.getElementsByClassName()

  • Document.getElementsByTagName() and Element.getElementsByTagName()

  • Document.getElementsByClassNameNS() and Element.getElementsByClassNameNS()

All these methods will return a live HTMLCollection, which means the collection of elements will be kept up-to-date even after its first retrieval. You can check the existence of an element by querying the collection using HTMLCollection.item() or .namedItem(), or, if the collection contains just one element, peek at .length.

var svg = document.getElementById("s");

// This is a live HTMLCollection.
var rects = document.getElementsByTagName("rect");

console.log(rects.length);              // 1: <rect> in collection
svg.removeChild(rects.namedItem("r"));  // remove <rect#r>
console.log(rects.length);              // 0: <rect> gone
<svg id="s">
  <rect id="r"/>
</svg>

There are also some properties available which provide access to live HTMLCollections or NodeLists which may be used for further traversal:

  • ParentNode.children : live HTMLCollection
  • Node.childNodes : live NodeList

Note, however, that NodeLists are not guaranteed to be live by themselves; you will have to check the documentation. The following two methods will return a non-liveNodeList and, thus, can not be used for these purposes.

  • Document.querySelectorAll() and Element.querySelectorAll()

If you need the flexibility they provide you could choose option 2 instead.

2. Use a MutationObserver.

The little-known and highly underestimated MutationObserver interface comes in handy, whenever you are interested in changes to the DOM. This is the more complex approach, which allows for much more flexibility, though.

You create a new MutationObserver providing a callback which is to be invoked every time a relevant change to the DOM occurs. When starting the observer you specify what changes are relevant by defining the element –and sub-tree– of interest and by passing in a MutationObserverInit config object. In the callback you are pretty much free to react to these changes any way you like.

var svg = document.getElementById("s");
var rect = document.getElementById("r");

var observer = new MutationObserver(function(mutations) {
  // This callback will be called for all changes you configured it to listen to
  // and will provide information about every change in the array of 
  // MutationRecords. You can use this to filter and react to the changes you are
  // interested in by providing an approriate callback function. 
  var removed = mutations.filter(function(mutation) {
    return mutation.removedNodes.length > 0;
  });
  console.log(`Observed removal of ${removed.length} node(s).`)
})
 
// Listen for changes of the svg element to the child list only 
observer.observe(svg, { childList: true }); 

console.log("<rect> found: " + document.getElementById("r") != null);  // <rect> found
svg.removeChild(rect);                                                 // remove <rect>
console.log("<rect> found: " + document.getElementById("r") != null);  // <rect> gone
<svg id="s">
  <rect id="r"/>
</svg>
like image 126
altocumulus Avatar answered Nov 14 '22 21:11

altocumulus