bus-tracker component page
Build a more complex component that combines the usage of <template>
and customElements
, and sets up event listeners.
Overview
In this part we will:
- Create another component named
bus-tracker
. - Display the
google-map-view
component within this new component.
Problem
We want to build a component that displays the CTA bus tracker routes and displays a map. This part builds the shell of the component and includes:
- A header with a title
- A placeholder for where the list of routes will go.
- The
google-map-view
component we created in the previous section.
How to Solve This Problem
- Create a template that contains the styles and markup for this new element.
- Create a custom element called
bus-tracker
. - Include the styles/markup in this element’s
shadowRoot
. - Place the
google-map-view
component within this new element’sfooter
.
Technical requirements
The styles for our bus-tracker
component should be contained within Shadow DOM. The CSS needed for the rest of the guide is:
/* CSS to copy */
: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;
}
The minimal markup needed for this component is:
<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>
</footer>
The google-map-view
component should be displayed in the <footer>
section of the component.
What you need to know
To solve this problem you’ll need to:
- Know how to use CSS in a component that uses Shadow DOM.
- Use a component within another component.
Using styles within a component
Styles added to a page via <link rel=stylesheet>
cannot be used within a shadowRoot. Services like CodePen that allow you to add CSS will add them this way. Instead place your styles within a <style>
tag inside of the shadowRoot.
<style>
.foo { display: block; }
</style>
The :host
selector is a way to style the host of a shadowRoot
. A common use-case is to change the display. By default custom elements have a display of inline
which is like a <span>
. Often authors prefer to have a block
display, they can change this in their shadowRoot with this CSS:
<style>
:host {
display: block;
}
</style>
In our component we are using display: flex
to give space to the header, route list, and Google map.
Nesting components
Any component defined using customElements.define()
can be used within another element’s shadowRoot
just as they can inside of the page’s HTML. To add the google-map-view
to this new component, place the tag within the footer.
<parent-element></parent-element>
<template id="child-template">
<h2>I am the child</h2>
</template>
<template id="parent-template">
<h1>I am the parent</h1>
<child-element></child-element>
</template>
<script type="module">
class ChildElement extends HTMLElement {
constructor() {
super();
let template = document.querySelector('#child-template');
let fragment = document.importNode(template.content, true);
this.attachShadow({ mode: 'open' });
this.shadowRoot.append(fragment);
}
}
customElements.define('child-element', ChildElement);
class ParentElement extends HTMLElement {
constructor() {
super();
let template = document.querySelector('#parent-template');
let fragment = document.importNode(template.content, true);
this.attachShadow({ mode: 'open' });
this.shadowRoot.append(fragment);
}
}
customElements.define('parent-element', ParentElement);
</script>
Solution
✏️ Create a new component called bus-tracker
. Use the CSS and markup provided and place them in a <template>
with the id of #bt-template
. Create a shadowRoot for this element and append a clone of the template.
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>
<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 btTemplate = document.querySelector('#bt-template');
class BusTracker extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
let frag = document.importNode(btTemplate.content, true);
this.shadowRoot.append(frag);
}
}
customElements.define("bus-tracker", BusTracker);
</script>