2

I am currently experiencing some issues when trying to add a click event listener for some foreignObjects rendered using vanilla JS.

It works when I use the built in d3 on click functions, but I would prefer to have it done using the javascript code.

However, the function never triggers for these elements and I can't understand why.

The code example is not complete, but should highlight what I am trying to do.

var nodes = g.selectAll("foreignObject")
    .data(response.nodes)
    .enter()
    .append("foreignObject")
    .attr("x", function(d) {
        return d.x - nodeWidth / 2;
    })
    .attr("y", function(d) {
        return d.y - nodeHeight / 2;
    })
    .attr("width", nodeWidth)
    .attr("height", nodeHeight)
    .append("xhtml:div")
    .attr("class", "outer")
    .html(function(d) { 
        var nodeHtml = createNodeElement(d);
        return nodeHtml.outerHTML;
    })
   // If I append the img like this, it works, but ends up in the wrong "element scope"
   .append("img")
        .attr("class", "optionsImg")
        .attr("src","/images/options-squares.svg")
        .on("click", function(d) {
            currentTooltipObject = d;
            renderTooltipDiv();
        });


function createNodeElement(d) {
    let nodeElement = document.createElement("div");
    nodeElement.className = "nodeElement";
    let nodeOptionsImg = document.createElement("img");
    nodeOptionsImg.className = "nodeOptionsImg";
    nodeOptionsImg.src = "/images/options-squares.svg";
    nodeOptionsImg.addEventListener("click", function() {
        console.log("Clicked on optionsImg for this object: "+d);
    });
    nodeElement.appendChild(nodeOptionsImg);
    return nodeElement;
}
altocumulus
  • 21,179
  • 13
  • 61
  • 84
Eken
  • 103
  • 1
  • 1
  • 10
  • What do you mean by *"...ends up in the wrong "element scope"* in the comment? As you noticed, that's the correct way doing this, if you could clarify what you are trying to do it'll probably be easy to help you out. – altocumulus Oct 12 '19 at 14:25
  • Ah, sorry.. I figured it wouldn't be understandable. What I mean is that the img element appears on the same level as the html returned from the .html. Like this:
    the div with html returned from "nodeHtml"
    options-img
    But I want the img to be an element of the inner div, not the outer. But, when I put it inside the inner div the eventListener wont fire. And I can't put it as a global event listener since that will not allow me to get the unique object connected to that d3 node(?)
    – Eken Oct 12 '19 at 15:01
  • Do you mind posting what renderTooltipDiv is? You should have the action added by d3 like you have but most likely the renderTooltipDiv function needs the element information from node to render in the right location – Thatalent Oct 12 '19 at 15:26
  • It is just a function which renders some html using jquery (popup effect when clicking on the options img), but that one works as intended. Thanks! – Eken Oct 12 '19 at 15:51

2 Answers2

0

The main problem with your approach is the fact that using outerHTML and innerHTML (which is used internally by .html()) for creating / moving / copying elements is kind of like serializing and de-serializing your HTML DOM tree. This works for the HTML elements themselves, however, it does not preserve event listener. Hence, the listeners you attached to the elements in your function createNodeElement are lost during this process. This is a variation of other questions like, e.g. "Is it possible to append to innerHTML without destroying descendants' event listeners?".

If you take a step back and re-read the D3 API docs you will realize that you are in fact almost there: D3 provides means to append native DOM nodes to a selection:

selection.append(type) <>

If the specified type is a function, it is evaluated for each selected element, in order, being passed the current datum (d), the current index (i), and the current group (nodes), with this as the current DOM element (nodes[i]). This function should return an element to be appended.

If you would like to stick to your implementation of createNodeElement to create an element by using native JS methods, you can simply pass that function to selection.append() as it returns a newly created node along with its <img> child node. Your code can thus be simplified to:

var nodes = g.selectAll("foreignObject")
    /* styling omitted */
  .append("xhtml:div")
    .attr("class", "outer")
  .append(createNodeElement);

Because only references to DOM nodes are passed around all event listeners attached to the elements will also be preserved.

altocumulus
  • 21,179
  • 13
  • 61
  • 84
  • Wow. How I didn't think of that, I guess I've been confused. Thank you very much, I haven't tested it out yet, but I am sure it will work. I assume I can simply return the html for the nodeElement as I do today? Since there are multiple elements inside that element (with custom written text and images and so) - not shown in the example. – Eken Oct 12 '19 at 15:50
-1

In the createNodeElement function nodeOptionsImg element is not yet rendered when you do addEventListener. Listeners can be added only to rendered elements.

Thevs
  • 3,189
  • 2
  • 20
  • 32
  • Didn't think of that! Thanks. Just gotta figure out how to make that since I suppose it's the "d3" engine responsible for the calls to do the actual rendering(?). As you see, I return a html string to the .html function. I'll do some thinking – Eken Oct 12 '19 at 15:06