Wrapping Google Maps with Polymer Elements

Web Components Week continues at Divshot! Today we're taking a look at how you can wrap an existing JavaScript library with a custom element using Polymer to make simple, reusable components.

Yesterday we mentioned how you might want a <google-map> element. We're not going to create the world's best <google-map> element today, but we're going to create one.

Here at <divshot> we're a far-flung bunch. It'd be nice to show you a map of where we are.

Before we get started, here is that map. (requires a browser that can run Polymer)

The Map Element

We'll make a custom element for our employee map.

<polymer-element name="divshot-employee-map">
  <template>
    <style>
      :host { display: block; }
    </style>
  </template>
  <script>
    Polymer("divshot-employee-map");
  </script>
</polymer-element>

That's a pretty basic polymer element definition. It won't do much for now, but it does score us one block element that we can use.

I've gone ahead and set up a little project with polymer installed via bower.

I'm also putting the components I'm writing in a directory called /components.

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>About Divshot</title>
    <script src="/bower_components/platform/platform.js"></script>
    <link rel="import" href="/bower_components/polymer/polymer.html">
    <link rel="import" href="/components/divshot-employee-map.html">
    <style>
      body {
        font:500 14px/21px 'Helvetica Neue', Helvetica, Arials;
        width: 960px;
        margin: 0 auto;
      }

      divshot-employee-map {
        height: 600px;
      }
    </style>
  </head>
  <body>
    <h1>Divshot is a company. We have employees. Here we are:</h1>
    <divshot-employee-map>
    </divshot-employee-map>
  </body>
</html>

The Employee Element

I think it'd be nice to specify each employee right in the markup of the <divshot-employee-map>. Something like this:

<divshot-employee-map>
  <divshot-employee 
    name="Michael Bleigh" 
    location="Grand Canyon" 
    avatar="http://divshot.com/assets/team/michaelbleigh-e43b3bfc130619ffc9e4eac05a0147c8.jpeg">
  </divshot-employee>

  <divshot-employee
    name="Jake Johnson"
    location="Las Vegas"
    avatar="http://divshot.com/assets/team/jakejohnson-c867cabcca4a000e908b875a9d345ff4.png">
  </divshot-employee>

  <divshot-employee
    name="Scott Corgan"
    location="St. Louis Arch"
    avatar="http://divshot.com/assets/team/scottcorgan-dcc25b7bf02bb900d44764ad80d85c83.jpeg">
  </divshot-employee>    

  <divshot-employee
    name="Robert Willard"
    location="The Alamo"
    avatar="http://divshot.com/assets/team/robertwillard-b246c15c28d2dd3d9cd620dea27e422f.png">
  </divshot-employee>    

  <divshot-employee 
    name="Collin Miller" 
    location="Cape Canaveral" 
    avatar="http://divshot.com/assets/team/collinmiller-9679dd5bfde2bea57eb786b47df6f647.jpeg">
  </divshot-employee>

  <divshot-employee 
    name="Kevin Chau" 
    location="World's Largest Ball of Twine" 
    avatar="http://divshot.com/assets/team/kevinchau-cc8e79a62ad0cfb677295a05184768dd.jpg">
  </divshot-employee>

  <divshot-employee 
    name="Mary Best" 
    location="Pikes Peak" 
    avatar="http://divshot.com/assets/team/marybest-239e2a434bc536e2bbe33a7b7858f40e.jpg">
  </divshot-employee>
</divshot-employee-map>

Not a bad bit of markup. Should be easy for anybody to come along and add new employee content as we go.

But this isn't going to do anything yet. We'll have to define and import the <divshot-employee> element.

<polymer-element name="divshot-employee" attributes="name location avatar">
  <script>
    Polymer("divshot-employee");
  </script>
</polymer-element>
  <link rel="import" href="/components/divshot-employee.html">

Note that we set attributes for this element. The three attributes we used in our markup are specified in the element definition as well.

Integrating Google Maps JS

We're going to have to go into the definition for <divshot-employee-map> and teach it how to draw a map.

At the top of the file we'll load the scripts for google maps (not forgetting to set our api key).

<script type="text/javascript"
  src="https://maps.googleapis.com/maps/api/js?key={apiKey}">
</script>
<script type="text/javascript"
  src="https://maps.gstatic.com/intl/en_us/mapfiles/api-3/17/5/main.js">
</script>

(Author's Note: There's supposed to be some sort of magic that happens in the first script that loads the second script, but I couldn't figure it out before our publication deadline. "Not my circus, not my monkeys.")

And we'll update the rest of the definition to draw a map:

<polymer-element name="divshot-employee-map">
  <template>
    <style>
      :host { display: block; }
      #mapCanvas {
        width: 100%;
        height: 100%;
      }
    </style>
    <div id="mapCanvas"></div>
  </template>
  <script>
    Polymer("divshot-employee-map", {
      ready: function() {
        this.map = new google.maps.Map(this.$.mapCanvas, {
          center: new google.maps.LatLng(41, -91),
          disableDefaultUI: true,
          zoom: 5
        });
      }
    });
  </script>
</polymer-element>

We added a bit of css:

#mapCanvas {
  width: 100%;
  height: 100%;
}

and a bit of html to the element template:

  <div id="mapCanvas"></div>

This is so we can have an element to hang our map upon. We're not worried about repeating element ids, cluttering the dom, or anything like that because these elements will live in the Shadow DOM. Sort of like how <input> elements are made up of many parts, but the cruft is hidden from view.

Finally we've added a ready handler to actually insert the map into our custom element.

  ready: function() {
    this.map = new google.maps.Map(this.$.mapCanvas, {
      center: new google.maps.LatLng(41, -91),
      disableDefaultUI: true,
      zoom: 5
    });
  }

Polymer elements have a useful $ property. The $ property is populated with child elements keyed by their id attribute. So we're using that handy accessor to instantiate our map instance.

Connecting the Two Elements

So now we've got a <divshot-employee-map> instance full of <divshot-employee> instances that don't do anything. But not for long!

<polymer-element name="divshot-employee" attributes="name location avatar">
  <script>
    Polymer("divshot-employee", {
      ready: function() {
        this.map = this.parentNode.map;
        this.geocoder = new google.maps.Geocoder();
        this.geocoder.geocode({address: this.location}, function(results, status) {
          if (status == google.maps.GeocoderStatus.OK) {
            this.infoWindow = new google.maps.InfoWindow({
              content: "<div>" +
                "<img src=\"" + this.avatar + "\" style=\"width: 64px; height: 64px\">" +
                "<p>" +
                  this.name +
                "</p>" +
                "<p>" +
                  this.location +
                "</p>" +
              "</div>"
            });

            this.marker = new google.maps.Marker({
              map: this.map,
              position: results[0].geometry.location,
              title: this.name,
            });

            google.maps.event.addListener(this.marker, 'click', function() {
              this.infoWindow.open(this.map, this.marker);
            }.bind(this));

            this.infoWindow.open(this.map, this.marker);
          }
          else {
            console.error("Geocode was not successful for the following reason: " + status);
          }
        }.bind(this));
      }
    });
  </script>
</polymer-element>

When each <divshot-employee> element is ready, we do a geocoder lookup of it's location attribute and create a Marker and InfoWindow for it. It's more lines of code than the rest of the app, but it really isn't doing very much. Just looking up a latitude and longitude and then placing a marker on the map.

Just to prove we've been up to something I opened all the info windows automatically. As I said before, it's not the best <google-map> element that will ever be created, but it does trick for our low-budget employee info page.

The best part is that anybody can add to the employee map as long as they know a little bit of <html> and are adventurous enough to copy and paste!


Take a look at the source code, ad give it a whirl at: http://about-us.divshot.io/.

If you have any questions I invite you to leave a comment below :)