COVID-19 tracker in LWC

What's the best way to spend the weekend when the country is in lockdown mode - write some cool Lightning components. Over the weekend, I have created a single page application in LWC which gives you the live numbers of how many people are affected by the COVID-19 pandemic worldwide. In a single view, the application gives you an overview of how many people are affected globally. It also gives you the breakdown of the numbers on a per-country basis. You can filter the information by providing the desired country name. Also, besides each country's name is an info icon, which gives you more details for that specific country.

You can install this application using the following unmanaged package: https://login.salesforce.com/packaging/installPackage.apexp?p0=04t6F000004LDId
A live demo of the application is hosted on the following force.com site: https://covid19-info-developer-edition.ap17.force.com/statistics . Please note that this is a developer account and it allows only a limited number of hits per day, so you may get an exception if the limit is already consumed for the day.
The source of data is a web service that is created by a group of developers and hosted on Github.
Now let us look at the architecture of the application. I have followed a design pattern in which the parent component (sometimes called a container component) is responsible for fetching the data. The child components have minimal logic and are mainly used to display the data. Any required data manipulation is done by the parent component and the modified data is again passed back to the child component which re-renders itself on change of data. The need for data change by child components is communicated with the parent component using events. The application is composed of 3 components:
  • covidInfo: This is the top-level component of the application whose role is to do a web service call and fetch the required data. Once the data is fetched, it massages the data and feeds it the child components.
  • covidCounts: This component displays the aggregated count. This is a display only component and does not contain any business logic.
  • covidTable: This component displays the data on a per-country basis in a tabular format. This component also has the functionality to filter the table. 
  • covidCountryInfo: This component is displayed when the info icon present next to a country's name is clicked. This component displays the counts specific to a given country.
Let's have a look at the code and try to understand how this application works.
covidInfo.html
<template>
<template if:true={covidCounts.cases}>
<div class="slds-text-heading_large slds-text-align_center">
Covid-19 Worldwide Statistics
</div>
<!--Covid Numbers-->
<c-covid-counts covid-counts={covidCounts}></c-covid-counts>
<!--Covid country specific info-->
<c-covid-country-info
country-name={countryName}
country-covid-info={countryCovidInfo}
></c-covid-country-info>
<!--Covid Table-->
<c-covid-table
covid-info={_covidInfo}
onfilter={filterCovidInfo}
onmoreinfo={displayMoreInfo}
></c-covid-table>
</template>
</template>
view raw covidInfo.html hosted with ❤ by GitHub
covidInfo.js
When the element is inserted into the document, the connectedCallback method is automatically called. We make a web service call in this method using the fetch API provided by JavaScript. On completion of the web service call, we iterate over the response array and aggregate the information which is being passed to the child component covidCounts in the form of an object.
import { LightningElement } from "lwc";
export default class CovidInfo extends LightningElement {
// stores aggregated counts
covidCounts = {};
// stores the response returned from webservice
covidInfo;
// private property - used in filter functionality
_covidInfo;
// used to store the corresponding country name when info icon is clicked
countryName;
// filter version of covidInfo array - contains information specific to a country
countryCovidInfo = {};
// executed on component load
connectedCallback() {
let totalCases = 0;
let totalDeaths = 0;
let totalRecovered = 0;
let todaysTotalCases = 0;
let todaysTotalDeaths = 0;
// make a werbservice call to get the data
fetch("https://corona.lmao.ninja/countries")
.then(function(response) {
return response.json();
})
.then(responseJSON => {
this.covidInfo = responseJSON;
this._covidInfo = responseJSON;
// iterate over the response array and aggreate the counts
this.covidInfo.forEach(function(item, index) {
totalCases = totalCases + item.cases;
totalDeaths = totalDeaths + item.deaths;
totalRecovered = totalRecovered + item.recovered;
todaysTotalCases = todaysTotalCases + item.todayCases;
todaysTotalDeaths = todaysTotalDeaths + item.todayDeaths;
});
// using spread operator to add new properties to the object
this.covidCounts = {
...this.covidCounts,
cases: totalCases,
deaths: totalDeaths,
recovered: totalRecovered,
todaysCases: todaysTotalCases,
todaysDeaths: todaysTotalDeaths
};
});
}
// executed when user types something in the country filter
// handles the event thrown by covidTable component
filterCovidInfo(event) {
const searchKey = event.detail ? event.detail.toLowerCase() : event.detail;
this._covidInfo = this.covidInfo.filter(info =>
info.country.toLowerCase().includes(searchKey)
);
}
// executed when user clicks on the info icon
// handles the event thrown by covidTable component
displayMoreInfo(event) {
this.countryName = event.detail;
this.countryCovidInfo = this.covidInfo.find(
item => item.country === this.countryName
);
}
}
view raw covidInfo.js hosted with ❤ by GitHub
covidCounts.html
This component displays the data which is fed by the parent component covidInfo.
<template>
<div class="c-container">
<lightning-layout class="c-layout">
<lightning-layout-item
padding="around-small"
class="total-cases slds-text-align_center"
flexibility="auto"
>
<div class="header-column">
<p class="field-title" title="Total Cases">Total Cases</p>
<p class="field-value">
<lightning-formatted-number
value={covidCounts.cases}
></lightning-formatted-number>
</p>
</div>
</lightning-layout-item>
<lightning-layout-item
padding="around-small"
class="total-deaths slds-text-align_center"
flexibility="auto"
>
<div class="header-column">
<p class="field-title" title="Total Deaths">Total Deaths</p>
<p class="field-value">
<lightning-formatted-number
value={covidCounts.deaths}
></lightning-formatted-number>
</p>
</div>
</lightning-layout-item>
<lightning-layout-item
padding="around-small"
class="total-recovered slds-text-align_center"
flexibility="auto"
>
<div class="header-column">
<p class="field-title" title="Total Cases">Total Recovered</p>
<p class="field-value">
<lightning-formatted-number
value={covidCounts.recovered}
></lightning-formatted-number>
</p>
</div>
</lightning-layout-item>
</lightning-layout>
<lightning-layout class="c-layout">
<lightning-layout-item
padding="around-small"
class="total-cases slds-text-align_center"
flexibility="auto"
>
<div class="header-column">
<p class="field-title" title="Total Cases">Today's Cases</p>
<p class="field-value">
<lightning-formatted-number
value={covidCounts.todaysCases}
></lightning-formatted-number>
</p>
</div>
</lightning-layout-item>
<lightning-layout-item
padding="around-small"
class="total-deaths slds-text-align_center"
flexibility="auto"
>
<div class="header-column">
<p class="field-title" title="Total Deaths">Today's Deaths</p>
<p class="field-value">
<lightning-formatted-number
value={covidCounts.todaysDeaths}
></lightning-formatted-number>
</p>
</div>
</lightning-layout-item>
</lightning-layout>
</div>
</template>
covidCounts.js
import { LightningElement, api } from 'lwc';
export default class CovidCounts extends LightningElement {
@api covidCounts = {};
}
view raw covidCounts.js hosted with ❤ by GitHub
covidCounts.css
.c-container {
border: 1px solid #d8dde6;
margin: 10px 0 20px 0;
}
.header-column {
padding: 0 2rem;
}
.field-title {
font-family: "Trebuchet MS", Arial, Helvetica, sans-serif;
font-size: 0.75rem;
line-height: 1.25;
margin-bottom: 0.25rem;
color: white;
}
.field-value {
font-family: "Trebuchet MS", Arial, Helvetica, sans-serif;
font-size: 1rem;
line-height: 1.25;
margin-bottom: 0.25rem;
color: white;
}
.total-cases {
background-color: rgb(21, 137, 238);
border: thin;
}
.total-deaths {
background-color: #d20101;
border: thin;
}
.total-recovered {
background-color: #05a705;
border: thin;
}
.c-layout {
padding-top: 1%;
}
view raw covidCounts.css hosted with ❤ by GitHub
covidTable.html
This table displays the country wise data in a tabular format. I have used an HTML table styled with custom CSS to display the data.
<template>
<template if:true={covidInfo}>
<!-- Filter for table content-->
<lightning-input
type="text"
placeholder="Filter by country name"
onchange={handleChange}
></lightning-input>
<table class="c-table">
<tr>
<th>
Country
</th>
<th>Total Cases</th>
<th>Total Deaths</th>
<th>Total Recovered</th>
</tr>
<template for:each={covidInfo} for:item="info">
<tr key={info.country}>
<td>
<lightning-avatar
src={info.countryInfo.flag}
class="c-avatar"
></lightning-avatar>
{info.country}
<lightning-button-icon
icon-name="utility:info"
variant="bare"
alternative-text="More Info"
title="More Info"
class="c-button-icon"
value={info.country}
onclick={handleInfoIconClick}
></lightning-button-icon>
</td>
<td>
<lightning-formatted-number
value={info.cases}
></lightning-formatted-number>
</td>
<td>
<lightning-formatted-number
value={info.deaths}
></lightning-formatted-number>
</td>
<td>
<lightning-formatted-number
value={info.recovered}
></lightning-formatted-number>
</td>
</tr>
</template>
</table>
</template>
</template>
view raw covidTable.html hosted with ❤ by GitHub
covidTable.js
The handleChange method is called when user types in the search box. I have debounced the method so that it does not throw an event with the change of each keystroke. Instead, the method waits for 1000ms and then throws an event that is handled by the parent component - covidInfo. The debouncing allows us to have a better performing app and you should always consider debouncing methods which perform some action on change event.
import { LightningElement, api } from "lwc";
export default class CovidTable extends LightningElement {
@api covidInfo;
// called when user types in the search box
handleChange(event) {
const countryName = event.target.value;
// Debouncing this method: Do not throw the filter event unless user has finished
// typing the country name is search box
window.clearTimeout(this.delayTimeout);
this.delayTimeout = setTimeout(() => {
this.filterList(countryName);
}, 1000);
}
// throws an event which is handled by covidInfo
filterList(countryName) {
const valueChangeEvent = new CustomEvent("filter", {
detail: countryName
});
this.dispatchEvent(valueChangeEvent);
}
// throws an event which is handled by covidInfo
handleInfoIconClick(event) {
const infoIconClickEvent = new CustomEvent("moreinfo", {
detail: event.target.value
});
this.dispatchEvent(infoIconClickEvent);
}
}
view raw covidTable.js hosted with ❤ by GitHub
covidTable.css
.c-table {
font-family: "Trebuchet MS", Arial, Helvetica, sans-serif;
border-collapse: collapse;
width: 100%;
}
.c-table td,
.c-table th {
border: 1px solid #ddd;
padding: 8px;
}
.c-table tr:nth-child(even) {
background-color: #f2f2f2;
}
.c-table tr:hover {
background-color: #ddd;
}
.c-table th {
text-align: left;
background-color: #4caf50;
color: white;
}
.c-avatar {
margin-right: 5%;
}
.c-button-icon {
margin-left: 1%;
}
view raw covidTable.css hosted with ❤ by GitHub
covidCountryInfo.html
This component is conditionally rendered. When the user clicks on the info icon present next to each country's name is the covidTable component, this component will render.
<template>
<template if:true={countryName}>
<div class="slds-text-heading_medium slds-text-align_center">
Data for {countryName}
<lightning-avatar
src={countryCovidInfo.countryInfo.flag}
class="c-avatar"
></lightning-avatar>
</div>
<table class="c-table">
<tr>
<th>Total Cases</th>
<th>Today's Cases</th>
<th>Total Deaths</th>
<th>Today's Deaths</th>
<th>Recovered</th>
<th>Active</th>
<th>Critical</th>
<th>Cases Per One Million</th>
<th>Deaths Per One Million</th>
</tr>
<tr>
<td>
<lightning-formatted-number
value={countryCovidInfo.cases}
></lightning-formatted-number>
</td>
<td>
<lightning-formatted-number
value={countryCovidInfo.todayCases}
></lightning-formatted-number>
</td>
<td>
<lightning-formatted-number
value={countryCovidInfo.deaths}
></lightning-formatted-number>
</td>
<td>
<lightning-formatted-number
value={countryCovidInfo.todayDeaths}
></lightning-formatted-number>
</td>
<td>
<lightning-formatted-number
value={countryCovidInfo.recovered}
></lightning-formatted-number>
</td>
<td>
<lightning-formatted-number
value={countryCovidInfo.active}
></lightning-formatted-number>
</td>
<td>
<lightning-formatted-number
value={countryCovidInfo.critical}
></lightning-formatted-number>
</td>
<td>
<lightning-formatted-number
value={countryCovidInfo.casesPerOneMillion}
></lightning-formatted-number>
</td>
<td>
<lightning-formatted-number
value={countryCovidInfo.deathsPerOneMillion}
></lightning-formatted-number>
</td>
</tr>
</table>
</template>
</template>
covidCountryInfo.js
import { LightningElement, api } from "lwc";
export default class CovidCountryInfo extends LightningElement {
@api countryName;
@api countryCovidInfo;
}
covidCountryInfo.css
.c-table {
font-family: "Trebuchet MS", Arial, Helvetica, sans-serif;
border-collapse: collapse;
width: 100%;
}
.c-table td,
.c-table th {
border: 1px solid #ddd;
padding: 8px;
}
.c-table tr:nth-child(even) {
background-color: #f2f2f2;
}
.c-table tr:hover {
background-color: #ddd;
}
.c-table th {
text-align: left;
background-color: #4caf50;
color: white;
}
.c-avatar {
margin-left: 1%;
margin-top: 1%;
}
I hope you have enjoyed reading this article. If you have any questions, please feel free to drop a comment or message me on LinkedIn.

Comments

  1. Such a very useful article. Very interesting to read this article. I would like to thank you for the efforts you had made for writing this awesome article. After reading your article I was amazed. I know that you explain it very well. And I hope that other readers will also experience how I feel after reading your article .Check out our website also we are one of the best salesforce marketing cloud implementation service in . We are register salesforce consulting and ISV Partner.

    ReplyDelete

Post a Comment

Popular posts from this blog

Salesforce Lightning: Countdown timer

Salesforce Hacks: System.LimitException: Too many queueable jobs added to the queue: 2

Building an Org Role Hierarchy component in LWC