9

In D3.js v4, when registering an event listener through a traditional callback function, this references the current DOM element:

d3.select("div").on('mouseenter', function() {
  d3.select(this).text("Yay");
});

ES6 offers arrow functions, which IMHO make D3.js code a lot more readable because they are very concise. However, traditional callbacks cannot blindly be replaced with arrow functions:

d3.select("div").on('mouseenter', () => {
  d3.select(this); // undefined
});

The article "On D3 and Arrow Functions" gives a very good explanation of why this is not bound as expected. The article suggests using traditional callbacks for code that needs access to the current DOM element.

Is it possible to access the current DOM element from an arrow function?

Gerardo Furtado
  • 100,839
  • 9
  • 121
  • 171
Rahel Lüthy
  • 6,837
  • 3
  • 36
  • 51

2 Answers2

10

There is an idiomatic way of doing this in D3: just use the less famous third argument:

selection.on("mouseenter", (d, i, nodes) => {
    d3.select(nodes[i]);
});

And that's the same of:

selection.on("mouseenter", function() {
    d3.select(this);
});

I wrote an example here: d3 v4 retrieve drag DOM target from drag callback when `this` is not available

Here is a demo:

d3.selectAll("circle").on("mouseover", (d, i, p) => {
        d3.select(p[i]).attr("fill", "maroon")
    })
    .on("mouseout", (d, i, p) => {
        d3.select(p[i]).attr("fill", "seagreen")
    });
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg>
 <circle cx="50" cy="50" r="20" fill="seagreen"></circle>
 <circle cx="125" cy="50" r="20" fill="seagreen"></circle>
 <circle cx="200" cy="50" r="20" fill="seagreen"></circle>
</svg>

Actually, if you look at the end of the article you linked, he gives the same solution.

Your proposed solution, d3.event.target, despite working for event listeners, does not work in several situations. For instance:

d3.selectAll("circle").each(()=>d3.select(d3.event.target).attr("fill", "red"))
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg>
 <circle cx="50" cy="50" r="20" fill="seagreen"></circle>
 <circle cx="125" cy="50" r="20" fill="seagreen"></circle>
 <circle cx="200" cy="50" r="20" fill="seagreen"></circle>
</svg>

But the same code works using the third argument:

d3.selectAll("circle").each((d,i,p)=>d3.select(p[i]).attr("fill", "red"))
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg>
 <circle cx="50" cy="50" r="20" fill="seagreen"></circle>
 <circle cx="125" cy="50" r="20" fill="seagreen"></circle>
 <circle cx="200" cy="50" r="20" fill="seagreen"></circle>
</svg>
Graham
  • 7,431
  • 18
  • 59
  • 84
Gerardo Furtado
  • 100,839
  • 9
  • 121
  • 171
  • Thanks, I was aware of this solution. While it may be the "idiomatic" way, IMHO `d3.select(d3.event.target)` is more readable than `d3.select(nodes[i])`. But it's good to have multiple approaches so people can choose! – Rahel Lüthy Mar 08 '17 at 10:42
  • I don't know if `d3.event.target` is "more readable", that's opinion-based. But I know this: it's *not* the same of `this` and, more important, it only works in specific situations (I'm aware of "event listeners" in your question's title). See my edit. – Gerardo Furtado Mar 08 '17 at 11:08
  • Yes, I agree, my comment about readability is specific to the event listener example. – Rahel Lüthy Mar 08 '17 at 11:26
1

It is possible to use an ES6 arrow function and access the current DOM element via d3.event.target:

d3.select("div").on('mouseenter', () => {
  d3.select(d3.event.target).text("Yay, this works!");
});
Rahel Lüthy
  • 6,837
  • 3
  • 36
  • 51
  • Did you mean `d`, the `event` parameter? – Bergi Jan 08 '17 at 12:26
  • Nope, I really meant [d3.event](https://github.com/d3/d3-selection#event). `d` is not actually used, I will remove it for clarity... – Rahel Lüthy Jan 08 '17 at 12:30
  • Yes, it is possible, but what's the point? It is certainly not more readable than normal function. The purpose of arrow function is to provide lexical `this`. Which is not the case for older libraries that weren't written with this concern in mind. – Estus Flask Jan 08 '17 at 13:22
  • They are certainly more readable for simple things like `.attr("x", d => xScale(d))`. Personally, I'd rather use them consistently, i.e. not mix with traditional syntax. But that's just my two cents (coming from Scala / FP). – Rahel Lüthy Jan 08 '17 at 14:13
  • Yes, one-liners with implied `return` are strong part of arrows - but only where they are necessary. Otherwise it is an antipattern. Unintentional return may screw up the things (very common among coffeescript devs, where ES6 arrows were borrowed from). And it supposes the wrong signature (TS or Flow may not be happy with such callback). I like arrows a lot and stick to them wherever it is prctical, but here an anonymous `function` function looks very consistent. It designates that a callback uses non-lexical `this` (would be nice to have Coffee `->` thin arrows also in JS, but there are none) – Estus Flask Jan 08 '17 at 19:20
  • Also, I'm not sure how `d3.event.target` works, but it looks like it is assigned synchronously on each event. This means that `.on(..., () => { setTimeout(() => { ...d3.event.target... }, 100) })` may refer to wrong target. That's a matter of taste to some degree, but an arrow looks inappropriate to me in cases like that. – Estus Flask Jan 08 '17 at 19:25
  • Thank you for the detailed clarifications. As mentioned, I have a Java/Scala background, so a lot to learn in the JS world. If an arrow function really leads to wrong behavior, then obviously it is the wrong choice... – Rahel Lüthy Jan 08 '17 at 19:40
  • 1
    Sure, you're welcome. Even familiar things can have different semantics in different languages. Btw, when there are more than 1 respondent in SO comments, a user should be addressed directly with @netzwerg. I've seen your last comment by pure chance. – Estus Flask Jan 08 '17 at 22:06