While Aurelia 2 is still not quite ready for release, you can use it right now as the basic core pieces are functional. I thought it would be fun to build a weather application using Aurelia 2.
If you don’t want to run through a tutorial and just want the final result, all of the code for the weather application can be found on GitHub here.
What Are We Building?
In this tutorial, we are going to be building an Aurelia 2 application that displays weather information. Original, right? You will learn how to create new Aurelia 2 applications, as well as work with the OpenWeatherMap API where we will consume JSON for the information.
Before we can continue, head over to the OpenWeatherMap website here and create a free account. This application will only be for personal use, so the limits of 60 calls per minute are perfect for us.
You will then want to generate an API key by heading over to the API Keys section once you are signed up and logged in.
A Note On Code In This Tutorial
Aurelia allows you to leverage conventions for things such as custom elements, value converters and whatnot. It also allows you to be more explicit in how you name and import dependencies.
For the purposes of this tutorial post, we will be leveraging conventions, but the code in the repository will leverage no conventions and use decorators for describing components. My personal preference is to be quite explicit in my Aurelia applications.
Getting Started
Unlike Aurelia 1, there is nothing to install globally (the aurelia-cli
is not a dependency you need any more). To bootstrap a new Aurelia 2 application, you simply open up a terminal/PowerShell window and run:
npx makes aurelia
Because TypeScript is the future, I recommend choosing the “Default TypeScript Aurelia 2 App” option in the prompt. Then choose, “npm” for the package installer option and wait for your app to be created.
To confirm everything installed correctly, open up the generated application directory (in my case it is weather-app
) and then run the application using npm start
a browser window should open and point to port 9000
.
Create A Weather Service
In an Aurelia application, using singleton service classes is a great habit to get into too. Singletons are easy to test and work well with Aurelia’s dependency injection (DI).
In the src
directory create a new folder called services
and a file called weather-api.ts
which will handle making calls to the OpenWeatherMap API service.
import { HttpClient } from '@aurelia/fetch-client'; import { DOM } from '@aurelia/runtime-html'; const http = new HttpClient(DOM); export class WeatherApi { private apiKey = ''; private units = 'metric'; public async getWeather(address) { const req = await http.fetch(`https://api.openweathermap.org/data/2.5/forecast?q=${address}&units=this.units&APPID=${this.apiKey}`); return req.json(); } }
This simple service will allow us to query the API for weather information. But, we are not using TypeScript to its full potential here. Let’s write some interfaces and type the response.
import { HttpClient } from '@aurelia/fetch-client'; import { inject } from 'aurelia'; @inject(HttpClient) export class WeatherApi { private apiKey = ''; private units = 'metric'; constructor(private http: HttpClient) { } public async getWeather(latitude: number, longitude: number): Promise<IWeatherResponse> { const req = await this.http.fetch(`https://api.openweathermap.org/data/2.5/forecast?lat=${latitude}&lon=${longitude}&units=${this.units}&APPID=${this.apiKey}`); return req.json(); } } interface IWeatherResponse { cod: string; message: number; cnt: number; list: IWeatherResponseItem[]; } interface IWeatherResponseItemWeather { id: number; main: string; description: string; icon: string; } interface IWeatherResponseItem { dt: number; main: { temp: number; feels_like: number; temp_min: number; temp_max: number; pressure: number; sea_level: number; grnd_level: number; humidity: number; temp_kf: number; }; weather: IWeatherResponseItemWeather[]; clouds: { all: number; }; wind: { speed: number; deg: number; }; rain: { '3h': number; }; sys: { pod: string; }; dt_txt: string; }
Now, there is one thing I want to point out with the above example. We’re hard-coding the API key into the singleton class, in a real application, you would and should never do this. Anyone who has your API key will be able to make requests and blast through your limits quickly. Never store API keys client-side.
We now have the class we will use to query for weather information. I won’t go into super specifics around markup and whatnot as those things can be seen in the GitHub repository for this tutorial here.
Leveraging Dependency Injection (DI) To Import Our Service
Now that we have our weather API service, we need to include it for use in our application. We will be editing the generated my-app.ts
file as part of the Aurelia application creation process.
We want to replace the entirety of our my-app.ts
file with the following:
import { WeatherApi } from './services/weather-api'; export class MyApp { private weather; constructor(private api: WeatherApi) { } attached() { navigator.geolocation.getCurrentPosition((position) => this.success(position), () => this.error()); } async success(position: Position) { const latitude = position.coords.latitude; const longitude = position.coords.longitude; this.weather = await this.api.getWeather(latitude, longitude); } error() { } }
Because my-app.ts
is a rendered custom element (as can be seen inside of index.ejs
we get DI automatically when we use TypeScript. This means we don’t have to use the inject
decorator to inject things.
All dependencies get passed through the constructor and using TypeScript with a visibility keyword, they get hoisted onto the class itself for use. It’s a nice touch and one of my favourite things about TypeScript.
The attached
method is a lifecycle method that gets called in components once the DOM is ready. This is where you handle interacting with the DOM or calling various API methods. We call the getCurrentPosition
method here to request the users latitude and longitude.
The success
callback is called via the navigation position callback on success. This is where we get the latitude and longitude values, then we make a call to our injected API class and call the getWeather
method.
You might also notice we are using async/await
here which allows us to wait for the results of a promise and get the data. We assign the value to the class variable weather
which will be referenced in the view template shortly.
Creating A Value Converter For Formatting Dates/Times
We are going to use the date-fns
library for working with dates and formatting them. One of the values the weather API returns is a date string which we will parse and then format for display purposes.
You might not have worked with date-fns
before, but it is similar to Moment.js which is a heavier and often unnecessary option to go with.
To install date-fns
all we need to do is run:
npm install date-fns
In Aurelia, a value converter is exactly what it sounds like. It converts values to something else (either to the view or from the view). In our use case, we only want to convert a value in the view.
For resources, I highly recommend creating a resources
directory which exports an array of resources to make global.
Create a file called src/resources/value-converters/date-format.ts
and add in the following:
import { valueConverter } from '@aurelia/runtime'; import { format } from 'date-fns'; export class FormatDateValueConverter { toView(date): string { return date ? format(new Date(date), 'MM/dd/yyyy - h:mm bbbb') : date; } }
Inside of the resources
directory create a file called index.ts
with the following:
import { FormatDate } from './value-converters/format-date'; export const resources = [ FormatDate ];
This exports an array of one or more resources. For the purposes of this tutorial, we are only exporting one resource to make global. In a real application, you might have several. If you worked with Aurelia 1, this paradigm will look familiar with you with an array of resources that get globalised. Except, globalResources
is no longer a thing.
Inside of src/main.ts
we want to import those resources and register them:
import Aurelia from 'aurelia'; import { MyApp } from './my-app'; import { resources } from './resources'; Aurelia .register(resources) .app(MyApp) .start();
The Markup
We now have the basics in place, let’s start with our markup and styling. We are going to use Bootstrap 4 because it has a good grid system and will make styling things easier.
Install and Configure Bootstrap
npm install bootstrap
Before we start adding in any HTML, we need to import the main Bootstrap CSS file into our application.
At the top of src/main.ts
add the following import:
import 'bootstrap/dist/css/bootstrap.css';
We now have the styling we need for marking up our columns and aspects of the grid system.
Creating A Weather Component
Breaking your application into components is a great way to create applications. Smaller components are easier to test, they are also easier to maintain and neater.
Inside of the src
directory create a new folder called components
and create a component called weather-item
which we will import and use to display our weather information.
Now, we want to create the HTML file for our custom element: src/components/weather-item.html
<bindable name="data" /> <p><strong>Date:</strong> ${data.dt_txt | formatDate}</p> <p><strong>Clouds:</strong> ${data.clouds.all}%</p> <p><strong>Temperature:</strong> ${data.main.temp}°</p> <p><strong>Feels Like:</strong> ${data.main.feels_like}°</p> <p><strong>Humidity:</strong> ${data.main.humidity}%</p>
A brief explanation of what is happening here, we just created a HTML only custom element. The bindable
element at the top is telling Aurelia that we have a custom element which accepts a bindable property called data
which allows data to be passed through.
You will notice below when we import and use our element, we are binding on the data
property by specifying data.bind
. Inside of our custom element, we reference this bindable value specifically to get pieces of data passed in.
If you have experience working with other frameworks or libraries such as React, you might know of these as “props” in Aurelia they’re bindable properties.
If you want to build custom elements with business logic that extends beyond simple bindables, you will want to consult the documentation on creating custom elements that leverage a view-model (something we do not need to do here).
<import from="./components/weather-item.html"></import> <div class="container spacer-v"> <form class="mb-4"> <h2 class="mb-3">Weather. Whenever.</h2> </form> <div if.bind="weather && weather.cod === '404'"> <h3>Error</h3> <p>${weather.message}</p> </div> <div class="row" if.bind="weather && weather.cod !== '404'"> <div class="col-md-3"> <div class="row"> <div class="col-md-3"><img src="http://openweathermap.org/img/wn/${weather.list[0].weather[0].icon}.png"></div> <div class="col-md-8"> <h3>${weather.city.name}, ${weather.city.country}</h3> </div> </div> </div> <div class="col-md-8"> <div class="row"> <weather-item data.bind="item" class="col-md-4 mb-3" repeat.for="item of weather.list"></weather-item> </div> </div> </div> </div>
The first line is simply using the import element to include our component for use in our view. The import element works like a Javascript import, except it can also import HTML files as well as CSS files. If you have experience with Aurelia 1, this is the same element as require
.
The rest is standard Aurelia templating, if you’re familiar with Aurelia v1, this will look familiar to you already.
If you’re new to Aurelia, I highly recommend reading the documentation to get a better understanding of how to author Aurelia templates and work with the templating.
To complete the styling, open up my-app.css
which is a CSS file included automatically by Aurelia (matching the name of my-app.ts
using conventions).
.spacer-v { padding-top: 50px; }
Running The App
Provided you followed all of the steps correctly, to run the application simply type the following in the project directory:
npm start
A browser window should open and you should see an application that requests your current location and shows you the weather. It should look like this.
There are some improvements you could make on your own like a field that allows users to type addresses, a map or more detailed weather information, adding in routing to view specific fine-grain information. This is a quick and basic tutorial showing you how easy it is to build in Aurelia 2.