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.

A scrollable list of bus routes on top, a map on the bottom.

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

  1. Create a template using an li for each route.
  2. Write a function that fetches the list of routes from the CTA bus tracker API.
  3. 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.
  4. Append the DOM to the .routes-list list.
  5. 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>