Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Javascript

KnockoutJS and Google Maps Binding

5.00/5 (5 votes)
19 May 2012CPOL5 min read 26.1K  
Integrating Google Maps and KnockoutJS

Introduction

This post describes the integration between Google Maps and KnockoutJS. Concretely, you can learn how to make the maps marker part of the View and automatically change its position any time when the ViewModel behind changes. The ViewModel obviously has to contain the latitude and longitude positions of the point that you wish to visualize on the map.

Previously, I have worked a bit with Silverlight/WPF which in general leaves one mark on a person: the preference for declarative definition of the UI leveraging the rich possibilities of data binding provided by the previously mentioned platforms. At this moment, I have a small free-time project where I am visualizing a collection of points on a map. This post describes how to make the marker automatically change its position after the model values behind changes. Just like in this picture below, where the position changes when user changes the values of latitude and longitude in the input boxes.

image

Since I like Model-View-ViewModel pattern, I was looking for a framework to use this pattern in JS, obviously KnockoutJS saved me. The application that I am working on has to visualize several markers on Google Maps. As far as I know, there is no way to define the markers declaratively. You have to use JS:

JavaScript
marker = new google.maps.Marker({
  map:map,
  draggable:true,
  animation: google.maps.Animation.DROP,
  position: parliament
}); 

google.maps.event.addListener(marker, 'click', toggleBounce);

So let's say you have a ViewModel which holds a collection of interesting points, that will be visualized on the map. You have to iterate over this collection to show all of them on the map. One possible way around would be to use the subscribe method of KO. You could subscribe for example to the latitude of the point (assuming that the latitude would be an observable) and on any change perform the JS code. There is a better way.

Defining Custom Binding for Google Maps

The way to go here is to define a custom binding, which will take care of the update of the point on the map, any time, that one of the observable properties (in basic scenario: latitude and longitude) would change ko.bindingHandlers.map = {.

JavaScript
init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
  var position = new google.maps.LatLng(allBindingsAccessor().latitude(), 
                 allBindingsAccessor().longitude());

  var marker = new google.maps.Marker({
    map: allBindingsAccessor().map,
    position: position,
    icon: 'Icons/star.png',
    title: name
  });

  viewModel._mapMarker = marker;
},
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
  var latlng = new google.maps.LatLng(allBindingsAccessor().latitude(), 
               allBindingsAccessor().longitude());
  viewModel._mapMarker.setPosition(latlng);
  }
};
XML
<div data-bind="latitude: viewModel.Lat, longitude:viewModel.Lng, map:map" ></div>

So let's describe what is going on here. We have defined a map binding. This binding is used on a div element. Actually, the type of the element is not important. There are also latitude and longitude bindings, which are not defined. That is because the map binding takes care of everything. The binding has two functions: init and update, first one called only once, the second one called every time the observable value changes.

The allBindingsAccessor parameter contains a collection of all sibling bindings passed to the element in the data-bind attribute. The valueAccessor holds just the concrete binding (in this case the map value, because we are in definition of the map binding). So from the allBindingsAccessor, we can easily obtain the values that we need:

JavaScript
allBindingsAccessor().latitude()
allBindingsAccessor().longitude()
allBindingsAccessor().map()

Notice that the map is passed to the binding in parameter (that is concretely the google.maps.Map object, not the DOM element. Once we have these values, there is nothing easier than to add the marker to the map.

And there is one important thing to do at the end – save the marker, somewhere so we can update its position later. Here again, KO comes to the rescue because we can use the viewModel parameter passed to the binding and we can attach the marker to the ViewModel. Here, I suppose that there is no existing variable with name _mapMarker in the ViewModel and JS can happily add the variable to the ViewModel.

JavaScript
viewModel._mapMarker = marker;

The update method has an easy job, because the marker has been stored, and we only need to update its position.

JavaScript
viewModel._mapMarker.setPosition(latlng);

Almost Full Example

Just check it here on JsFiddle.

Possible Improvements

One thing that I do not like about this, is the fact, that you have to pass the map as an argument to the binding and the div element has to be outside of the map. Coming from Silverlight/WPF, you would like to do something like this:

XML
<div id="map_canvas">
<div data-bind="latitude: vm.Lat, longitude:vm.Lng">Whatever I want to show on the map marker</div>
</div>

That is actually the beauty of declarative UI definition. You can save a lot of code only by composing the elements in the correct order. However this is not possible – at least, I was not able to get it to work. I was close however:

JavaScript
init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
  var map = element.parentNode;
  var googleMap = new google.maps.Map(.map.);
  //add the pointer
  var contentToAddToMarker = element;
}

Again thanks to KO, here the element variable represents the DOM element to which the binding is attached. If the div element is inside the map, then we can get the parent element (which is the div for the map) and we are able to create a new map on this element. The problem which I had is that once the new map was created, the div elements nested inside the map disappeared. Even if that would work, some mechanism would have to be introduced, in order to create the map only the first time (in case there are more markers to show on the map) and store it somewhere (probably as global JS variable).

On the other hand, thanks to the element, you can get all the div which should be for example given as the description to the marker.

Summary: KnouckoutJS is great. It lets me get rid of the bordelic JS code. I am working on a small application and once it’s done, I will show some more useful functions. For now, I have been really surprised by the flexibility of the framework. Every time I have some “nice to have” feature which I am adding, Knockout has the solution for me.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)