0

I am trying to create a click event handler for a vis.js network like the one shown in this example:

network.on("click", function (params) {
    params.event = "[original event]";
    document.getElementById('eventSpan').innerHTML = '<h2>Click event:</h2>' + JSON.stringify(params, null, 4);
    console.log('click event, getNodeAt returns: ' + this.getNodeAt(params.pointer.DOM));
});`

However I am trying to do so in a loop and would like to have the anonymous event handling function be able to use local variables in the loop (before they are changed in successive iterations).

For example:

for (data in data_list) {
    var network = new vis.Network(data['container'], data['data'], data['options']);
    network.on("click", function(params) {
         console.log(data) // On event, data always equal to last element in data_list. I want it to save the data from the iteration this function was created in
         console.log(params) // I also need to access 'params'
    }
}

I understand that this should be done with a closure, however I could not find any examples of how to do this while also maintaining access to the 'params' passed in. How can I create a closure which can accept the 'params' argument but also save the state of 'data'?

Tyler Cloutier
  • 2,040
  • 2
  • 21
  • 31
Matthew Lueder
  • 953
  • 2
  • 12
  • 25

3 Answers3

1

I typically externalize the function, pass it the data I want to keep around and return a function that accepts the handlers parameters:

function getOnClickHandler(data) {
   return function(params) {
      console.log(data);
      console.log(params);
   }
}

for (data in data_list) {
    var network = new vis.Network(data['container'], data['data'], data['options']);
    network.on("click", getOnClickHandler(data));
}
Rob M.
  • 35,491
  • 6
  • 51
  • 50
0
for (data in data_list) {
    var network = new vis.Network(data['container'], data['data'], data['options']);
    network.on("click", (params) => {
         console.log(data) // On event, data always equal to last element in data_list. I want it to save the data from the iteration this function was created in
         console.log(params) // I also need to access 'params'
    }
}

Let me know if you need further clarification.

Peter Van Drunen
  • 543
  • 5
  • 15
  • "data" in this code is still being overwritten by the final iteration of the loop – Matthew Lueder Sep 15 '17 at 18:40
  • @Teemu The function is a lamdba now which will allow access to external local scope variables (i.e. `data` in this example). It's possible I misunderstood what the OP was trying to accomplish, however. There are some other answers which do a better job solving this problem. – Peter Van Drunen Sep 15 '17 at 18:43
  • That makes no difference, whether the "lambda" is an arrow function or an anonymous function expression, `data` is accessible in both cases, and both give only the last value from `for .. in`. – Teemu Sep 15 '17 at 18:50
  • Hmmm... Good to know. Thanks for the lesson. – Peter Van Drunen Sep 19 '17 at 01:25
0

Ah, the ol' callback in a loop gotcha of Javascript.

The explanation for why this happens is that your code executes in it's entirety before a click event is received. The means that it loops through creating new networks, defining new onClick handlers, and then adding them to the network. While each of these handlers is separate, they all are capturing the same data variable, which will at the end of the loop end up referencing to the last data element.

var arr = [1, 2, 3, 4, 5]
for (i in arr) {
    setTimeout(function () { console.log(i) }, 1000);
}
// prints 4, 4, 4, 4, 4

There are two ways to fix this problem. The first would be to take advantage of ES 6's let declaration which causes variables to be block scoped (e.g. body of for loop), vs var declarations which are function scoped.

var arr = [1, 2, 3, 4, 5]
for (let i in arr) {  // NOTE THE USE OF let HERE.
    setTimeout(function () { console.log(i) }, 1000);
}
// prints 0, 1, 2, 3, 4

The second way would be to pass your var to a function so as to create a new variable for each looping of the code which is scoped to that function.

function handlerWrapper(i) {
    return function() { console.log(i) };
}

var arr = [1, 2, 3, 4, 5]
for (i in arr) {
    setTimeout(handlerWrapper(i), 1000);
}
// prints 0, 1, 2, 3, 4

With this arrangement it's clear where your parameters should go for your callback function:

function handlerWrapper(data) {
    return function(params) { console.log(data, params) };
}
Tyler Cloutier
  • 2,040
  • 2
  • 21
  • 31