views:

235

answers:

2

I have a shared infoWindow for all my markers. It works good if I use jquery's $().each(function(){}), but if I change it to JavaScrips's native for or while loop, it's not working as expected.

Whenever I click a marker, it will open the last populated marker's infoWindow, instead of the clicked marker's infoWindow.

This works:

$(stores).each(function() {

   var storeId = $(this).attr('storeId');
   var address = $(this).attr('rawAddress');
   var city = $(this).attr('city');
   var state = $(this).attr('state');
   var zip = $(this).attr('zip');
   var lat = $(this).attr('lat');
   var lng = $(this).attr('lng');
   var distance = $(this).attr('distance');

   //create marker
   var point = new google.maps.LatLng(lat, lng);

   var marker = new google.maps.Marker({ position: point
     , map: InStorePickup.map
     , title: city + ' #' + storeId
   });

   //create infowindow content
   var infoWindowContent = '<div style="font-size: 8pt;">'
     + '<strong>' + city + ' #' + storeId + '</strong> (' + parseFloat(distance).toFixed(2) + 'mi)<br />'
     + address + '<br />' + city + ', ' + state + ' ' + zip
     + '</div>';

   google.maps.event.addListener(marker, 'click', function() {
     InStorePickup.openInfoWindow(marker, infoWindowContent);
   });

   bounds.extend(point);
 });

But this does not work:

for (var i = 0; i < 3; i++) {
    var store = stores[i];

    var storeId = $(store).attr('storeId');
    var address = $(store).attr('rawAddress');
    var city = $(store).attr('city');
    var state = $(store).attr('state');
    var zip = $(store).attr('zip');
    var lat = $(store).attr('lat');
    var lng = $(store).attr('lng');
    var distance = $(this).attr('distance');

    //create marker
    var point = new google.maps.LatLng(lat, lng);

    var marker = new google.maps.Marker({ position: point
                  , map: InStorePickup.map
                  , title: city + ' #' + storeId
    });

    //create infowindow content
    var infoWindowContent = '<div style="font-size: 8pt;">'
                   + '<strong>' + city + ' #' + storeId + '</strong> (' + parseFloat(distance).toFixed(2) + 'mi)<br />'
                   + address + '<br />' + city + ', ' + state + ' ' + zip
                   + '</div>';

    google.maps.event.addListener(marker, 'click', function() {
        InStorePickup.openInfoWindow(marker, infoWindowContent);
    });

    bounds.extend(point);
}

Any thoughts?

A: 

You would be having a very common closure problem in the for loop. Incidentally I have answered a similar question on this topic just yesterday, which you may want to check out.

Variables enclosed in a closure share the same single environment, so by the time the click callback is called, the loop will have run its course and the infoWindowContent variable will be left pointing to the last value it was assigned to.

One way to solve this is with more closures, using a function factory:

function makeInfoWindowListener (pMarker, pContent) {  
  return function() {
    InStorePickup.openInfoWindow(pMarker, pContent);
  };  
}


// ...

for (var i = 0; i < 3; i++) {

  // ...

  var infoWindowContent = '<div style="font-size: 8pt;">' + city + '...</div>';

  google.maps.event.addListener(
    marker, 
    'click', 
    makeInfoWindowListener(marker, infoWindowContent)
  );
}

This can be quite a tricky topic, if you are not familiar with how closures work. You may be interested in checking out the following article for a brief introduction:


UPDATE:

The reason why the jQuery example works, is because the function you pass to the each() method encloses each iteration in its own scope. You could practically do the same thing in your plain JavaScript example:

for (var i = 0; i < 3; i++) {

  // each iteration will now have its own scope
  (function () {

     var infoWindowContent = '<div style="font-size: 8pt;">' + city + '...</div>';

     google.maps.event.addListener(marker, 'click', function() {
       InStorePickup.openInfoWindow(marker, infoWindowContent);
     });

  })();
}

2nd UPDATE:

Note that the first example can also be rewritten as follows, using an anonymous function:

for (var i = 0; i < 3; i++) {

  // ...

  var infoWindowContent = '<div style="font-size: 8pt;">' + city + '...</div>';

  google.maps.event.addListener(marker, 'click', (function (pMarker, pContent) {  
    return function() {
      InStorePickup.openInfoWindow(pMarker, pContent);
    };  
  })(marker, infoWindowContent));
}
Daniel Vassallo
Thanks for the help Daniel, the link is very informative.The second solution from you worked for me but the first one did not.Regarding your first solution, did you mean?google.maps.event.addListener(marker, 'click', makeInfoWindowListener(marker, infoWindowContent));that worked for me.which one do you prefer amongst your 2 solutions?
koderoid
@koderoid: Oops you're right about the first example. I had a big copy/paste mistake in there. As you found out, the whole `InStorePickup.openInfoWindow` was unnecessary. Fixed my answer... Note that the factory approach can be "inlined" using an anonymous function, as shown in the last example of my updated answer. Regarding my preference, I'm more used to this last approach, but it's just a matter of personal taste.
Daniel Vassallo
Yes, I agree. The second approach looks easier and that's my taste too, but it might have some performance trade-off. BTW, your update to the first solution is also nice, I will actually use that instead. Thanks,
koderoid