Home > OS >  Is this event listener behavior as expected in D3?
Is this event listener behavior as expected in D3?

Time:11-20

I bumped into some unexpected behavior when trying to refer to a variable outside of an event handler.

Below is function called update that joins data with div elements. In this case, there's only one data point, so only one div element is rendered. The function also takes in a number currentState as an argument. The div element has a click event listener, which prints out the currentState number and increments a global variable state by 1. The original value of state is 5. Then the click handler calls update with the incremented state as a parameter.

When the div is clicked, currentState that's passed into update increments as expected. This is evident from the output of console.log("update called. currentState: ", currentState).

However, inside of the 'click' event handler, currentState remains at 5 regardless of how many times the div element is clicked.

Correct me if I'm wrong, but it seems to me that when the 'click' event listener is created in D3, it holds onto whatever context existed at that time. And because that listener is attached to the div only when it first enters the DOM, that context is never updated.

An easy fix for this issue is just not to attach the click listener on the enter selection, but rather outside of the call to join. That way, the listener will be updated each time update is called.

I basically have two questions. I wanted to confirm whether I'm correctly understanding what's happening. And second, is this behavior in D3 favorable? Perhaps I'm missing an advantage to it. Although this example's a bit contrived, I would have liked for the click handler to refer to the value of currentState that gets updated with each call to update. Or is it not good practice to attach listeners on the enter selection?

const data = ['randomString']

let state = 5

function update (currentState) {
  console.log("update called. currentState: ", currentState)
  d3.selectAll('div')
    .data(data)
    .join(enter => enter.append('div')
      .style('width', '50px')
      .style('height', '50px')
      .style('background', 'red')
      .on('click', function () {
        console.log("click event. currentState: ", currentState)
        state  = 1
        update(state)
    }))
}

update(state)

CodePudding user response:

What is happening is that your initial on binding is the only one that d3 uses in your case. Let's analyse this code:

  d3.selectAll('div')
    .data(data)
    // ...
    .join(enter => enter.append('div')
      .on('click', function () {
        console.log("click event. currentState: ", currentState)
        state  = 1
        update(state)
    }))

What is this doing?

  1. Select all div that exist in the DOM so far
  2. Attach data to them
  3. If there are less div nodes than data entries, create a new div for each one and attach a click handler which logs currentState

What happens if you run this code a second time? At step 3, there is already a div for each data entry and thus no new div is created and no new click handlers are attached unless if the data changes.

CodePudding user response:

As a complement to the other answer, which explains correctly what's happening in D3, this is not a D3 behaviour, this is how JavaScript works.

Here's a demo for showing what's happening with just JavaScript, no D3. Just as in your example when foo is created it depends on its context and therefore on the currentState value. For a simple comparison with your D3 code, foo is stored in an object, so it's not created again every time update is called.

Thus, as expected, modifying the currentState value has no effect on foo after it has been created: it will log 5 every time, not 5, 6, 7 etc...

let state = 5;
const obj = {};

function update(currentState) {
  if (!obj.foo) obj.foo = () => console.log(currentState);
  obj.foo();
  state  = 1;
};

update(state);
update(state);
update(state);

  • Related