2

On a page, I have an SVG map of the US and Canada, and an HTML list of provinces and states. Hovering over any province, either its name in the list OR its depiction on the map, should make both name and depiction turn a different color. All the names and paths have logical IDs/classes on them already.

Here's a fiddle with my code. (It's a horrible procedural mess at the moment, so please forgive me.)

jQuery's event functions don't work on SVG, and though I know there's a jQuery plugin that supposedly helps, I thought this would be a good opportunity to use a larger proportion of vanilla Javascript than I'm used to.

The most relevant part of the code is the makeMapInteractive function on lines 46 through 69 of the Javascript:

function makeMapInteractive(provinces) {

    for(var province in provinces) { // Iterate over every state/province code
        var $HTMLtargets = $('ul.provinces li.' + province);
        var $SVGtargets = $('path#{0}, g#{0} path'.format(province));
        var $allTargets = $HTMLtargets.add($SVGtargets);

        // I tried it first with $().each(); when that didn't work,
        // I commented it out and tried without it. Neither one works.

        /* $allTargets.each(function() {
            this.addEventListener('mouseover', function(e) {
                console.log(e);
                $HTMLtargets.css('color', '#990000');
                $SVGtargets.attr('fill', '#990000');
            }, false)
        }); */

        for(var i = 0; i < $allTargets.length; i++) {
            $allTargets.get(i).addEventListener('mouseover', function(e) {
                $HTMLtargets.css('color', '#990000');
                $SVGtargets.attr('fill', '#990000');
            }, false);
        }
    }
}

What I'm trying to tell it to do is to add a mouseover listener to every element, that triggers a change on all elements involved in that element's province.

What actually happens is that hovering over anything on the whole page triggers the very last event listener added, the one for Wyoming. It's like when I change the $allTargets variable, it changes all the previously-added listeners to the elements contained in its new value. But I can't see how that's happening, since I'm applying the event listeners to the DOM elements inside that variable, not the jQuery object itself.

Can someone explain exactly what is going on here? I know I'm using jQuery a bit here, but I'd like the answer to use no more than I'm already using; it's my vanilla Javascript skill that needs increasing.

75th Trombone
  • 1,364
  • 15
  • 28
  • 1
    Your targets are changing in each loop. You need to wrap them in a closure. This question has been asked dozens of times; excuse me while I find a suitable duplicate. – Evan Davis Sep 18 '13 at 19:42
  • That darn Wyoming is so full of events! :) – Mark Schultheiss Sep 18 '13 at 19:48
  • @Mathletics - the answer may be the same general type of answer as has been offered before, but the question and this particular situation is not a duplicate. This question is unique. – jfriend00 Sep 18 '13 at 19:48
  • @jfriend00 I respectfully disagree. They're both "my variables aren't what I thought they would be; I need a closure." – Evan Davis Sep 18 '13 at 23:38

1 Answers1

6

The problem is that your $HTMLtargets and $SVGtargets variables are not what you want them to be inside your event handler callback because when the event fires (sometime later), your outer for loop has already finished and thus those two variables are on their ending value.

You will need a closure to capture those variables separately for each event handler. Here's one way to do that:

    // create closure to freeze the target variables
    (function(hTargets, sTargets) {
        for(var i = 0; i < $allTargets.length; i++) {
            $allTargets.get(i).addEventListener('mouseover', function(e) {
                hTargets.css('color', '#990000');
                sTargets.attr('fill', '#990000');
            }, false);
        }
    })($HTMLtargets, $SVGtargets);

FYI, I changed the name of the variables inside the closure to make it more obvious what was happening. It is not required to change the name of the arguments to the immediately executing function expression as they will just override the previously defined ones, but I think it's clearer what is happening if you change the names.

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • 1
    I usually wondering why changing variables name, is it only for readability or something i'm missing? – A. Wolff Sep 18 '13 at 19:48
  • 1
    Yeah, I think he was just trying to better illustrate what's actually going on. Fortunately, though I wasn't smart enough about Javascript to know what to do without asking, I do understand it enough to have immediately realized my mistake just from his paragraph. Thanks, @jfriend00 ! – 75th Trombone Sep 18 '13 at 19:50
  • 2
    @A.Wolff - one does not have to change the variable name, but I find it clearer to someone new in this concept that we're creating a new argument variable whose value is captured in the closure. If you use the same variable name, then what is actually happening (an override of the variable same name by a new variable) is not quite so obvious. – jfriend00 Sep 18 '13 at 19:50
  • @jfriend00 Thx, i appreciate your feedback! – A. Wolff Sep 18 '13 at 19:53