Kendo UI (http://www.telerik.com/kendo-ui) is a comprehensive and extensible HTML5/JavaScript framework from Telerik.
At YTBE, we have developed our own library of extensions that we utilize in our projects to further enhance the capabilities of this framework.
The purpose of the article is to provide a basic overview of Kendo UI’s extensibility features by developing a simple custom GoogleMap widget with MVVM bindings.
Precursor: The Built-in Map Widget
In their Q1 2014 release (see: blogs.telerik.com/blogs/14-03-19/kendo-ui-q1-2014-release-announcement), Telerik added a GeoVizualization component that allows developers to plot pins, overlays, and GeoJSON data on a map. This component relies upon map tiles provided by a third-party service such as OpenStreetMap (www.osm.org), Bing Maps API (msdn.microsoft.com/en-us/library/ff428643.aspx), and MapQuest Open (wiki.openstreetmap.org/wiki/Mapquest#MapQuest-hosted_map_tiles), etc. Important note: Just like Google, many of these map tile providers carry licensing and/or other restrictions on the use of their tiles and tile servers. OpenStreetMap’s tile server policy can be found at: wiki.openstreetmap.org/wiki/Tile_usage_policy.
A Basic Google Maps Widget with MVVM Support
Our custom widget will do the following:
- Automatically create a Google Maps canvas
- Retrieve a latitude and longitude from the View Model as defined by data-binding rules
- Display the latitude and longitude as a marker on the map
Dependencies
Our widget has the following JavaScript dependencies:
- jQuery
- KendoUI
- Google Maps API – http://maps.googleapis.com/maps/api/js?key=YOURAPIKEYHERE&sensor=false
Source Code: Widget
The following code defines our widget, which is responsible for generating the user interface.
//Define event constants
var DATABINDING = "dataBinding", DATABOUND = "dataBound";
//Define the widget
var googleMap = kendo.ui.Widget.extend(
{
init: function (e, o) {
if (!google || !google.maps) {
throw "Google Maps javascript reference required for this widget to function!"
}
o.zoom = o.zoom || parseInt($(e).data("zoom")) || 4;
o.animateMarker = o.animateMarker || $(e).data("animateMarker") || google.maps.Animation.DROP;
kendo.ui.Widget.fn.init.call(this, e, o);
$(e).addClass(this.options.name.toLowerCase());
},
options: {
name: "YGoogleMap"
},
events: [
DATABINDING,
DATABOUND
],
setValues: function (v) {
if (!v.latitude || !v.longitude) { //If we do not have both a latitude and longitude
this.element.empty().hide(); //Remove all content and hide the control (no data to display)
return;
}
this.trigger(DATABINDING); //Alert listeners that we are applying bindings
this.element.show();
var latlng = new google.maps.LatLng(v.latitude, v.longitude),
zoom = this.options.zoom,
mapOptions = {
zoom: zoom,
center: latlng,
disableDefaultUI: true
},
map = new google.maps.Map(this.element[0], mapOptions),
marker = new google.maps.Marker({
position: latlng,
map: map,
animation: this.options.animateMarker,
title: v.title
});
console.log(v.title);
this.trigger(DATABOUND); //Alert listeners that we have finished applying bindings
}
});
kendo.ui.plugin(googleMap); //Register the googleMap widget as a plugin
Declaration
We begin declaring our new Widget by extending the base object provided by Kendo (kendo.ui.Widget). Following the declaration, we register this Widget as a plug-in:
kendo.ui.plugin(googleMap);
Among other things, registering the Widget it to be automatically created via a data-role attribute (see “Using the Widget”).
Init
In the initialization block, we override the prototype (kendo.ui.Widget) initializer to:
- Make sure that Google Maps API has been registered and initialized and throw an exception if it has not.
- Retrieve optional settings that can be provided via HTML data attributes and provide defaults for these settings if they have not been supplied:
- zoom – The initial zoom level for the map (the default is 4).
- animateMarker – The type of animation to apply to the marker (the default is a DROP animation).
- Call the prototype’s initializer
- Add a CSS class name (using the name of our Widget as the class name) to the Widget’s container so we can easily more easily apply CSS rules to it.
Options
In the options block we specify the name of our new widget as it will exist in the Kendo UI namespace (Ex: kendo.ui.YGoogleMap). This name is also important because it will also determine how we specify the data-role attribute (Ex: data-role=”ygooglemap”).
Events
Here we list the events our Widget will trigger.
SetValues
SetValues is where the lion-share of the work takes place:
- If the latitude and longitude do not exist, we remove the existing map (if one exists) and hide the container.
- If both values have been provided, we
- Alert any listeners that we’re about to bind the data.
- Show the container.
- Create a Google LatLng object from our supplied latitude and longitude and declare our map options.
- Create the Map and Marker by specifying the widget’s “element” as the container for the Map and specifying the supplied title as the title for our Marker.
- Alert any listeners that we have finished binding the data.
Source Code: Binder
The following code applies custom bindings for latitude, longitude, and title (the title of our Marker) to our Google Maps Widget.
//Define binders for the widget
kendo.data.binders.widget.latitude =
kendo.data.binders.widget.longitude =
kendo.data.binders.widget.title =
kendo.data.Binder.extend({
refresh: function () {
//Make sure we’re dealing with a binding for our custom Widget
if (this.element instanceof googleMap) {
var b = { //Get relevant bindings
lat: this.bindings["latitude"],
lng: this.bindings["longitude"],
ttl: this.bindings["title"]
};
//Get values and pass to the Widget's setValues method
this.element.setValues({
latitude: b.lat ? b.lat.get() : undefined,
longitude: b.lng ? b.lng.get() : undefined,
title: b.ttl ? b.ttl.get() : undefined
});
}
}
});
Declaration
We begin by declaring our custom binders as children of the kendo.data.binders.widget object and by extending kendo.data.Binder. There’s no need to register the custom binders as we did for the Widget itself with the call to “plugin” above. Registration happens automatically because Kendo examines the kendo.data.binders.widget object at runtime to apply bindings for widgets.
Note that in the above example we’re sharing a single binder object for all three bound properties. We could have used separate binders for each; but for our purposes a single binder will suffice.
Refresh
The “Refresh” method of a binder gets called when one of the bound values has changed. In the above example, we:
- Check the “element” to ensure we are handling an instance of our widget.
if (this.element instanceof googleMap) { - Retrieve the values for each of our bindings.
var b = {
lat: this.bindings[“latitude”],
lng: this.bindings[“longitude”],
ttl: this.bindings[“title”]}; - Pass these values to the “setValues” function defined by our widget. Remember, the setValues function waits until the latitude and longitude exist before creating the map:
this.element.setValues({
latitude: b.lat ? b.lat.get() : undefined,
longitude: b.lng ? b.lng.get() : undefined,
title: b.ttl ? b.ttl.get() : undefined
});
Using the Widget
Using the Widget in a MVVM scenario is as simple as defining the data-role as ygooglemap and supplying the custom binding attributes.
Assuming our view model looked like this:
var viewModel = new kendo.data.ObservableObject({
latitude:38.897676,
longitude:-77.03653,
name:"White House"
});
$(function(){
kendo.bind($("body"), viewModel); //Bind to the body of the HTML document
});
Our HTML would look like this:
<style>
.ygooglemap {
width:300px;
height:300px;
}
</style>
<div data-role="ygooglemap" data-bind="latitude:latitude,longitude:longitude,title:name" data-zoom="16"></div>
Here it is in action: