Display markers for vehicles page
Learn how to listen to changes in properties on your custom elements and how to properly reflect back the property values to the outside world.
Overview
In this part we will:
- Update the
<google-map-view>
component to display markers. - Receive an array of vehicles as a property from the
<bus-tracker>
parent component. - Add a marker for each vehicle using the embedded Googles Maps widget.
- Reflect back the array of vehicles and array of markers as properties on the
<google-map-view>
.
Problem
In the previous exercise we fetched a list of vehicles when a route is selected and logged them to the console. Now we want to instead pass those to the google-map-view
element. The google-map-view
element should add a marker for each vehicle.
When an error occurs in the API it should wipe away any existing markers.
How to Solve This Problem
- Update the
bus-tracker
component to pass the vehicles list to thegoogle-map-view
via thevehicles
property. - Add a getter/setter pair on the
google-map-view
to handle.vehicles
. When set it should use the Marker snippet (below) to create a new marker for each vehicle. - When a route is selected and markers are already displayed for a previous route, remove the previous markers.
Technical requirements
To create a new marker use new google.maps.Marker
. This takes an object with some options that look like this:
new google.maps.Marker({
position: {
lat: latitude,
lng: longitude
},
map: googleMapObject
});
In this case map
is the thing we created in a previous exercise by calling new google.maps.Map
.
Additionally this snippet can be used to remove a marker:
marker.setMap(null);
What you need to know
- How to use JavaScript getters and setters to handle dynamic property values.
- How to use default values in custom elements.
getters/setters
JavaScript setters allow observation of property changes. Adding a setter for vehicles
provides a hook for when the <bus-tracker>
component passes the array of vehicles for the selected route.
In order to reflect back the list of vehicles in a getter you can save the vehicle list to another property on the element (like an underscore property). It’s common when a setter exists that a getter does as well.
This is an example of a getter/setter pair in a JavaScript class.
class Person {
set age(val) {
console.log('Setting age');
this._age = val;
}
get age() {
console.log('Getting age');
return this._age;
}
}
let kid = new Person();
kid.age = 4;
console.log(kid.age);
We can use getters/setters within custom element classes as well.
<my-counter></my-counter>
<script type="module">
class CounterElement extends HTMLElement {
constructor() {
super();
this._count = 0;
this.render();
}
render() {
this.innerHTML = `Count: ${this.count}`;
}
get count() {
return this._count;
}
set count(value) {
this._count = value;
this.render();
}
}
customElements.define('my-counter', CounterElement);
let counter = document.querySelector('my-counter');
setTimeout(() => counter.count++, 5000);
setTimeout(() => counter.count = 15, 10000);
setTimeout(() => counter.count--, 15000);
</script>
Default values
Most properties supported by built-in elements have some sort of default value. For example the <progress> element has a max
property that defaults to 1
: document.createElement('progress').max; // 1
. All elements have an onclick
property whose default value is null
. It’s good practice to provide default values for your supported public properties, and these can be set in the constructor. Combining getters, setters and default values for properties makes your component more robust.
class DogElement extends HTMLElement {
constructor() {
super();
this._breed = null;
}
get breed() {
return this._breed;
}
set breed(val) {
this._breed = val;
}
}
Solution
✏️ Add default values for markers
and vehicles
in the constructor (use an underscore property for vehicles). Add a getter/setter pair for vehicles
, where the setter creates new markers on the map with new google.maps.Marker
.
Click to see the solution
<style>
html,
body {
height: 100%;
}
body {
font-family: "Catamaran", sans-serif;
background-color: #f2f2f2;
display: flex;
flex-direction: column;
margin: 0;
}
</style>
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyD7POAQA-i16Vws48h4yRFVGBZzIExOAJI"></script>
<bus-tracker></bus-tracker>
<template id="bt-template">
<style>
:host {
display: flex;
flex-direction: column;
}
.top {
flex-grow: 1;
overflow-y: auto;
height: 10%;
display: flex;
flex-direction: column;
}
footer {
height: 250px;
position: relative;
}
.gmap {
width: 100%;
height: 250px;
background-color: grey;
}
header {
box-shadow: 0px 3px 5px 0px rgba(0, 0, 0, 0.1);
background-color: #313131;
color: white;
min-height: 60px;
display: flex;
flex-direction: column;
justify-content: center;
line-height: 1.2;
}
header h1 {
text-align: center;
font-size: 18px;
text-transform: uppercase;
letter-spacing: 1px;
margin: 0;
}
#selected-route:not(.route-selected) {
display: none;
}
.route-selected {
line-height: 1;
position: absolute;
z-index: 1;
text-align: right;
background: rgba(6, 6, 6, 0.6);
top: 10px;
right: 10px;
padding: 6px 10px;
color: white;
border-radius: 2px;
cursor: pointer;
}
.route-selected small {
display: block;
font-size: 14px;
color: #ddd;
}
.route-selected .error-message {
font-size: 14px;
background-color: #ff5722;
border-radius: 10px;
padding: 4px 8px 1px;
margin-top: 5px;
}
.routes-list {
padding: 20px 0;
margin: 0;
overflow-y: auto;
}
.routes-list li {
list-style: none;
cursor: pointer;
background: white;
border: 1px solid #dedede;
margin: 1% 2%;
border-radius: 25px;
color: #2196f3;
width: 41%;
display: inline-flex;
font-size: 14px;
line-height: 1.2;
}
.routes-list li:hover {
border-color: transparent;
background-color: #008eff;
color: white;
box-shadow: 0px 5px 20px 0px rgba(0, 0, 0, 0.2);
}
.routes-list li .check {
display: none;
}
.routes-list li.active {
color: #666;
background-color: #e8e8e8;
}
.routes-list li.active .check {
display: inline-block;
margin-left: 5px;
color: #2cc532;
}
.routes-list li.active:hover {
border-color: #dedede;
box-shadow: none;
}
.routes-list button {
width: 100%;
padding: 8px 8px 6px;
border: none;
border-radius: 25px;
background: transparent;
text-align: left;
font: inherit;
color: inherit;
}
.route-number {
display: inline-block;
border-right: 1px solid #dedede;
padding-right: 5px;
margin-right: 5px;
min-width: 18px;
text-align: right;
}
p {
text-align: center;
margin: 0;
color: #ccc;
font-size: 14px;
}
</style>
<div class="top">
<header>
<h1>Chicago CTA Bus Tracker</h1>
<p id="loading-routes">Loading routes…</p>
</header>
<ul class="routes-list"></ul>
</div>
<footer>
<button id="selected-route" type="button">
</button>
<google-map-view></google-map-view>
</footer>
</template>
<template id="error-template">
<div class="error-message">
No vehicles available for this route
</div>
</template>
<template id="gmap-template">
<style>
.gmap {
width: 100%;
height: 250px;
background-color: grey;
}
</style>
<div class="gmap"></div>
</template>
<template id="route-template">
<li>
<button type="button">
<span class="route-number"></span>
<span class="route-name"></span>
<span class="check">✔</span>
</button>
</li>
</template>
<script type="module">
const template = document.querySelector('#gmap-template');
class GoogleMapView extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
let nodes = document.importNode(template.content, true);
this.shadowRoot.append(nodes);
this.markers = null;
this._vehicles = null;
}
connectedCallback() {
let gmap = this.shadowRoot.querySelector('.gmap');
this.map = new google.maps.Map(gmap, {
zoom: 10,
center: {
lat: 41.881,
lng: -87.623
}
});
}
get vehicles() {
return this._vehicles;
}
set vehicles(newVehicles) {
this._vehicles = newVehicles;
if (this.markers) {
for(let marker of this.markers) {
marker.setMap(null);
}
this.markers = null;
}
if (newVehicles) {
this.markers = newVehicles.map(vehicle => {
return new google.maps.Marker({
position: {
lat: parseFloat(vehicle.lat),
lng: parseFloat(vehicle.lon)
},
map: this.map
});
});
}
}
}
customElements.define('google-map-view', GoogleMapView);
const apiRoot = "https://cta-bustracker.vercel.app/api/";
const getRoutesEndpoint = apiRoot + "routes";
const getVehiclesEndpoint = apiRoot + "vehicles";
const btTemplate = document.querySelector('#bt-template');
const routeTemplate = document.querySelector('#route-template');
const errorTemplate = document.querySelector('#error-template');
class BusTracker extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
let frag = document.importNode(btTemplate.content, true);
this.shadowRoot.append(frag);
this.routesList = this.shadowRoot.querySelector('.routes-list');
this.selectedRouteBtn = this.shadowRoot.querySelector('#selected-route');
this.googleMapView = this.shadowRoot.querySelector('google-map-view');
}
connectedCallback() {
this.getRoutes();
}
async getRoutes() {
let response = await fetch(getRoutesEndpoint);
let data = await response.json();
let routes = data["bustime-response"].routes;
for(let route of routes) {
let frag = document.importNode(routeTemplate.content, true);
frag.querySelector('.route-number').textContent = route.rt;
frag.querySelector('.route-name').textContent = route.rtnm;
frag.querySelector('button').addEventListener('click', ev => {
this.pickRoute(route, ev.currentTarget.parentNode);
});
this.routesList.append(frag);
}
this.shadowRoot.querySelector('#loading-routes').remove();
}
async getVehicles(route) {
let response = await fetch(getVehiclesEndpoint + '?rt=' + route.rt);
let data = await response.json();
this.selectedRouteBtn.innerHTML = `
<small>Route ${this.route.rt}:</small> ${this.route.rtnm}
`;
if (data['bustime-response'].error) {
let frag = document.importNode(errorTemplate.content, true);
this.selectedRouteBtn.append(frag);
this.googleMapView.vehicles = [];
} else {
let vehicles = data['bustime-response'].vehicle;
this.googleMapView.vehicles = vehicles;
}
this.selectedRouteBtn.classList.add('route-selected');
}
pickRoute(route, li) {
this.route = route;
this.getVehicles(route);
if(this.activeRoute) {
this.activeRoute.classList.remove('active');
}
this.activeRoute = li;
this.activeRoute.classList.add('active');
}
}
customElements.define("bus-tracker", BusTracker);
</script>