Polymer Dart Code Lab: Google Maps

This code lab is a step-by-step guide to creating a fully functional Google Maps web app with Polymer Dart and custom elements from Polymer's Google Web Components collection. With Polymer custom elements and data binding, you'll see that it's possible to put together an advanced application using little or no imperative (Dart or JavaScript) code.

Note: This code lab's full code is available on Github for those who would like to play with it or use it as a reference.

The code lab was tested with Dart SDK v1.13.2.

Credit

This Polymer Dart code lab was adapted from a similar lab for JavaScript, which was presented at Polymer Summit 2015.

Dart, Polymer, and Setting Up

If you haven't already done the intro code lab, Polymer Dart Code Lab: Your First Elements, I recommend you do so before continuing with this one. At the very least, you should review the first few sections for instructions on how to set up your Polymer Dart development environment. Also included there are links to familiarize yourself with Dart and Polymer in general.

This code lab assumes some knowledge of HTML, CSS, and Dart syntax.

Creating Your Project

To create your project directory for this code lab, follow the instructions in Polymer Dart Code Lab: Your First Elements under the section Step 1: Gotta Start Somewhere.

Code Lab: Google Maps

When you're finished, you'll have a web app that looks something like this:

maps app

It will display a Google Map and provide an interface for showing travel directions with four different modes of travel. Let's get started!

Step 1: Showing a Map

If you've followed the instructions in the introductory code lab for creating a new Polymer Dart project, you'll already have a working application with several code files. In Step 1, you'll add/remove code from those files to get a Google Map on the screen.

Step 1.1: Iron Flex Layout

A Google Map is a finicky sort of component, and it requires that its container element have a set height. With Polymer's iron-flex-layout convenience classes, this sort of thing is a cinch.

Import It

First, import the layout tools into your Dart app by adding this line just below the other import statements in your main Dart code file, which you'll find in the project's web directory:

web/index.dart

import 'package:polymer_elements/iron_flex_layout/classes/iron_flex_layout.dart';

Now everything iron-flex-layout has to offer will be available in index.html.

Use It

Add the fullbleed CSS class to your app's <body>:

web/index.html

<body unresolved class="fullbleed">

The unresolved attribute is a tool used by Polymer to keep your users from seeing a flash of unstyled content (FOUC). It tells Polymer to hide the body until the custom elements inside it are ready to go. The fullbleed CSS class causes your app to fill all available space in the browser's client area.

Next, add the fit class to the <main-app> custom element:

web/index.html

<main-app class="fit"></main-app>

With this in place, <main-app> will expand to fill its container, creating the kind of environment a Google Map can be comfortable in.

Step 1.2: The Map

You're going to place a Google Map element inside your <main-app> custom element.

Import It

To gain access, you must first import it (hopefully you're sensing a pattern here).

lib/main_app.dart

import 'package:polymer_elements/google_map.dart';
Use It

Now, replace the contents of the <template> tag in your custom element's HTML view file with this:

lib/main_app.html

<google-map id="g-map" 
            latitude="40.2735697"
            longitude="-111.7154958"
            zoom="13">
</google-map>
Run It

If you run your app's entry point (web/index.html) with Dartium now, you should see a map filling your browser viewport, centered on one of the meeting places of the Google Developer Group of Utah in the United States. It's even responsive! Resize your browser window to see it in action.

Step 1.3: Add a Marker

There is even a Polymer element for map markers. You'll add one here to better pinpoint the location of the GDG Utah meetup.

Disable Default UI

First, get rid of some of the distracting map UI by adding the disable-default-ui attribute to <google-map>:

lib/main_app.html

<google-map disable-default-ui ...> 
Add the Marker

Then add the marker element as a child of <google-map>:

lib/main_app.html

<google-map-marker latitude="40.2735697"
                   longitude="-111.7154958"
                   title="GDG Utah Dart Meetup&#13;Orem, Utah&#13;USA"
                   draggable="true">
</google-map-marker>

The marker locations are set using latitude and longitude attributes, and you can add as many of these markers as you like.

Run It

Run your app again. You'll see that the on-screen controls are gone and there is a marker on the map.

marker

Step 2: Directions

As if your app didn't already do enough, now you're going to add the ability to display directions on the map. With Polymer, it's a snap, because they've thoughtfully provided an element for that. There are two steps to making use of the directions element:

  1. Declare an instance of the element in the HTML file.
  2. Connect it to the map element.

Step 2.1: Add a Directions Element

Import It

As always, before you can use a custom element, you must import it.

lib/main_app.dart

import 'package:polymer_elements/google_map_directions.dart';
Use It

With that done, you can declare an instance. Place this tag anywhere inside your main app's <template>, as a sibling to the <google-map>:

lib/main_app.html

<google-map-directions id="directions"
                       start-address="Orem"
                       end-address="SLC">
</google-map-directions>

That element is set up to give directions for traveling between Orem, Utah and Salt Lake City, Utah in the United States.

Step 2.2: Hook 'Em Up

The <google-map-directions> element fetches directions, but that isn't very useful by itself. You've got to connect the directions results to <google-map> to get them to render. Both elements have a map attribute, the value of which is an instance of a Map object from the Google Maps JavaScript API. To connect the directions element to the map element, set them to use the same Map object.

Add this ready() method to your MainApp class:

lib/main_app.dart

void ready() {
  GoogleMap gMap = $['g-map'];
  GoogleMapDirections dir = $['directions'];
  gMap.addEventListener('api-load', (_) => dir.map = gMap.map);
}
Main Points
  • Polymer will call ready() when the local DOM (the <template>) of <main-app> is finished initializing.
  • You can access any element in your custom element's local DOM that has a static id using the $ object. This is Polymer's automatic node finding feature. A reference to the GoogleMap can thus be acquired with $['g-map'] and, similarly, you can get at the GoogleMapsDirections object with $['directions'].
  • Waiting until the api-load event is fired on the GoogleMap ensures that it has loaded. Once that happens, you set the two elements' map properties to be the same.
Hey! Isn't that imperative code? What gives?

I boasted in the intro that you could get through the majority of this app without writing much, if any, imperative code. Now, that isn't much, but it turns out you don't even need that. In a later step, you will remove the ready() method entirely in favor of a declarative approach, but it's instructive to demonstrate how one might go about wiring things up the hard way.

Run It

If you run the app now, you should see a path from Orem to Salt Lake City superimposed on the map.

directions

Step 3: User Input Card

In this step, you'll use Polymer's Paper Elements collection to enable the app's user to customize the directions. It turns out not everyone is interested in visiting Salt Lake City.

Step 3.1: Import All the Things

Import all the elements you'll need.

lib/main_app.dart

import 'package:polymer_elements/iron_icons.dart';
import 'package:polymer_elements/iron_icon.dart';
import 'package:polymer_elements/paper_item.dart';
import 'package:polymer_elements/paper_icon_item.dart';
import 'package:polymer_elements/paper_input.dart';
import 'package:polymer_elements/paper_card.dart';

Step 3.2: Add the Card

Wrapping the user inputs in a material design card will be a great way to set off this part of the app from the map. You'll use the paper card element for this.

Add the <paper-card> custom element below your map elements, but inside the <template> tag:

lib/main_app.html

<paper-card elevation="2">
</paper-card>

You can use elevation to control the card's shadow.

Step 3.3: Add the Inputs

Next you'll add inputs to set the start and end addresses of the <google-map-directions> element. The inputs will be couched within some paper icon item elements next to a search icon, just to be snazzy. This will also serve to lay out the inputs in a list formation.

Add this markup inside the <paper-card> element:

lib/main_app.html

<paper-icon-item tabindex="-1">
  <iron-icon icon="search" item-icon></iron-icon>
  <paper-input label="Start address" value="Orem"></paper-input>
</paper-icon-item>
<paper-icon-item tabindex="-1">
  <iron-icon icon="search" item-icon></iron-icon>
  <paper-input label="End address" value="SLC"></paper-input>
</paper-icon-item>

The <paper-icon-item> elements use tabindex="-1" to avoid taking keyboard focus, which would be disruptive when trying to tab between the input elements.

Each <paper-icon-item> contains an <iron-icon> element configured to display the search icon from the iron-icons default set. You imported the icon set and the element in Step 3.1 to make them available here. Adding the item-iconattribute helps the <paper-icon-item> identify the icon among its children.

The <paper-input> elements are typical text inputs, but styled with Google's material design principles. For now, they have hard-coded value attributes.

Step 3.4: Add the Styles and Take a Look

You need to add a little bit of CSS to position the card on the map.

Style It

Add these rules to the <style> tag inside your <main-app>:

lib/main_app.html

paper-card {
  position: absolute;
  bottom: 25px;
  left: 25px;
  z-index: 1;
}

This will position your <paper-card> near the lower-left corner of the display, atop the map.

Run It

Your new card should look something like this:

card

Step 4: Bind the Data

In the last step, you created search inputs for the map directions. In this step, you'll hook them up and make them work.

Step 4.1: Bind the Map

Right now, you've got some Dart code tying the map properties of <google-map> and <google-map-directions> together. It's time to do things the Polymer way, with data binding.

I'm Not Ready

Delete the ready() method that you worked so hard to paste into the MainApp class, as it will no longer be needed. It can be found in lib/main_app.dart.

The Map Element

Add a new attribute to your <google-map> instance:

lib/main_app.html

<google-map map="{{map}}" ...>

The curly brackets tell Polymer to sync its map attribute to a property, also called map, on your MainApp class in the <main-app> element's Dart file. This property is undeclared, so don't go looking for it. In fact, unless you have a need to manipulate this property in your Dart code in some way, there's no need to ever declare it. Even in a Dart project, Polymer is essentially a JavaScript-based system, so the property will be created and used behind the scenes automatically, as is normal behavior for JavaScript.

The Directions Element

Now you just need to sync <google-map-directions> the same way:

lib/main_app.html

<google-map-directions map="{{map}}" ...>
Run It

With ready() removed and the new bindings in place, if you run your app now, you should see no differences at all. The directions from Orem to Salt Lake City should still be visible on the map.

Step 4.2: Bind the Inputs

You will use a binding technique very much like that in Step 4.1 to bind the inputs and the directions elements together.

The Directions Element

Replace the hard-coded values of these attributes with bindings on <google-map-directions>:

lib/main_app.html

<google-map-directions start-address="{{start}}" end-address="{{end}}" ...>

Just like with the map property, this will cause start-address and end-address to stay synced with the undeclared start and end properties.

Start

Now bind the input value for the start address to start:

lib/main_app.html

<paper-input label="Start address" value="{{start}}"></paper-input>
End

And do the same for the end address:

lib/main_app.html

<paper-input label="End address" value="{{end}}"></paper-input>
Run It

Run the app. As you type in the input fields, valid entries will cause the map to update the directions display. Try values like "CA" and "NYC".

Note: As of this writing, a bug in <paper-icon-item> eats spaces, so you won't be able to type spaces in the inputs as long as they are contained in <paper-icon-item> elements. It is expected that this will be corrected soon.

Step 5: Travel Modes

The default travel mode for <google-map-directions> is for driving, but what if your user wants directions for walking or cycling? Polymer's paper elements make it easy to add a mode selector.

Step 5.1: Add a Mode Selector

You'll use the <paper-tabs> custom element and a new icon set for this step.

Import It

This should be routine by now.

lib/main_app.dart

import 'package:polymer_elements/maps_icons.dart';
import 'package:polymer_elements/paper_tabs.dart';

You import maps-icons to get access to Polymer's map icon set.

Use It

Next, add a set of tabs representing each of the travel modes. Place this markup after the <paper-icon-item> tags, but still inside of the <paper-card>:

lib/main_app.html

<paper-tabs selected="0">
  <paper-tab>
    <iron-icon icon="maps:directions-car"></iron-icon>
    <span>DRIVING</span>
  </paper-tab>
  <paper-tab>
    <iron-icon icon="maps:directions-walk"></iron-icon>
    <span>WALKING</span>
  </paper-tab>
  <paper-tab>
    <iron-icon icon="maps:directions-bike"></iron-icon>
    <span>BICYCLING</span>
  </paper-tab>
  <paper-tab>
    <iron-icon icon="maps:directions-transit"></iron-icon>
    <span>TRANSIT</span>
  </paper-tab>
</paper-tabs>

This kind of markup is where web components really shine. Creating a set of tabs with more traditional methods typically ends up looking like a giant mess of nested <div> tags with weird classes, IDs, and roles, which then has to be "activated" with some sort of JavaScript call. With Polymer, it's clear when you look at the markup what's going on, and most forms of activation are automatic.

You use the selected attribute to set a default selection. The tabs element indexes its children like an array, so the first <paper-tab> is child "0".

To use an icon from a set other than the default in <iron-icon>, you prefix the icon name with the name of its set, as in maps:directions-car.

Style It

The default style of <paper-tabs> uses a lot of yellow. Yellow highlights, yellow ink ripples...it's not that great. You should go ahead and make it a more pleasing material design blue. Add the following CSS to the main app's <style> section:

lib/main_app.html

paper-tabs {
  --paper-tabs-selection-bar-color: #0D47A1;
  margin-top: 16px;
}

paper-tab {
  --paper-tab-ink: #BBDEFB;
}

paper-tab iron-icon {
  margin-right: 10px;
}

paper-tab.iron-selected {
  background: rgb(66, 133, 244);
  color: white;
}

Polymer uses custom CSS properties to expose styling hooks, letting code outside a custom element customize its local DOM. The <paper-tabs> element happens to expose --paper-tabs-selection-bar-color for changing the color of—you guessed it—the selection bar. The docs for paper-tabs lists all the custom CSS properties that it makes available.

A selected tab gets assigned the iron-selected CSS class, so you can target a selected tab using the paper-tab.iron-selected selector.

Run It

You should now see a group of four tabs in the <paper-card>, each with an icon and a label.

card with tabs

You can click/tap the tabs to see the selection change, but of course, the map won't yet respond to your selection. For that, you need bindings!

The Card Keeps Shifting Around...

If selecting the different tabs causes the card to annoyingly change widths, you can correct this by adding a fixed width to the card. Add a rule like this to the paper-card CSS selector in your app's <style> area:

width: 490px;

Adjust the number as needed to stabilize your card.

Step 5.2: Bind It Up

To make the map do things in response to user selections, you need to bind the tabs to the directions element somehow. Once again, Polymer makes this almost too easy.

The Directions Element

You'll use a one-way data binding (using square brackets) on the directions element to keep its travel-mode attribute synced with an as-yet-undeclared travelMode property on your MainApp class. What this means is that setting travelMode will automatically set the directions element's travel-mode attribute, but not the reverse. Setting the travel-mode attribute by some other means will have no effect on MainApp's travelMode property.

lib/main_app.html

<google-map-directions travel-mode="[[travelMode]]" ...>

For a more detailed examination of Polymer Dart's data binding syntax, check out the docs.

The Tabs Element

You need to replace the selected attribute's value with a two-way binding on the tabs element, and add one extra attribute as well.

lib/main_app.html

<paper-tabs selected="{{travelMode}}" attr-for-selected="label">

Using attr-for-selected, you're telling <paper-tabs> that each individual <paper-tab> child will have a label attribute containing its selection value (not yet added). When a tab is selected, its label value will be assigned to the <paper-tabs> element's selected attribute. This overrides the element's default behavior of using a zero-based index to track selections.

The data binding syntax (selected="{{travelMode}}") tells Polymer to update a property on MainApp called travelMode with the value of selected whenever there is a change. This, in turn, activates the one-way data binding back to <google-map-directions>.

Essentially, this sets up <main-app> as a mediator, facilitating decoupled communication between <paper-tabs> and <google-map-directions>.

Tab Labels

Now to add a label attribute to each <paper-tab>:

lib/main_app.html

<paper-tab label="DRIVING">
...
<paper-tab label="WALKING">
...
<paper-tab label="BICYCLING">
...
<paper-tab label="TRANSIT">
...
Run It

Enter a start and end address, then click/tap the travel mode tabs to see the map change.

Just One Problem...

You might have noticed that even though the default directions are for driving, the DRIVING tab is no longer visibly selected when the app loads. That's because you no longer have selected="0" on the <paper-tabs> element. Instead, selected is bound to travelMode, an undeclared property of the MainApp class.

In order to set a default value for travelMode, you'll need to go ahead and declare it:

lib/main_app.dart

class MainApp extends PolymerElement {

  @property
  String travelMode = "DRIVING";
...

With this property defined in your Dart class and initialized to "DRIVING", you should now see the default selection once again. When you start the app, the DRIVING tab will be highlighted.

To learn more about properties and the @property annotation, take a look at the Polymer Dart Properties docs.

Step 6: There Is No Step 6

You did it! If you worked your way through this entire code lab, you're tougher and smarter than most, and you have a bright future. I hope you've seen that with Polymer Dart, it's criminally easy to build even very complex applications, since custom element authors basically do all your work for you. The unofficial Polymer motto is "There's an element for that." But when there's not, maybe you'll be the one to make it.

In addition to Polymer's own collections of elements, there are a few other custom element repositories worth a look: