Listing routes page
Fetch a list of bus routes from the CTA bus tracker API and add them to the .routes-list
element.
Overview
In this part we will:
- Fetch the list of routes from the API.
- Display the route name and number in the list.
Problem
Our bus-tracker component currently only includes a header and a map. We want to add a routes list so that eventually the user can select a route which will display within the map. At the end of this exercise we want a scrollable list of routes to be displayed.
Additionally we have text within the header that says Loading routes… statically. We want to remove this text after the routes have been rendered.
How to Solve This Problem
- Create a template using an
li
for each route. - Write a function that fetches the list of routes from the CTA bus tracker API.
- Loop over the routes and activate a template for each. Add the route number to the
.route-number
element, and the route name to the.route-name
element. - Append the DOM to the
.routes-list
list. - Remove the
#loading-routes
element since the routes are now loaded.
Technical requirements
The following snippet of JavaScript will be useful for fetching data from the bus tracker API. Use the getRoutesEndpoint
string to fetch the list of routes.
const apiRoot = "https://cta-bustracker.vercel.app/api/";
const getRoutesEndpoint = apiRoot + "routes";
const getVehiclesEndpoint = apiRoot + "vehicles";
To display the routes we want to create an <li>
for each route and attach it to the .routes-list
element. Use this markup to create that li. Inspect the results of the API request to figure out how to display the .route-number
and .route-name
appropriately.
<li>
<button type="button">
<span class="route-number"></span>
<span class="route-name"></span>
<span class="check">✔</span>
</button>
</li>
What you need to know
- How to use fetch to make API requests.
- Setting an element’s text.
Fetch
fetch
is a function on the window
object that is used to make network requests. In its simplest form it only needs a string URL, which will be used to make a GET request.
fetch differs slightly from the older XMLHttpRequest in a variety of ways; for example fetch does not include cookies by default. It’s easier to use, however, because it uses Promises. If you don’t need to support Internet Explorer you’ll probably want to use fetch in your applications.
fetch()
returns a Response object. To get a JSON object from this use response.json()
like so:
async function listFoods() {
let response = await fetch('/api/food');
let foods = await response.json();
let foodList = document.querySelector('.foods');
for(let food of foods) {
let li = document.createElement('li');
li.textContent = food;
foodList.append(li);
}
}
listFoods();
textContent
Every element has a .textContent
property. Setting this property to a string will render the text as children.
let el = document.createElement('h1');
document.body.append(el);
el.textContent = 'Hello from .textContent';
This is equivalent to creating Text nodes:
let el = document.createElement('h1');
document.body.append(el);
let text = document.createTextNode('Hello from a text node');
el.append(text);
setTimeout(() => {
text.data = 'This text node was modified';
}, 3000);
Usually you will use .textContent
unless building a library where performance is critical. textContent is the most convenient way to change an element’s text.
Solution
✏️ Use the markup provided above and create another template with the id of route-template
. Keep a reference to this template in your JavaScript along with the other template. Copy the URL snippet from above and paste that so that it can be used within the component.
Create a method on the component, we’re calling it getRoutes
here that is called in the connectedCallback
method. It’s an async method that uses fetch to retrieve the list of routes which we can get with data["bustime-response"].routes
.
Loop over the routes and clone an instance of the template filling in the route number with route.rt
and the route name with route.rtnm
.
At the end of the getRoutes
method remove the #loading-routes
element.
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="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');
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');
}
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;
this.routesList.append(frag);
}
this.shadowRoot.querySelector('#loading-routes').remove();
}
}
customElements.define("bus-tracker", BusTracker);
</script>