Weather Report
This beginner guide walks you through building a simple weather report widget. It takes about 25 minutes to complete. It was written with CanJS 6.2.
The final widget looks like:
See the Pen Weather Report Guide (CanJS 6 Beginner) [Finished] by Bitovi (@bitovi) on CodePen.
To use the widget:
- Enter a location (example: Chicago)
- See the 5-Day forecast for the city you entered.
Start this tutorial by cloning the following CodePen:
See the Pen Weather Report Guide (CanJS 6 Beginner) [Starter] by Bitovi (@bitovi) on CodePen.
This CodePen has initial prototype HTML and CSS which is useful for getting the application to look right.
This starter code includes:
- Initial prototype HTML and CSS which is useful for getting the application to look right
- Pre-made styles so the app looks pretty 😍
The following sections are broken down into:
- The problem — A description of what the section is trying to accomplish.
- What you need to know — Information about CanJS that is useful for solving the problem.
- The solution — The solution to the problem.
Setup
The problem
Set up this CanJS app by:
- Defining a
WeatherReport
can-stache-element class. - Defining a custom element that outputs the pre-constructed HTML.
- Rendering the template by using the custom element.
What you need to know
A basic CanJS setup uses instances of a StacheElement, which glues ObservableObject-like properties to a
view
in order to manage its behavior as follows:import { StacheElement } from "//unpkg.com/can@6/core.mjs"; // Define the Component class MyComponent extends StacheElement { static view = `…`; static props = {}; } // Define the custom element tag customElements.define("my-component", CCPayment);
CanJS components will be mounted in the DOM by adding the component tag in the HTML page:
<my-component></my-component>
The solution
Update the JavaScript tab to:
- Define can-stache-element class
- Define element’s view by copying the content of the HTML tab in the view property
- Register the custom element tag by using customElements.define
import { StacheElement } from "//unpkg.com/can@6/core.mjs";
class WeatherReport extends StacheElement {
static view = `
<div class="weather-widget">
<div class="location-entry">
<label for="location">Enter your location:</label>
<input id="location" type="text" />
</div>
<p class="loading-message">Loading forecast…</p>
<div class="forecast">
<h1>5-Day Chicago Weather Forecast</h1>
<ul>
<li>
<span class="date">Today</span>
<span class="description light-rain">light rain</span>
<span class="high-temp">100<sup>°</sup></span>
<span class="low-temp">-10<sup>°</sup></span>
</li>
</ul>
</div>
</div>
`;
}
customElements.define("weather-report", WeatherReport);
Update the HTML tab to replace the old template with custom element:
<weather-report></weather-report>
<script>
const appKey = "17ba01d3931252096b4ab03df56891cd";
function transformData(data) {
let forecasts = [];
const today = new Date();
const transformedForecast = data.list.map(item => {
return {
date: new Date(item.dt_txt),
high: item.main.temp_max,
icon: item.weather[0].icon,
low: item.main.temp_min,
text: item.weather[0].description
};
});
const todayForecasts = transformedForecast.filter(forecast => {
const forecastDate = forecast.date;
return today.getDate() === forecastDate.getDate();
});
if (todayForecasts.length) {
forecasts.push(todayForecasts[0]);
}
const nextDaysForecasts = transformedForecast.filter(forecast => {
const forecastDate = forecast.date;
return (
today.getDate() !== forecastDate.getDate() &&
forecastDate.getHours() === 12
);
});
forecasts = forecasts.concat(nextDaysForecasts);
return forecasts;
}
</script>
Allow a user to enter a location
The problem
We want an input
element to:
- Allow a person to type a location to search for weather.
- Show the user the location they typed.
What you need to know
The properties defined in the
props
object can have default values like:class MyComponent extends StacheElement { static props = { age: { default: 34 } }; }
CanJS components use can-stache to render data in a template and keep it live.
The key:to binding can set an input’s
value
to an element property like:<input value:to="this.age" />
The solution
Update the JavaScript tab to:
- Define a
location
property as a string. - Update
location
value on the element when the input changes. - Show value of the element’s
location
property.
import { StacheElement } from "//unpkg.com/can@6/core.mjs";
class WeatherReport extends StacheElement {
static view = `
<div class="weather-widget">
<div class="location-entry">
<label for="location">Enter your location:</label>
<input id="location" type="text" value:to="this.location" />
</div>
<p class="loading-message">Loading forecast…</p>
<div class="forecast">
<h1>5-Day {{ this.location }} Weather Forecast</h1>
<ul>
<li>
<span class="date">Today</span>
<span class="description light-rain">light rain</span>
<span class="high-temp">100<sup>°</sup></span>
<span class="low-temp">-10<sup>°</sup></span>
</li>
</ul>
</div>
</div>
`;
static props = {
location: String
};
}
customElements.define("weather-report", WeatherReport);
Get the forecast
The problem
Once we’ve entered a city, we need to get the forecast data.
What you need to know
ES5 getter syntax can be used to define a props property that changes when another property changes. For example, the following defines an
excitedMessage
property that always has a!
after themessage
property:class MyElement extends StacheElement static props = { get excitedMessage() { return this.message+"!"; }, message: "string" } });
Use {{# if(value) }} to do
if/else
branching in can-stache.Promises are observable in can-stache. Given a promise
somePromise
, you can:- Check if the promise is loading like:
{{# if(somePromise.isPending) }}
.
- Check if the promise is loading like:
Open Weather Map provides a service endpoint for retrieving a forecast that matches a city name. For example, the following requests the forecast for Chicago:
https://api.openweathermap.org/data/2.5/forecast?q=Chicago&mode=json&units=imperial&apiKey=17ba01d3931252096b4ab03df56891cd
The fetch API is an easy way to make requests to a URL and get back JSON. Use it like:
fetch(url).then(response => { return response.json(); }).then(data => { });
can-param is able to convert an object into a query string format like:
param({format: "json", q: "chicago"}) //-> "format=json&q=chicago"
The solution
Update the template in the JS tab to:
- Wrap the loading message with
{{# if(this.forecastPromise.isPending) }}
- Wrap the forecast with
{{# if(this.forecastPromise.isResolved) }}
- Define a
forecastPromise
property that gets the forecast.
import { param, StacheElement } from "//unpkg.com/can@6/core.mjs";
class WeatherReport extends StacheElement {
static view = `
<div class="weather-widget">
<div class="location-entry">
<label for="location">Enter your location:</label>
<input id="location" type="text" value:to="this.location" />
</div>
{{# if(this.forecastPromise.isPending) }}
<p class="loading-message">Loading forecast…</p>
{{/ if }}
{{# if(this.forecastPromise.isResolved) }}
<div class="forecast">
<h1>5-Day {{ this.location }} Weather Forecast</h1>
<ul>
<li>
<span class="date">Today</span>
<span class="description light-rain">light rain</span>
<span class="high-temp">100<sup>°</sup></span>
<span class="low-temp">-10<sup>°</sup></span>
</li>
</ul>
</div>
{{/ if }}
</div>
`;
static props = {
get forecastPromise() {
if (this.location) {
return fetch(
"https://api.openweathermap.org/data/2.5/forecast?" +
param({
apiKey: appKey,
mode: "json",
q: this.location,
units: "imperial"
})
)
.then(response => {
return response.json();
})
.then(transformData);
}
},
location: String
};
}
customElements.define("weather-report", WeatherReport);
Display the forecast
The problem
Now that we’ve fetched the forecast data, we need to display it in the app.
What you need to know
Use {{# for(of) }} to do looping in can-stache.
Promises are observable in can-stache. Given a promise
somePromise
, you can:- Loop through the resolved value of the promise like:
{{# for(item of somePromise.value) }}
.
- Loop through the resolved value of the promise like:
Methods on the element can be called within a can-stache template like:
{{ this.myMethod(someValue) }}
The stylesheet includes icons for classNames that match:
sunny
,light-rain
,scattered-clouds
, etc.You can check whether one Date is equal to another Date by calling getDate on both dates and comparing the values.
The solution
Update the template in the JS tab to:
- Display each forecast day’s details (date, text, high, and low).
- Define a
toClassName
method that lowercases and hyphenates any text passed in. - Use the
toClassName
method to convert the forecast’stext
into aclassName
value that - Define
isToday
method that returnstrue
if the passed date is today will be matched by the stylesheet. - Use the
isToday
method to display the termToday
instead of today’s date.
import { param, StacheElement } from "//unpkg.com/can@6/core.mjs";
class WeatherReport extends StacheElement {
static view = `
<div class="weather-widget">
<div class="location-entry">
<label for="location">Enter your location:</label>
<input id="location" type="text" value:to="this.location" />
</div>
{{# if(this.forecastPromise.isPending) }}
<p class="loading-message">Loading forecast…</p>
{{/ if }}
{{# if(this.forecastPromise.isResolved) }}
<div class="forecast">
<h1>5-Day {{ this.location }} Weather Forecast</h1>
<ul>
{{# for(forecast of this.forecastPromise.value) }}
<li>
<span class="date">
{{# if(this.isToday(forecast.date)) }}
Today
{{ else }}
{{ forecast.date.toLocaleDateString() }}
{{/ if }}
</span>
<span class="description {{ this.toClassName(forecast.text) }}">{{ forecast.text }}</span>
<span class="high-temp">{{ forecast.high }}<sup>°</sup></span>
<span class="low-temp">{{ forecast.low }}<sup>°</sup></span>
</li>
{{/ for }}
</ul>
</div>
{{/ if }}
</div>
`;
static props = {
get forecastPromise() {
if (this.location) {
return fetch(
"https://api.openweathermap.org/data/2.5/forecast?" +
param({
apiKey: appKey,
mode: "json",
q: this.location,
units: "imperial"
})
)
.then(response => {
return response.json();
})
.then(transformData);
}
},
location: String
};
isToday(date) {
const today = new Date();
return today.getDate() === date.getDate();
}
toClassName(txt) {
return txt.toLowerCase().replace(/ /g, "-");
}
}
customElements.define("weather-report", WeatherReport);
Result
When finished, you should see something like the following CodePen:
See the Pen Weather Report Guide (CanJS 6 Beginner) [Finished] by Bitovi (@bitovi) on CodePen.