Using Angular to get a weather app standing up in minutes

John Au-Yeung
Aug 15 · 10 min read
Wim van ‘t Einde on Upsplash

OpenWeather is a weather website that provides its own free API for people to use. It is handy for building small weather apps for our own use. In this story, we will build our own weather app with the OpenWeatherMap API.

First, get an API key here.

We will build the app by using the Angular framework. Angular uses a component-based architecture to keep our code organized. It also has a flux library to store data in a central place. It is useful because it can do a lot of stuff (structuring code, routing, etc.), and it has libraries (e.g. the Angular Material library) to make our apps look pretty. It also has a program to scaffold and build the app with the Angular CLI.


Let’s Start Building

To begin, we run npm i -g @angular/cli to install the Angular CLI. Then we add the libraries that we need to make our app functional and look good. For this, we use Angular Material. We do this by running:

npm install --save @angular/material @angular/cdk @angular/animations

We also need to install @ngrx/store by running:

npm install @ngrx/store --save

We install moment to format dates by running:

npm i moment

Then we start writing our app. Let’s add a few components: We need an alerts page to display the UV index, a current weather component to display the current weather, a forecast component to display the forecast, a page to hold everything, a top bar to display the app name, and a search box.

Now we create the code that is used by multiple parts of the app. We need a reducer to store the data centrally. To do this, create a file called location-reducer.ts, and put in the following:

import { Action } from '@ngrx/store';export const initialState = '';
export const SET_LOCATION = 'SET_LOCATION';
export function locationReducer(state = initialState, action: any) {
switch (action.type) {
case SET_LOCATION:
state = action.payload
return state;
default:
return state;
}
}

Then we create the functions to get the weather data. We create an Angular service called WeatherService. To do this, run:

ng g service weather

In weather.service.ts, we put:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from 'src/environments/environment';
import * as moment from 'moment';
const apiKey: string = environment.apiKey;
@Injectable({
providedIn: 'root'
})
export class WeatherService {
constructor(private http: HttpClient) { } getCurrentWeather(loc: string) {
return this.http.get(`${environment.apiUrl}/weather?q=${loc}&appid=${apiKey}`)
}
getForecast(loc: string) {
return this.http.get(`${environment.apiUrl}/forecast?q=${loc}&appid=${apiKey}`)
}
getUv(lat: number, lon: number) {
let startDate = Math.round(+moment(new Date()).subtract(1, 'week').toDate() / 1000);
let endDate = Math.round(+moment(new Date()).add(1, 'week').toDate() / 1000);
return this.http.get(`${environment.apiUrl}/uvi/history?lat=${lat}&lon=${lon}&start=${startDate}&end=${endDate}&appid=${apiKey}`)
}
}

In environments/environment.ts, we put:

export const environment = {
production: false,
apiKey: 'api key',
apiUrl: 'http://api.openweathermap.org/data/2.5'
};

apiKey is the key you got from the website.

We run the following commands to generate our components:

ng g component uv
ng g component currentWeather
ng g component forecast
ng g component homePage
ng g component topBar

Now we add code to the components:

In uv.component.ts, we put:

import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { Store, select } from '@ngrx/store';
import { WeatherService } from '../weather.service';
@Component({
selector: 'app-uv',
templateUrl: './uv.component.html',
styleUrls: ['./uv.component.css']
})
export class UvComponent implements OnInit {
loc$: Observable<string>;
loc: string;
currentWeather: any = <any>{};
uv: any[] = [];
msg: string;
constructor(
private store: Store<any>,
private weatherService: WeatherService
) {
this.loc$ = store.pipe(select('loc'));
this.loc$.subscribe(loc => {
this.loc = loc;
this.searchWeather(loc);
})
}
ngOnInit() {
}
searchWeather(loc: string) {
this.msg = '';
this.currentWeather = {};
this.weatherService.getCurrentWeather(loc)
.subscribe(res => {
this.currentWeather = res;
}, err => {
}, () => {
this.searchUv(loc);
})
}
searchUv(loc: string) {
this.weatherService.getUv(this.currentWeather.coord.lat, this.currentWeather.coord.lon)
.subscribe(res => {
this.uv = res as any[];
}, err => {
})
}
resultFound() {
return Object.keys(this.currentWeather).length > 0;
}
}

This gets the UV index from the API.

In uv.component.html, we put:

<div *ngIf='resultFound()'>
<h1 class="center">Current UV data for {{currentWeather.name}}</h1>
<mat-card *ngFor='let l of uv' class="mat-elevation-z18">
<mat-card-header>
<mat-card-title>
<h2>{{l.date_iso | date:'MMM d, y, h:mm:ss a'}}</h2>
</mat-card-title>
</mat-card-header>
<mat-card-content>
<mat-list>
<mat-list-item>
<span>
UV Index: {{l.value}}
</span>
</mat-list-item>
</mat-list>
</mat-card-content>
</mat-card>
</div>
<div *ngIf='!resultFound()'>
<h1 class="center">{{msg || 'Failed to get weather.'}}</h1>
</div>

To format dates, we write:

{{l.date_iso | date:'MMM d, y, h:mm:ss a'}}

This is called a pipe in Angular jargon. It is a function which transforms one object into another. It can be used in templates and in logic code.

Similarly, in current-weather.component.ts, we put:

import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { Store, select } from '@ngrx/store';
import { WeatherService } from '../weather.service';
@Component({
selector: 'app-current-weather',
templateUrl: './current-weather.component.html',
styleUrls: ['./current-weather.component.css']
})
export class CurrentWeatherComponent implements OnInit {
loc$: Observable<string>;
loc: string;
currentWeather: any = <any>{};
msg: string;
constructor(
private store: Store<any>,
private weatherService: WeatherService
) {
this.loc$ = store.pipe(select('loc'));
this.loc$.subscribe(loc => {
this.loc = loc;
this.searchWeather(loc);
})
}
ngOnInit() {
}
searchWeather(loc: string) {
this.msg = '';
this.currentWeather = {};
this.weatherService.getCurrentWeather(loc)
.subscribe(res => {
this.currentWeather = res;
}, err => {
if (err.error && err.error.message) {
alert(err.error.message);
this.msg = err.error.message;
return;
}
alert('Failed to get weather.');
}, () => {
})
}
resultFound() {
return Object.keys(this.currentWeather).length > 0;
}
}

This is where we get the current weather. In the corresponding template, current-weather.component.html, we put:

<h1 class="center" *ngIf='resultFound()'>Current weather for {{currentWeather.name}}</h1>
<table *ngIf='resultFound()'>
<tbody>
<tr>
<td>
<h3>Current Temperature:</h3>
</td>
<td>
<h3>{{currentWeather.main?.temp - 273.15 | number:'1.0-0'}}<sup>o</sup>C</h3>
</td>
</tr>
<tr>
<td>
<h3>Maximum Temperature:</h3>
</td>
<td>
<h3>{{currentWeather.main?.temp - 273.15 | number:'1.0-0'}}<sup>o</sup>C</h3>
</td>
</tr>
<tr>
<td>
<h3>Minimum Temperature:</h3>
</td>
<td>
<h3>{{currentWeather.main?.temp_min - 273.15 | number:'1.0-0'}}<sup>o</sup>C</h3>
</td>
</tr>
<tr>
<td>
<h3>Clouds:</h3>
</td>
<td>
<h3>{{currentWeather.clouds?.all}}%</h3>
</td>
</tr>
<tr>
<td>
<h3>Humidity</h3>
</td>
<td>
<h3>{{currentWeather.main?.humidity}}%</h3>
</td>
</tr>
<tr>
<td>
<h3>Pressure</h3>
</td>
<td>
<h3>{{currentWeather.main?.pressure}}mb</h3>
</td>
</tr>
<tr>
<td>
<h3>Sunrise</h3>
</td>
<td>
<h3>{{currentWeather.sys?.sunrise*1000 | date:'long'}}</h3>
</td>
</tr>
<tr>
<td>
<h3>Sunset</h3>
</td>
<td>
<h3>{{currentWeather.sys?.sunset*1000 | date:'long'}}</h3>
</td>
</tr>
<tr>
<td>
<h3>Visibility</h3>
</td>
<td>
<h3>{{currentWeather.visibility}}m</h3>
</td>
</tr>
</tbody>
</table>
<div *ngIf='!resultFound()'>
<h1 class="center">{{msg || 'Failed to get weather.'}}</h1>
</div>

It’s just a table to display the data.

In forecast.component.ts, we put:

import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { Store, select } from '@ngrx/store';
import { WeatherService } from '../weather.service';
@Component({
selector: 'app-forecast',
templateUrl: './forecast.component.html',
styleUrls: ['./forecast.component.css']
})
export class ForecastComponent implements OnInit {
loc$: Observable<string>;
loc: string;
currentWeather: any = <any>{};
forecast: any = <any>{};
msg: string;
constructor(
private store: Store<any>,
private weatherService: WeatherService
) {
this.loc$ = store.pipe(select('loc'));
this.loc$.subscribe(loc => {
this.loc = loc;
this.searchWeather(loc);
})
}
ngOnInit() {
}
searchWeather(loc: string) {
this.msg = '';
this.currentWeather = {};
this.weatherService.getCurrentWeather(loc)
.subscribe(res => {
this.currentWeather = res;
}, err => {
}, () => {
this.searchForecast(loc);
})
}
searchForecast(loc: string) {
this.weatherService.getForecast(loc)
.subscribe(res => {
this.forecast = res;
}, err => {
})
}
resultFound() {
return Object.keys(this.currentWeather).length > 0;
}
}

In the corresponding template, forecast.component.html, we put:

<div *ngIf='resultFound()'>
<h1 class="center">Current weather forecast for {{currentWeather.name}}</h1>
<mat-card *ngFor='let l of forecast.list' class="mat-elevation-z18">
<mat-card-header>
<mat-card-title>
<h2>{{l.dt*1000 | date:'MMM d, y, h:mm:ss a'}}</h2>
</mat-card-title>
</mat-card-header>
<mat-card-content>
<table *ngIf='resultFound()'>
<tbody>
<tr>
<td>Temperature</td>
<td>{{l.main?.temp - 273.15 | number:'1.0-0'}}<sup>o</sup>C</td>
</tr>
<tr>
<td>Minimum Temperature</td>
<td>{{l.main?.temp_min - 273.15 | number:'1.0-0'}}<sup>o</sup>C</td>
</tr>
<tr>
<td>Maximum Temperature</td>
<td>{{l.main?.temp_max - 273.15 | number:'1.0-0'}}<sup>o</sup>C</td>
</tr>
<tr>
<td>Pressure</td>
<td>{{l.main?.pressure | number:'1.0-0'}}mb</td>
</tr>
<tr>
<td>Sea Level</td>
<td>{{l.main?.sea_level | number:'1.0-0'}}m</td>
</tr>
<tr>
<td>Ground Level</td>
<td>{{l.main?.grnd_level | number:'1.0-0'}}m</td>
</tr>
<tr>
<td>Humidity</td>
<td> {{l.main?.humidity | number:'1.0-0'}}%</td>
</tr>
<tr>
<td>Weather</td>
<td>
<ul>
<li *ngFor='let w of l.weather'>
{{w?.main }}: {{w?.description }}
</li>
</ul>
</td>
</tr>
<tr>
<td>Wind Speed</td>
<td>{{l.wind?.speed }}</td>
</tr>
<tr>
<td>Wind Direction</td>
<td>{{l.wind?.deg }}<sup>o</sup></td>
</tr>
</tbody>
</table>
</mat-card-content>
</mat-card>
</div>
<div *ngIf='!resultFound()'>
<h1 class="center">{{msg || 'Failed to get weather.'}}</h1>
</div>

In home-page.component.ts, we put:

import { Component, OnInit } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { Observable } from 'rxjs';
@Component({
selector: 'app-home-page',
templateUrl: './home-page.component.html',
styleUrls: ['./home-page.component.css']
})
export class HomePageComponent implements OnInit {
loc$: Observable<string>;
loc: string;
constructor(private store: Store<any>) {
this.loc$ = store.pipe(select('loc'));
this.loc$.subscribe(loc => {
this.loc = loc;
})
}
ngOnInit() {
}
}

And in home-page.component.html, we have:

<app-top-bar></app-top-bar>
<div id='container'>
<div *ngIf='!loc' id='search'>
<h1>Enter location to find weather info.</h1>
</div>
<mat-tab-group *ngIf='loc'>
<mat-tab label="Current Weather">
<app-current-weather></app-current-weather>
</mat-tab>
<mat-tab label="Forecast">
<app-forecast></app-forecast>
</mat-tab>
<mat-tab label="UV Index">
<app-uv></app-uv>
</mat-tab>
</mat-tab-group>
</div>

Finally, in top-bar.component.ts, we have:

import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { SET_LOCATION } from '../location-reducer';
import { NgForm } from '@angular/forms';
@Component({
selector: 'app-top-bar',
templateUrl: './top-bar.component.html',
styleUrls: ['./top-bar.component.css']
})
export class TopBarComponent implements OnInit {
loc: string;
constructor(private store: Store<any>) { }ngOnInit() {
}
search(searchForm: NgForm) {
if (searchForm.invalid) {
return;
}
this.store.dispatch({ type: SET_LOCATION, payload: this.loc });
}
}

And in top-bar.component.html, we have:

<mat-toolbar>
<span id='title'>Weather App</span>
<form (ngSubmit)='search(searchForm)' #searchForm='ngForm'>
<mat-form-field class="form-field">
<input matInput placeholder="Search Location" [(ngModel)]='loc' #lo='ngModel' name='lo' required type='text'
autocomplete="off">
<mat-error *ngIf="lo.invalid && (lo.dirty || lo.touched)">
<span *ngIf='lo.errors.required'>
Location is required.
</span>
</mat-error>
</mat-form-field>
<button mat-button type='submit'>Search</button>
</form>
</mat-toolbar>

In top-bar.component.css, we have:

#title{
margin-right: 30px;
}
.mat-form-field{
font-size: 13px;
}
.mat-button{
height: 33px;
}
.mat-toolbar{
background-color: green;
color: white;
}
.mat-error, .mat-form-field-invalid .mat-input-element, .mat-warn .mat-input-element{
color: white !important;
border-bottom-color: white !important;
}
.mat-focused .placeholder{
color: white;
}
::ng-deep .form-field.mat-form-field-appearance-legacy .mat-form-field-underline,
.form-field.mat-form-field-appearance-legacy .mat-form-field-ripple,
.form-field.mat-form-field-appearance-legacy.mat-focused
.mat-form-field-underline,
.form-field.mat-form-field-appearance-legacy.mat-focused
.mat-form-field-ripple {
background-color: white !important;
border-bottom-color: white !important;
}
/** Overrides label color **/
::ng-deep .form-field.mat-form-field-appearance-legacy .mat-form-field-label,
.form-field.mat-form-field-appearance-legacy.mat-focused
.mat-form-field-label {
color: white !important;
border-bottom-color: white !important;
}
/** Overrides caret & text color **/
::ng-deep .form-field.mat-form-field-appearance-legacy .mat-input-element {
caret-color: white !important;
color: white !important;
border-bottom-color: white !important;
}
::ng-deep .mat-form-field-underline, ::ng-deep .mat-form-field-ripple {
background-color: white !important;
}

Let’s add some colors to the top bar.

In style.css, we add:

/* You can add global styles to this file, and also import other style files */
@import "~@angular/material/prebuilt-themes/indigo-pink.css";
body{
margin: 0px;
font-family: 'Roboto', sans-serif;
}
.center{
text-align: center;
}
table{
width: 100%
}

This is where we import the Material Design theme for styling. In index.html, let’s add the following bit of code so we can use the Roboto font that we specified:

<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">

Notice that we have this:

search(searchForm: NgForm) {
if (searchForm.invalid) {
return;
}
this.store.dispatch({ type: SET_LOCATION, payload: this.loc });
}

This is where the search happens. The search keyword is propagated throughout the whole app by sending the keyword in the store. Wherever we see …

this.loc$ = store.pipe(select('loc'));
this.loc$.subscribe(loc => {
this.loc = loc;
})

… we subscribe to the latest search keyword from the flux store. We do not have to worry about passing data around the app to propagate the search keyword. This is similar to the block below:

this.loc$ = store.pipe(select('loc'));
this.loc$.subscribe(loc => {
this.loc = loc;
this.searchWeather(loc);
})

This sends the keyword to the service function which searches for the weather data according to the keyword we enter.

Anything starting with mat is an Angular Material widget. They are all styled so we don’t have to it ourselves.

In app.module.ts , we replace the existing code with:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import {
MatButtonModule,
MatToolbarModule,
MatInputModule,
MatTabsModule,
MatCardModule,
MatDividerModule,
MatListModule
} from '@angular/material';
import { HomePageComponent } from './home-page/home-page.component';
import { StoreModule } from '@ngrx/store';
import { locationReducer } from './location-reducer';
import { TopBarComponent } from './top-bar/top-bar.component';
import { FormsModule } from '@angular/forms';
import { WeatherService } from './weather.service';
import { CurrentWeatherComponent } from './current-weather/current-weather.component';
import { ForecastComponent } from './forecast/forecast.component';
import { UvComponent } from './uv/uv.component';
import { AlertsComponent } from './alerts/alerts.component';
import { HttpClientModule } from '@angular/common/http';
@NgModule({
declarations: [
AppComponent,
HomePageComponent,
TopBarComponent,
CurrentWeatherComponent,
ForecastComponent,
UvComponent,
AlertsComponent
],
imports: [
BrowserModule,
AppRoutingModule,
BrowserAnimationsModule,
MatButtonModule,
MatToolbarModule,
StoreModule.forRoot({
loc: locationReducer
}),
FormsModule,
MatInputModule,
MatTabsModule,
MatCardModule,
HttpClientModule,
MatDividerModule,
MatListModule
],
providers: [
WeatherService
],
bootstrap: [AppComponent]
})
export class AppModule { }

to include the Angular Material and the code we wrote in the main app module.

Results

In the end, we have:

Better Programming

Advice for programmers.

John Au-Yeung

Written by

Web developer. Subscribe to my email list now at http://jauyeung.net/subscribe/ . Follow me on Twitter at https://twitter.com/AuMayeung

Better Programming

Advice for programmers.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade