1
votes

In a project where I'm using the Google Maps InfoWindow to display details for locations, I have run into problems with knockout binding. I've bound the data of the selected location with knockout the HTML content of the InfoWindow, which seems to work fine in the beginning.

Now the problem: as soon as the InfoWindow is closed (e.g. by pressing the x-button), google maps removes the html content completely from the DOM and adds it again, when the InfoWindow is displayed again (e.g. when the next marker is clicked).

The problem here is, that the binding gets lost as soon as the element is removed from the DOM, so the content doesn't update anymore.

Any idea how i can reapply the binding to this exact element? calling ko.applyBindings() again leads to the exception "You cannot apply bindings multiple times to the same element".

Here the HTML element for the InfoWindow content:

<div id="info-content" data-bind="if:selectedPlace">
  <div class="gm-iw gm-sm" data-bind="with:selectedPlace">
    <div class="gm-title" data-bind="text:name"></div>
    <div class="gm-basicinfo">
      <div class="gm-addr" data-bind="text:vicinity"></div>
      <div class="gm-website" data-bind="if:website"><a target="_blank" data-bind="attr: {href:website}, text:websiteText"></a></div>
      <div class="gm-phone" data-bind="text:formatted_phone_number"></div>
    </div>
    <div class="gm-rev" >
      <span data-bind="if:rating">
        <span class="gm-numeric-rev" data-bind="text:rating"></span>
        <div class="gm-stars-b">
          <div class="gm-stars-f" data-bind="style: { width: getStarWidth } "></div>
        </div>
      </span>
      <span data-bind="if:url"><a target="_blank" data-bind="attr: {href:url}">see more</a></span>
    </div>
  </div>

JS initialize function and onClick function:

// the initialize function gets called after ko.applyBindings(model)
ViewModel.prototype.initialize = function () {

    ... code to initialize map and search elements

    // instantiate an InfoWindow
    infoWindow = new google.maps.InfoWindow();
    infoWindow.setContent(document.getElementById('info-content'));

    ... 

}

// gets called for each place returned by the search, 
// koPlace is the knockout observable for a place returned by the search, 
// i is the index of the search result
function addMarker(koPlace, i) {

    // create a marker
    var m = new google.maps.Marker({
      title: koPlace.name(),
      position: koPlace.jsPlace.geometry.location,
      animation: google.maps.Animation.DROP,
    });
    m.koPlace = koPlace;
    koPlace.marker = m;
    markers[i] = m;

    google.maps.event.addListener(m, 'click', markerClicked);
    setTimeout(dropMarker(i), i * 100);
}

function markerClicked() {
    var koPlace = this.koPlace; // this refers to the marker object

    if (koPlace) { // just checking
        koPlace.selected(true); // this is used for highlighting.
        model.selectedPlace(koPlace); // this should set the binding.
        infoWindow.open(map, this);
    }
}
3

3 Answers

1
votes

I thought I would add another solution to this problem that I have used.

You can set the content string (as Nurik stated) and have it keep your bindings but there are a couple of extra steps. You then need to use jQuery to convert the string into a DOM node and reapply the knockout bindings:

function makeContent() {
    var html = '<div id="info-window-container" style="display:none;">' +
                    '<div id="info-content" data-bind="if:selectedPlace">' +
                    '...' +
                    '</div>' +
               '</div>';
    html = $parseHTML(html)[0];
    return html;
)}

function addMarker() {
    var infoWindow = new google.maps.InfoWindow();
    var m = new google.maps.Marker({
      title: koPlace.name(),
      ...
      content: makeContent()
    });
    m.koPlace = koPlace;
    koPlace.marker = m;
    markers[i] = m;

    google.maps.event.addListener(m, 'click', function() {
        infoWindow.setContent(this.content);
        infoWindow.open(map,this);
    )};

    ko.applyBindings(YourViewModel, m.content);
)}
0
votes

It seems that I found a solution myself by wrapping the content in another invisible container div and re-adding the DOM element to the container when the InfoWindow is closed.

The HTML now looks like this:

<div id="info-window-container" style="display:none;">
    <div id="info-content" data-bind="if:selectedPlace">
        ...
    </div>
</div>

I added this line to the init function:

google.maps.event.addListener(infoWindow, 'closeclick', closeInfoWindow);

and this function to the JS to re-add the infoWindow content to the DOM:

function closeInfoWindow() {
    document.getElementById('info-window-container').appendChild(infoWindow.getContent());
}

now knockout updates the infoWindow content as expected and it gets displayed correctly when a marker is clicked.

0
votes

Thanks for the question. Its a hard one:) If you look at the google documentation for the infoWindow it shows that you need to tell the function where the content is coming from:

instead of writing: infoWindow = new google.maps.InfoWindow(); infoWindow.setContent(document.getElementById('info-content'));

you could consider this:

infowindow = new google.maps.InfoWindow({
    content: contentString 
});

of course you would also need to define contentString.

Check the documentation here: https://developers.google.com/maps/documentation/javascript/examples/infowindow-simple