In this codelab, you'll create a fully working Google Maps app using elements in Polymer's Google Web Components collection. The app will be responsive and will include driving directions and transit mode. Along the way, you'll also learn about Polymer's data-binding features and iron element set.
$ npm install -g polymer-cli
$ git clone https://github.com/ComcastSamples/polymer-maps.git
$ cd polymer-maps
$ npm install
When you run $ npm install
, npm will download and install a list of dependencies (including the Polymer core library) into the node_modules
/ folder. Fetching the components may take some time if your Internet connection is slow.
Your folder structure should contain these files (among a few others):
polymer-maps/
node_modules/ <!-- installed dependencies from npm -->
index.html <!-- your app -->
package.json <!-- npm metadata file used for managing dependencies -->
styles.css
Using the Polymer CLI we installed earlier, start up a local dev server with
$ polymer serve
Once that command prints out the folllowing, your server is running:
info: [cli.command.serve] Files in this directory are available under the following URLs
applications: http://127.0.0.1:8081
reusable components: http://127.0.0.1:8081/components/polymer-maps/
Once the server is running, navigate to http://127.0.0.1:8081 to view your app. This is a great way to preview changes as you make them.
At this point the app doesn't do much. Let's add a map!
The Google Web Components collection provides the <google-map>
element for declaratively rendering a Google Map. To use it, you first need to install it using npm.
You can verify that <google-map>
(and any dependencies) was installed by checking that node_modules/@johnriv/google-map/
was created and populated.
To use the map element, you'll need to obtain an API key for the Google Maps API. Visit https://developers.google.com/maps/documentation/javascript/get-api-key and copy your key. We'll use it in the next couple steps.
Be on the lookout for YOUR_KEY
in the code samples. When you encounter it, be sure to replace YOUR_KEY
with your actual Maps API key.
To employ <google-map>
, you need to:
index.html
.<google-map>
to have a height so the map appears (by default, the component will fill the height of its container)Since we know you're going to want to reload the page to see the map as soon as you add the <google-map>
markup to your page, we'll actually do Step 3, styling the container of the <google-map>
, first.
In order for the map to properly display itself once we add it, you need to set its container (in this case, <body>
) to have a fixed height.
Open styles.css
and replace its contents with this default styling:
body, html {
font-family: 'Roboto', Arial, sans-serif;
height: 100%;
margin: 0;
}
In index.html
, replace the <!-- JavaScript Imports go here. Format: <script type="module" src=""></script> -->
HTML comment in the <head>
and replace it with a single import that loads google-map.js
:
<head>
....
<script src="node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js"></script>
<script type="module" src="node_modules/@johnriv/google-map/google-map.js"></script>
<link rel="stylesheet" href="styles.css">
</head>
Next, replace the contents of <body>
with an instance of <google-map>
:
<body>
<google-map latitude="37.779" longitude="-122.3892"
api-key="YOUR_KEY" zoom="13" disable-default-ui></google-map>
</body>
As you can see, using <google-map>
is completely declarative! The map is centered using the latitude
and longitude
attributes and its zoom level is set by the zoom
attribute. We've also specified our API key for the Maps API using the api-key
attribute.
Also note that we've disabled the map's controls by adding the disable-default-ui
attribute. Since disableDefaultUi
is a boolean property, its presence as an HTML attribute makes it truthy. In Polymer, properties are camelCased while their equivalent HTML attributes are kebab-cased.
<google-map>
supports adding map markers to the map by declaring <google-map-marker>
elements as children. The marker locations are also set using latitude
and longitude
attributes.
Back in index.html, add a draggable <google-map-marker>
to the map:
<google-map latitude="37.779" longitude="-122.3892"
api-key="YOUR_KEY" zoom="13" disable-default-ui>
<google-map-marker latitude="37.779" longitude="-122.3892"
title="Go Giants!" draggable="true"></google-map-marker>
</google-map>
If you haven't already done so, navigate to http://127.0.0.1:8081/. At this point, you should see a map that takes up the entire viewport and has a single marker pin.
We'll add driving directions!
The <google-map-directions>
element was installed alongside <google-map>
. It provides driving direction information using the Google Maps API.
To employ <google-map-directions>
:
index.html
.In index.html
, add an ES Module Import for google-map-directions.html
:
<head>
....
<script src="node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js"></script>
<script type="module" src="node_modules/@johnriv/google-map/google-map.js"></script>
<script type="module" src="node_modules/@johnriv/google-map/google-map-directions.js"></script>
</head>
Declare <google-map-directions>
as a sibling of <google-map>
. Set its start-address
attribute to "San Francisco" and the end-address
to "Mountain View".
<body>
<google-map latitude="37.779" longitude="-122.3892"
api-key="YOUR_KEY" zoom="13" disable-default-ui>
...
</google-map>
<google-map-directions
start-address="San Francisco"
end-address="Mountain View"
api-key="YOUR_KEY"></google-map-directions>
</body>
<google-map-directions>
fetches directions, but it isn't that useful by itself. You need to connect the results from <google-map-directions>
to <google-map>
so the directions render on the map.
Both elements expose a .map
property that allow users to access/set an underlying Map
object (used by the Google Maps JavaScript API). To get these two elements talking, set them to use the same Map
object.
At the bottom of index.html, set the directions element to share the map:
index.html
...
<script>
(function() {
var gMap = document.querySelector('google-map');
var gMapDir = document.querySelector('google-map-directions');
var bindMaps = function() {
gMapDir.map = gMap.map;
};
if (gMap.map) {
bindMaps();
} else {
gMap.addEventListener('api-load', function(e) {
bindMaps();
});
}
}());
</script>
</body>
Note: Since we don't know if the map has been loaded yet, we check if it has loaded first, and then if it has not, we wait for the map element to fire its api-load
event to ensure that the map has been loaded before we share the map property from the map with the directions.
$ polymer serve
.At this point, you should see the map, a marker, and added driving directions between San Francisco and Mountain View.
Learn Polymer's data-binding features and allow users to input their own start and end addresses.
Let's add input fields to allow users to input the start and end addresses. Polymer provides several elements for working with input: paper-card, paper-item, paper-input
, iron-icon
, and iron-icons
.
In index.html, add new ES Module Imports for these components:
<head>
....
<script type="module" src="node_modules/@polymer/iron-icon/iron-icon.js"></script>
<script type="module" src="node_modules/@polymer/iron-icons/iron-icons.js"></script>
<script type="module" src="node_modules/@polymer/paper-item/paper-item.js"></script>
<script type="module" src="node_modules/@polymer/paper-item/paper-icon-item.js"></script>
<script type="module" src="node_modules/@polymer/paper-input/paper-input.js"></script>
<script type="module" src="node_modules/@polymer/paper-card/paper-card.js"></script>
</head>
At the bottom of the page, declare two <paper-icon-item>
elements and a material design input for the start and end addresses. <paper-icon-item>
is a convenience element for making an item with an icon.
<paper-icon-item>
<paper-input label="Start address" value="San Francisco"></paper-input>
</paper-icon-item>
<paper-icon-item>
<paper-input label="End address" value="Mountain View"></paper-input>
</paper-icon-item>
<iron-icon>
is an element useful for displaying an icon. It takes an icon
attribute, which references an icon from Polymer's default icon sets.
<iron-icon>
element.icon
attribute to icon="search"
.<paper-icon-item>
<iron-icon icon="search" slot="item-icon"></iron-icon>
<paper-input label="Start address" value="San Francisco"></paper-input>
</paper-icon-item>
<paper-icon-item>
<iron-icon icon="search" slot="item-icon"></iron-icon>
<paper-input label="End address" value="Mountain View"></paper-input>
</paper-icon-item>
Wrapping our inputs in a material design <paper-card>
is a great way to highlight this part of the app.
<paper-card elevation="2"> <!-- elevation controls the amount of box shadow -->
<paper-icon-item>
...
</paper-icon-item>
<paper-icon-item>
...
</paper-icon-item>
</paper-card>
In styles.css, add default styling to the card so it displays in the lower left corner, above the map.
paper-card {
position: absolute;
bottom: 25px;
left: 25px;
z-index: 1;
}
Use Polymer's data binding features to drive the driving directions from the address inputs.
In the last step, we created search inputs for the driving directions. In this step of the codelab, we'll hook them up to power <google-map-directions>
.
Polymer's data-binding features are only available when creating a <dom-module>
inside a template managed by Polymer.
To use Polymer bindings for elements placed in the main document, use the <dom-bind>
element.
Essentially, it allows you to use Polymer sugaring features outside of Polymer. For example, it allows you to use {{}}
bindings with elements in your main page.
Properties defined on an <dom-bind>
can be data bound. For example:
<dom-bind id="t">
<template>
<!-- Hello, Eric. How are you today? -->
Hello, <span>{{name}}</span>. <span>{{greeting}}</span>
</template>
</dom-bind>
<script>
var t = document.querySelector('#t');
t.name = 'Eric';
t.greeting = 'How are you today?';
</script>
Would bind the name
and greeting
properties to the values assigned on the template.
In the last step, you wrote JavaScript to set the direction's map
property:
var gMap = document.querySelector('google-map');
var gMapDir = document.querySelector('google-map-directions');
var bindMaps = function() {
gMapDir.map = gMap.map;
};
Both the maps and directions elements declare a map
property in their properties
object. This means you can use an attribute of the same name to data-bind the the two properties together.
<script>
you added in Step 4.<body>
inside a <dom-bind><template>...</template></dom-bind>
.map
attribute of <google-map>
to the map
attribute of <google-map-directions>
. Make sure this is done inside <dom-bind><template>...</template></dom-bind>
. This will bind the elements' map
properties together. When one changes, the other will too.<dom-bind>
<template>
<google-map map="{{map}}" ...></google-map>
<google-map-directions map="{{map}}"...></google-map-directions>
</template>
</dom-bind>
Note: We've used {{map}}
as the binding property name, but you can use whatever name you like (for example, map="{{foo}}")
.
Right now, the startAddress
and endAddress
properties are hardcoded to "San Francisco" and "Mountain View", respectively.
<google-map-directions
start-address="San Francisco" end-address="Mountain View"
api-key="YOUR_KEY">
</google-map-directions>
Likewise, the address inputs are hardcoded:
<paper-input label="Start address" value="San Francisco"></paper-input>
<paper-input label="End address" value="Mountain View"></paper-input>
We can make things more dynamic by binding these inputs to the attributes of the <google-map-directions>
.
Bind the startAddress
and endAddress
to the value
of the appropriate input:
<dom-bind>
<template>
<google-map map="{{map}}" latitude="37.779" longitude="-122.3892"
api-key="YOUR_KEY" disable-default-ui zoom="13">
<google-map-marker latitude="37.779" longitude="-122.3892"
title="Go Giants!" draggable="true"></google-map-marker>
</google-map>
<google-map-directions map="{{map}}"
start-address="{{start}}"
end-address="{{end}}"
api-key="YOUR_KEY"></google-map-directions>
<paper-card elevation="2">
<paper-icon-item>
<iron-icon icon="search" slot="item-icon"></iron-icon>
<paper-input label="Start address"
value="{{start}}"></paper-input>
</paper-icon-item>
<paper-icon-item>
<iron-icon icon="search" slot="item-icon"></iron-icon>
<paper-input label="End address"
value="{{end}}"></paper-input>
</paper-icon-item>
</paper-card>
</template>
</dom-bind>
The name you use in each {{}}
doesn't matter, as long as the binding names match for each pair of properties you want to bind together.
Also note we've wrapped all of the elements in a single <dom-bind><template>...</template></dom-bind>
.
$ polymer serve
.You should see the map update itself with driving directions from California to New York:
Try entering other destinations! You should see the map update as new destinations are entered.
Allow users to select the type of directions (walking, transit, driving).
In index.html, add the following ES Module Imports to the <head>
:
<head>
....
<script type="module" src="node_modules/@polymer/iron-icons/maps-icons.js"></script>
<script type="module" src="node_modules/@polymer/paper-tabs/paper-tabs.js"></script>
</head>
Note: maps-icons
is required to load Polymer's map iconset. You'll use it later to render icons for the travel modes.
We'll use <paper-tabs>
to allow users to select their travel type. Each transit mode will also use an icon from the maps
icon set instead of the default one. To use one of the other icon sets, the icon
attribute takes the form: <iconset>:<name>
, where <iconset>
is the name of the icon set (e.g. "maps") and <name>
is the name of the icon from that set.
As an example, the following code places the directions-car
icon from the maps
icon set:
<iron-icon icon="maps:directions-car" item-icon></iron-icon>
The <google-map-directions>
element contains a travelMode
property for specifying
the type of directions to render. It has four possible values: "DRIVING", "WALKING", "BICYCLING", and "TRANSIT".
<paper-card>
container, add a <paper-tabs>
with the travel mode options.<iron-icon>
and <span>
containing helper text.<paper-card elevation="2">
...
<!-- selected="0" selects the first item in the tab list.
Change it to another index if you want a different default. -->
<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>
</paper-card>
<paper-tabs>
is styled by default. It has a yellow material design ink effect and selection bar indication the selected tab. We can make it nicer!
Polymer uses CSS custom properties to expose styling hooks for internal parts of a component's Shady DOM. For example, paper-tabs exposes --paper-tabs-selection-bar-color
to colorize the tab's selection bar.
CSS custom properties are coming natively to browsers. Until they do, Polymer provides <custom-style>
, a custom element that wraps the <style>
tag and teaches it the custom properties styling system. To use this element, define it in the <head>
of your main page:
<head>
...
<custom-style>
<style>
paper-tabs {
--paper-tabs-selection-bar-color: #0D47A1;
margin-top: 16px;
}
paper-tab {
--paper-tab-ink: #BBDEFB;
}
/* Other styles that make things more pleasant.
These could instead be added in styles.css since they
do not use any Polymer styling features. */
paper-tab iron-icon {
margin-right: 10px;
}
paper-tab.iron-selected {
background: rgb(66, 133, 244);
color: white;
}
</style>
</custom-style>
</head>
This bit of code will colorize the selection bar and ink ripple effect a nice material design blue.
The last thing you need to do is data-bind the travelMode
property of <google-map-directions>
to <paper-tabs>
's .selected
property. The tabs updates this property whenever a new child item is selected. By default, its set to the index of the selected item. We need to override this in order to bind to the string value of each direction type. The attr-for-selected
property does just that.
Bind the direction's travelMode
property to the tab's selected
property:
<google-map-directions map="{{map}}" api-key="YOUR_KEY"
start-address="{{start}}"
end-address="{{end}}"
travel-mode="[[travelMode]]"></google-map-directions>
...
<paper-card>
<paper-tabs selected="{{travelMode}}" attr-for-selected="label">
<paper-tab label="DRIVING">
<iron-icon icon="maps:directions-car"></iron-icon>
<span>DRIVING</span>
</paper-tab>
<paper-tab label="WALKING">
<iron-icon icon="maps:directions-walk"></iron-icon>
<span>WALKING</span>
</paper-tab>
<paper-tab label="BICYCLING">
<iron-icon icon="maps:directions-bike"></iron-icon>
<span>BICYCLING</span>
</paper-tab>
<paper-tab label="TRANSIT">
<iron-icon icon="maps:directions-transit"></iron-icon>
<span>TRANSIT</span>
</paper-tab>
</paper-tabs>
</paper-card>
$ polymer serve
.The map should automatically update to show different forms of travel:
You built an entire maps application complete with driving directions, and all without writing a single line of code!
Think about that for a second. This just goes to show you the power of web components. They're reusable and composable. Zest in Polymer's data-binding features and you can create an entire app using nothing but declarative markup.