Week 10
We got closer to a fully functioning weather widget in the last session. Before we focus on making network requests, we'll refactor and add a few minor features.
Loading Indicator
A typical pattern with applications loading data over the network is to show a loading indicator or some other visual aid to let the user know there's no data yet.
Let's build a fundamental loading indicator. Open index.html
and make the following change:
<!DOCTYPE html>
<html>
<head>
<title>Weather Widget</title>
<link rel="stylesheet" href="style.css" />
<!-- <script src="https://code.jquery.com/jquery-3.6.1.min.js"></script> -->
<script src="/jquery-3.6.1.min.js"></script>
<script src="/script.js"></script>
</head>
<body>
<div class="weather-widget">
<!-- Add the following line -->
<div class="loading-indicator">Loading...</div>
<div class="headline">
</div>
<ul class="forecast">
</ul>
</div>
</body>
</html>
Most loading indicators are animated in some way. We can use CSS to make it pulse. Add the following snippet to your style.css
:
.loading-indicator {
opacity: 0;
display: flex;
justify-content: center;
border: 1px solid black;
border-radius: 3px;
animation: pulse infinite ease-out alternate 2s;
}
@keyframes pulse {
0% {
opacity: 0;
}
50% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
Once you refresh the page in your browser, you see a pulsating loading indicator. Next, we'll use JavaScript to turn it on and off as needed.
Open script.js
and add two new functions at the end of the file:
function showLoadingIndicator() {
$('.loading-indicator').show();
}
function hideLoadingIndicator() {
$('.loading-indicator').hide();
}
These are so-called utility functions. We'll call them from our code as we need it.
//script.js
$(document).ready(function() {
showLoadingIndicator(); //add this line
fetchWeatherData().then(generateHtml);
});
First of all, we'll enable the loading indicator first thing as the application loads and leave it on while we wait for data.
Once we receive weather data, we also need to disable it again. The best place for that is in generateHtml
:
function generateHtml(weatherData) {
hideLoadingIndicator(); //add this line
$(".headline").append($(`<h1>${weatherData.currentLocation}</h1>`));
$(".headline").append($(`<h2>${weatherData.currentTempF} F</h2>`));
for (var i = 0; i < weatherData.forecastTemperatures.length; i++) {
var currentTemperature = weatherData.forecastTemperatures[i];
var row = $("<li>");
row.append($(`<h2>${currentTemperature.tempF}</h2>`));
row.append($(`<h2>${currentTemperature.time}</h2>`));
$(".forecast").append(row);
}
}
Add Weather Forecast Data
So far, we're not rendering weather forecast data because it's not in the format we need for our weather widget.
Our widget expects a list of objects, where each object has a tempF
and time
property:
for (var i = 0; i < weatherData.forecastTemperatures.length; i++) {
var currentTemperature = weatherData.forecastTemperatures[i];
var row = $("<li>");
row.append($(`<h2>${currentTemperature.tempF}</h2>`));
row.append($(`<h2>${currentTemperature.time}</h2>`));
$(".forecast").append(row);
}
On the other hand, the weather API returns data in this format:
"hourly": {
"time": [
"2022-11-19T00:00",
"2022-11-19T01:00",
"2022-11-19T02:00"
],
"temperature_2m": [
30.3, 28.1, 28.5
]
}
Timestamps and temperatures are collected in two separate lists. It's expected that data coming from an API is in a different format than what we need for our application. Therefore, we'll add some code to transform the API's data format into our own:
function fetchWeatherData() {
return (
fetch("/data.json")
.then(function(response) {
return response.json();
})
.then(function(data) {
//add the code below
var forecastTemperatures = [];
// (2)
for (var i = 0; i < data.hourly.time.length; i++) {
var currentTime = data.hourly.time[i]; //(1)
var currentTempF = data.hourly.temperature_2m[i];
forecastTemperatures.push({
time: currentTime,
tempF: currentTempF
}); //(3)
}
return Promise.resolve({
currentTempF: data.current_weather.temperature,
currentLocation: "Boston",
forecastTemperatures: forecastTemperatures, //change this line
});
})
);
}
We add our transformation step in the .then
handler to transform forecast data into a format we can work with.
For each timestamp we pick (1), we'll find the corresponding temperature in the temperature list at the corresponding index.
Since both lists have the same length, we'll use a single for
loop and the timestamp list length as reference (2).
In each loop iteration, we pick the time at the current index and do the same for the temperature.
For both, we create a new object with the property names we need (time
, tempF
) and store it in a new list (3).
In the last step, we return forecastTemperatures
.
Why do we need to transform API data first?
Instead of going through the (perceived) hassle of writing additional code to transform data from one format to another, we could skip this step and work with the data directly. For an application as small as ours, we could "get away" with it. However, adding abstractions, such as defining a data format we need for rendering HTML, makes sense as the application grows. Our advantage: We can add additional calculations, for instance, converting Fahrenheit to Celsius upfront. Also, if we decide to go with a different API for some reason, implementing this change is a more straightforward exercise as we only need to change code that directly talks to the API and transforms any data. All other code remains the same. The latter is critical for larger applications.
Adjust display settings
If you look at the list of forecast data now, you notice it's hard to read. Let's add some CSS to fix this:
.weather-widget ul {
display: flex;
flex-direction: row;
list-style-type: none;
justify-content: space-around;
/*change this*/
padding-left: 120px;
/*change this*/
overflow: scroll;
}
/*Add the styles below*/
.weather-widget ul li {
border: 1px solid black;
margin: 0px 5px;
/*No margin on top or bottom, but 5px to the left and right */
display: flex;
flex-direction: column;
flex-wrap: wrap;
padding: 10px 0px;
}
We need to add some more padding to our ul
holding forecast data because it clips off the beginning of the first forecast box (padding-left: 120px;
).
Without any additional styles, forecast data looks smushed. To improve the situation, we enable flexbox for <li>
's.
Margins
You might notice the unusual syntax for margin
. When specifying margins (the same goes for padding
or border
), we can decide values for top
, bottom
, left
, and right
.
If we wanted to have a 10px
margin for all four sides, we could do it as this:
margin-left: 10px;
margin-right: 10px;
margin-top: 10px;
margin-bottom 10px;
Since this is a lot to type and repetitive, we can use the shorthand margin: 10px
.
In some cases, we'd like to have the same values for top
and bottom
but different ones for left
and right
:
margin: 5px 0px;
In this shorthand, we specify a margin of 5px
for the top and bottom and 0px
for the left and right.
Windspeed
Inspecting the weather data more closely, you notice it also contains the current wind speed.
{
"latitude": 42.36515,
"longitude": -71.0618,
"generationtime_ms": 0.3980398178100586,
"utc_offset_seconds": -18000,
"timezone": "America/New_York",
"timezone_abbreviation": "EST",
"elevation": 10.0,
"current_weather": {
"temperature": 30.7,
"windspeed": 7.4,
...
Let's add some code to indicate whether it's currently windy.
We start with an additional HTML headline element and add it to <div class="headline">
. In generateHtml
, we add some code to make this decision:
//script.js
function generateHtml(weatherData) {
hideLoadingIndicator();
$(".headline").append($(`<h1>${weatherData.currentLocation}</h1>`));
$(".headline").append($(`<h2>${weatherData.currentTempF} F</h2>`));
$(".headline").append($(`<h2>Windy</h2>`)); //add this line
for (var i = 0; i < weatherData.forecastTemperatures.length; i++) {
var currentTemperature = weatherData.forecastTemperatures[i];
var row = $("<li>");
//...
Now we always indicate it's windy outside. To make it dynamic, change your code to:
if (weatherData.current_weather.windspeed > 5) {
$(".headline").append($(`<h2>Windy</h2>`));
} else {
$(".headline").append($(`<h2>Not Windy</h2>`));
}