Selecting a route page
Learn how to use event listeners with a custom element to handle user interaction and update the DOM.
Overview
In this part we will:
- Handle selecting of routes.
- Fetch a list of vehicles for a selected route.
- Show the selected route name and number overlaying the map.
Problem
We have a list of routes and a map. We want to handle events when the button for a given route is clicked. It should fetch the vehicles for that route and log those to the console. In the next section we will use the vehicle list to display markers.
Additionally the route name and number should be shown overlaying the map. There is a button where this information can be added.
When a route is selected it should become active and show a checkmark by the name.
How to Solve This Problem
- Add a
click
event listener on thebutton
within each routeli
. - Create a method that will be called within the listener that receives the
route
object and theli
. - Create another method, this time an async method, that fetches the vehicles for the given
route
.- Add the route name and number to the
#selected-route
button. - If there is an error also append the error message snippet to that same button.
console.log
the list of vehicles. We will use them in the next section.- Add the
route-selected
class to the#selected-route
button once you have the vehicles.
- Add the route name and number to the
- Add the
active
class to the route’sli
. If there is already an active route, remove theactive
class from thatli
.
Technical requirements
Selected route button
This HTML can be used to display the route name and number. It should be a child of the #selected-route
button.
<small>Route NUMBER:</small> NAME
Inspect the route
object to see what the values for NUMBER and NAME should be.
Some routes may contain errors. When this happens use this markup in addition to showing the route number and name as described above:
<div class="error-message">
No vehicles available for this route
</div>
To make the #selected-route
button visible, add the route-selected
class.
Calling the API
To get the URL for this API request use:
getVehiclesEndpoint + '?rt=' + NUMBER
Where NUMBER
is the route number, which again can be gotten from the route object.
Marking the route active
Set the active
class on the li
element associated with the route to mark it as active.
What you need to know
This part doesn’t have any new web component APIs to learn, but builds off what we’ve already done. You will need to know a few basic DOM APIs however:
- How to add event listeners to elements.
- How to add classes to elements.
Event listeners
addEventListener is a method on elements that takes an event name, and a callback function. When the event occurs the callback function will be called.
Each route in our list contains a button
element. Set the event listener on this element.
element.addEventListener('click', ev => {
// Do whatever you need.
});
Adding classes
Each element contains a classList
object which is a little like a JavaScript Set
(but is subtley different). To add classes to an element use classList.add()
.
element.classList.add('some-new-class');
Solution
✏️ In the getRoutes
method inside of the routes
loop, get a reference to the button
for the route and add an event listener for click events. When that event occurs call a method that will call the API for vehicles and add the active
class to the associated li
.
Add a reference to the #selected-route
element in the constructor. When the API call completes update this element as needed and add the route-selected
class so that it will be highlighted.
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);
}
connectedCallback() {
let gmap = this.shadowRoot.querySelector('.gmap');
this.map = new google.maps.Map(gmap, {
zoom: 10,
center: {
lat: 41.881,
lng: -87.623
}
});
}
}
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');
}
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);
} else {
let vehicles = data['bustime-response'].vehicle;
console.log(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>