In this chapter, we'll design and build a simple Local Weather app using Angular and a third-party web API with an iterative development methodology. We'll focus on delivering value first while learning about the nuances and optimal ways of using Angular, TypeScript, Visual Studio (VS) Code, Reactive Programming, and RxJS. Before we dive into coding, we need to build a roadmap of features, create a mock-up of the application we intend to build, and diagram the high-level architecture of our app.
You'll be introduced to Angular fundamentals to build a simple web app and become familiar with the new Angular platform and full-stack architecture.
In this chapter, you are going to learn the following:
HttpClient
to retrieve data from OpenWeatherMap
APIsThe most up-to-date versions of the sample code for the book are on GitHub at the repository linked below. The repository contains the final and completed state of the code. You can verify your progress at the end of this chapter by looking for the end-of-chapter snapshot of code under the projects
folder.
For Chapter 3:
npm install
on the root folder to install dependencies.projects/ch3
npx ng serve ch3
Beware that the source code in the book or on GitHub may not always match the code generated by Angular CLI. There may also be slight differences in implementation between the code in the book and what's on GitHub because the ecosystem is ever-evolving. It is natural for the sample code to change over time. Also, on GitHub, expect to find corrections, fixes to support newer versions of libraries, or side-by-side implementations of multiple techniques for the reader to observe. The reader is only expected to implement the ideal solution recommended in the book. If you find errors or have questions, please create an issue or submit a pull request on GitHub for the benefit of all readers.
You can read more about updating Angular in Appendix C, Keeping Angular and Tools Evergreen. You can find this appendix online from https://static.packt-cdn.com/downloads/9781838648800_Appendix_C_Keeping_Angular_and_Tools_Evergreen.pdf or at https://expertlysimple.io/stay-evergreen.
Let's start by creating a high-level plan to understand what to implement before you start coding.
Having a roadmap before getting on the road is critical in ensuring that you reach your destination. Similarly, building a rough plan of action before you start coding is very important in ensuring project success. Building a plan early on enables your colleagues or clients to be aware of what you're planning to accomplish. However, any initial plan is guaranteed to change over time.
Agile software development aims to account for the change of priorities and features over time. Kanban and Scrum are the two most popular methodologies that you can use to manage your project. Each methodology has a concept of a backlog and lists that capture planned, in progress, and completed work. A backlog, which contains a prioritized list of tasks, establishes a shared understanding of what needs to be worked on next. Lists that capture the status of each task act as information radiators, where stakeholders can get updates without interrupting your workflow. Whether you're building an app for yourself or someone else, keeping a live backlog and tracking the progress of tasks pays dividends and keeps the focus on the goal you're trying to achieve.
In implementing the Local Weather app, we are going to leverage a GitHub project to act as a Kanban board. In an enterprise, you can use ticketing systems or tools that can keep a backlog, implement the Scrum methodology, and display Kanban boards. In GitHub, issues represent your backlog. You can leverage the built-in Projects tab to define a scope of work that represents a release or a sprint to establish a Kanban board. A GitHub project directly integrates with your GitHub repository's issues and keeps track of the status of issues via labels. This way, you can keep using the tool of your choice to interact with your repository and still, effortlessly, radiate information. In the next section, you are going to set up a project to achieve this goal.
Let's set up a GitHub project:
Figure 3.1: Creating a new project in GitHub
Observe your Kanban board, which should appear as follows:
Figure 3.2: The Kanban board for your project
If you have existing issues on your repository, you may be prompted to add cards to your board. You can safely ignore this for now and return to it with the + Add cards button. You are also presented with several To do cards. Feel free to review and dismiss these cards to clear out your board.
If you would like to keep track of every release or sprint, you can create a new project for each one. Creating new projects helps keep track of percentage completion for a given release or sprint, at the cost of introducing additional management overhead.
Next, we are going to configure the project as a Kanban board instead of a GitHub Project, which is a lightweight methodology to organize your work you might choose over other methodologies like Scrum.
Kanban does not define formal iterations or releases of your work. If you would like to have a low-overhead process, where you only work with a single project, you can do this by introducing a Backlog column to your project.
Now let's add a Backlog column:
Backlog
.Figure 3.3: Where to select "Newly added"
With this setup, new issues are added to the Backlog, allowing you to manually maintain the items you intend to work on in the To do column.
Let's create a backlog of issues to keep track of your progress as you implement the design of your application. When creating issues, you should focus on delivering functional iterations that bring some value to the user.
The technical hurdles you must clear to achieve those results are of no interest to your users or clients.
Here are the features we plan to build in our first release:
Let's also add some features that we won't implement in this book as a way to demonstrate how a backlog can capture your ideas:
localStorage
to cache user preferencesFeel free to add other features you can think of to your backlog.
Begin by creating the preceding features as issues on GitHub. Make sure to assign each new issue to the project you created earlier in the chapter. Once created, move the preceding defined features to the To do column. When you begin working on a task, move the card into the In progress column and when it's completed, move it to the Done column. The following is what the board looks like as we plan to begin working on the first feature – Display Current Location weather information for the current day:
Figure 3.4: A snapshot of the initial state of the board on GitHub
Note that I also added an issue to Create a mock-up for the app and moved it to Done, which is something I'll cover in the next section. Also, GitHub might automatically move a card from one state to another as you open and close them.
Ultimately, GitHub projects provide an easy-to-use GUI so that non-technical people can easily interact with GitHub issues. By allowing non-technical people to participate in the development process on GitHub, you unlock the benefits of GitHub becoming the single source of information for your entire project. Questions, answers, and discussions about features and issues are all tracked as part of GitHub issues, instead of being lost in emails. You can also store wiki-type documentation on GitHub. So, by centralizing all project-related information, data, conversations, and artifacts on GitHub, you are greatly simplifying the potentially complicated interaction of multiple systems that require continued maintenance at a high cost. For private repositories and on-premise enterprise installations, GitHub has a very reasonable cost. If you're sticking with open source, as we are in this chapter, all these tools are free.
As a bonus, I created a rudimentary wiki page on my repository at https://github.com/duluca/local-weather-app/wiki. Note that you can't upload images to README.md
or wiki pages. To get around this limitation, you can create a new issue, upload an image in a comment, and copy and paste the URL for it to embed images to README.md
or wiki pages. In the sample wiki, I followed this technique to embed the wireframe design into the page.
With a roadmap in place, you're now ready to create a mock-up of your application.
There are some great tools out there to do rough-looking mock-ups to demonstrate your idea with surprising amounts of rich functionality. If you have a dedicated UX designer, such tools are great for creating quasi prototypes. However, as a full-stack developer, I find the best tool out there to be pen and paper. This way, you don't have to learn yet another tool (YAT), and it is a far better alternative to having no design at all. Putting things on paper saves you from costly coding detours down the line and if you can validate your wireframe design with users ahead of time, even better. My app is called LocalCast Weather, but get creative and pick your own name. Behold, the wireframe design for your weather app:
Figure 3.5: Hand-drawn wireframe for LocalCast. (Tip: I did use a ruler!)
The wireframe shouldn't be anything fancy. I recommend starting with a hand-drawn design, which is very quick to do and carries over the rough outlines effectively.
There are great wireframing tools out there. I suggest and use a couple of them throughout this book, however, in the first days of your project, every hour matters.
Granted, this kind of rough design may never leave the boundaries of your team, but please know that nothing beats getting that instantaneous feedback and collaboration by putting your ideas down on paper or a whiteboard.
No matter how small or large your project is, it is critical to start with a sound architecture that can scale if duty calls. Most of the time, you can't accurately predict the size of your project ahead of time. Sticking to the architectural fundamentals discussed in Chapter 1, Introduction to Angular and Its Concepts, results in an architecture that is not overly burdensome, so you can quickly execute a simple app idea. The key is to ensure proper decoupling from the get-go.
In my view, there are two types of decoupling. One is soft-decoupling, where a "Gentlemen's Agreement" is made not to mix concerns and you try and not mess up the code base. This can apply to the code you write, all the way to infrastructure-level interactions. If you maintain your frontend code under the same code structure as your backend code, and if you let your REST server serve up your frontend application, then you are only practicing soft-decoupling.
You should instead practice hard-decoupling, which means frontend code lives in a separate repository, never calls the database directly, and is hosted on its web server altogether. This way, you can be sure that, at all times, your REST APIs or your frontend code is entirely replaceable and independent of other code. Practicing hard-decoupling has monetary and security benefits as well. The serving and scaling needs of your frontend application are guaranteed to be different from your backend, so you can optimize your host environment appropriately and save money. If you whitelist access to your REST APIs to only the calls originating from your frontend servers, you will vastly improve your security. Consider the following high-level architecture diagram for our LocalCast Weather app:
Figure 3.6: LocalCast high-level architecture
The high-level architecture shows that our Angular web application is completely decoupled from any backend. It is hosted on its web server, can communicate with a web API such as OpenWeatherMap, or optionally be paired with a backend infrastructure to unlock rich and customized features that a web API alone can't provide, such as storing per-user preferences or complementing the OpenWeatherMap API's dataset with our own.
Regardless of your backend technology, I recommend that your frontend always resides in its repository, and is served using its web server that does not depend on your API server.
In Chapter 10, RESTful APIs and Full-Stack Implementation, you'll deep-dive into learning how a MEAN stack application, using MongoDB, Express, Angular, and Node, comes together in practice.
Now that we have our features, wireframe designs, and high-level architecture in place, we can start implementing our app.
In Chapter 2, Setting Up Your Development Environment, you should have created an Angular application. We'll use that as our starting point. If you haven't done so, please go back to Chapter 2, Setting Up Your Development Environment, and create your project.
In this section, you'll leverage Angular components, interfaces, and services to build the current weather feature in a decoupled, cohesive, and encapsulated manner.
The landing page of an Angular app, by default, resides in app.component.html
. So, start by editing the template of AppComponent
with basic HTML, laying out the initial landing experience for the application.
We are now beginning the development of Feature 1: Display Current Location weather information for the current day so you can move the card in the Github project to the In progress column.
app.component.html
h1
tag, followed by the tagline of our app as a div
, and placeholders for where we may want to display the current weather, demonstrated as shown in the following code block:
src/app/app.component.html
<div style="text-align:center">
<h1>
LocalCast Weather
</h1>
<div>Your city, your forecast, right now!</div>
<h2>Current Weather</h2>
<div>current weather</div>
</div>
title
property from the component
class, so it's empty
src/app/app.component.ts
import { Component } from '@angular/core'
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {}
npm start
http://localhost:5000
on your browserYou should now be able to observe the changes you're making in real time in the browser.
Note that you should use the integrated terminal within VS Code to run commands, so you don't have to jump around different windows. Use [CTRL+`] on Windows or [^+`] on Mac to bring the terminal up. In case you're not familiar, ` is a backtick and is usually on the same key as ~ (tilde).
We need to display the current weather information, where <div>current weather</div>
is located. To achieve this, we need to build a component that is responsible for displaying the weather data.
The reason behind creating a separate component is an architectural best practice that is codified in the Model-View-ViewModel (MVVM) design pattern. You may have heard of the Model-View-Controller (MVC) pattern before. The vast majority of web-based code written circa 2005-2015 was written following the MVC pattern. MVVM differs from the MVC pattern in meaningful ways, as I explained in my 2013 article on DevPro:
An effective implementation of MVVM inherently enforces proper separation of concerns. Business logic is clearly separated from presentation logic. So, when a View is developed, it stays developed, because fixing a bug in one View's functionality doesn't impact other views. On the flip side, if [you use] visual inheritance effectively and [create] reusable user controls, fixing a bug in one place can fix issues throughout the application.
Angular provides a practical implementation of MVVM:
ViewModels neatly encapsulate any presentation logic and allow for simpler View code by acting as a specialized version of the model. The relationship between a View and ViewModel is straightforward, allowing more natural ways to wrap UI behavior in reusable user controls.
You can read more about the architectural nuance, with illustrations, at http://bit.ly/MVVMvsMVC.
Next, you create your very first Angular component, which includes the View and the ViewModel, using the Angular CLI's ng generate
command:
npx ng generate component current-weather
Ensure that you are executing ng
commands under the local-weather-app
folder, and not under the parent folder where you initialized the project. Also, note that npx ng generate component current-weather
can be rewritten as ng g c current-weather
. This book utilizes the shorthand format going forward and expects you to prepend npx
, if necessary.
app
folder:
src/app
├── app.component.css
├── app.component.html
├── app.component.spec.ts
├── app.component.ts
├── app.module.ts
├── current-weather
├── current-weather.component.css
├── current-weather.component.html
├── current-weather.component.spec.ts
└── current-weather.component.ts
A generated component has four parts:
current-weather.component.css
contains any CSS that is specific to the component and is an optional file.current-weather.component.html
contains the HTML template that defines the look of the component and rendering of the bindings and can be considered the View, in combination with any CSS styles used.current-weather.component.spec.ts
contains Jasmine-based unit tests that you can extend to test your component functionality.current-weather.component.ts
contains the @Component
decorator above the class definition and is the glue that ties together the CSS, HTML, and JavaScript code. The class itself can be considered the ViewModel, pulling data from services and performing any necessary transformations to expose sensible bindings for the View, shown as follows:src/app/current-weather/current-weather.component.ts
import { Component, OnInit } from '@angular/core'
@Component({
selector: 'app-current-weather',
templateUrl: './current-weather.component.html',
styleUrls: ['./current-weather.component.css'],
})
export class CurrentWeatherComponent implements OnInit {
constructor() {}
ngOnInit() {}
}
If the component you're planning to write is a simple one, you can write it using inline styles and an inline template to simplify the structure of your code. If we were to rewrite the component above using inline templates and styles, it would look like the following example:
example
import { Component, OnInit } from '@angular/core'
@Component({
selector: 'app-current-weather',
template: `
<p>
current-weather works!
</p>
`,
styles: []
})
export class CurrentWeatherComponent implements OnInit {
constructor() {}
ngOnInit() {}
}
However, we won't be inlining this template. So, keep your generated code as-is.
Note that the template is surrounded by the backtick character, `, instead of a single-quote character. The backtick character defines a template literal, which allows newlines to be defined without having to concatenate strings with a plus operator. You can read more about template literals at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals.
When you executed the generate
command, in addition to creating the component, the command also added the new component you created in the app's root module, app.module.ts
, avoiding the otherwise tedious task of wiring up components together:
src/app/app.module.ts
...
import {
CurrentWeatherComponent
} from './current-weather/ current-weather.component'
...
@NgModule({
declarations: [
AppComponent,
CurrentWeatherComponent
],
...
The bootstrap process of Angular is, admittedly, a bit convoluted. This is the chief reason the Angular CLI exists. index.html
contains an element named <app-root>
. When Angular begins execution, it first loads main.ts
, which configures the framework for browser use and loads the app module. The app module then loads all its dependencies and renders within the aforementioned <app-root>
element. In Chapter 7, Creating a Router-First Line-of-Business App, when we build a line-of-business app, we create feature modules to take advantage of the scalability features of Angular.
Now, we need to display our new component on the initial AppComponent
template, so it is visible to the end user.
CurrentWeatherComponent
to AppComponent
by replacing <div>current weather</div>
with <app-current-weather></app-current-weather>
:
src/app/app.component.html
<div style="text-align:center">
<h1>
LocalCast Weather
</h1>
<div>Your city, your forecast, right now!</div>
<h2>Current Weather</h2>
<app-current-weather></app-current-weather>
</div>
Figure 3.7: Initial render of your Local Weather app
Note the icon and name in the tab of the browser window. As a web development norm, in the index.html
file, update the <title>
tag and the favicon.ico
file with the name and icon of your application to customize the browser tab information. If your favicon doesn't update, append the href
attribute with a unique version number, such as href="favicon.ico?v=2"
. As a result, your app will start to look like a real web app, instead of a CLI-generated starter project.
Now that you have seen an Angular component in action, let's cover some basics of what is going on under the covers.
As discussed in Chapter 1, Introduction to Angular and Its Concepts, an Angular component is implemented as an ES2015 class, which allows us to leverage OOP concepts. Classes are traditionally present in strongly-typed languages, so it is excellent that JavaScript implements classes as a dynamically typed language. Classes allow us to group (encapsulate) functionality and behavior in self-contained units (objects). We can define the behavior in very generalized and abstract ways and implement an inheritance hierarchy to share and morph behavior into differing implementations.
Considering the CurrentWeatherComponent
class that follows, I can highlight some benefits of classes:
@Component(...)
export class CurrentWeatherComponent implements OnInit {
constructor() {}
ngOnInit() {}
}
Unlike a function, you can't directly use code within a class. It must be instantiated as an object with the new keyword. This means that we can have multiple instances of any given class and each object can maintain its internal state. In this case, Angular instantiates a component for us behind the scenes. A constructor
of a class is executed at the time of its instantiation. You can put any code that initializes other classes or variables inside a constructor. However, you shouldn't make an HTTP call or attempt to access DOM elements from a constructor. This is where the OnInit
life cycle hook comes into play.
As Angular is initializing CurrentWeatherComponent
as an object, it is also going through the entire graph of modules, components, services, and other dependencies to ensure all interdependent code is loaded into memory. During this time, Angular can't yet guarantee the availability of HTTP or DOM access. After all classes are instantiated, Angular goes through the classes that are decorated with @Component
, implements the OnInit
interface, and calls the ngOnInit
function within our class. This is why we need to put any code that needs HTTP or DOM access during the first load of our component into ngOnInit
.
Classes can have properties, variables, and functions. From an Angular template, you can access any property, variable, or function inside of an expression. The syntax of an expression looks like {{ expression }}, [target]="expression", (event)="expression"
or *ngIf="expression"
.
Now you have a good understanding of how the code, or the ViewModel, behind the template, the View, is instantiated and how you can access that code from the template. In the next section, we'll build an interface, which is a contract that defines the shape of an object.
Now that your View and ViewModel are in place, you need to define your model. If you look back on the design, you'll see that the component needs to display:
You first need to create an interface that represents this data structure. We are creating an interface instead of a class because an interface is an abstraction that does not contain any implementation. When creating touchpoints or passing data between various components, we can ensure a decoupled design if we rely on an abstract definition over an object that may implement unpredictable custom behavior, leading to bugs.
Start by creating the interface:
npx ng generate interface ICurrentWeather
icurrent-weather.ts
with an empty interface definition that looks like this:
src/app/icurrent-weather.ts
export interface ICurrentWeather {
}
This is not an ideal setup, since we may add numerous interfaces to our app, and it can get tedious tracking down various interfaces. Over time, as you add concrete implementations of these interfaces as classes, it makes sense to put classes and their interfaces in their files.
Why not just call the interface CurrentWeather
? This is because, later on, we may create a class to implement some interesting behavior for CurrentWeather
. Interfaces establish a contract, establishing the list of available properties on any class or interface that implements or extends the interface. It is always important to be aware of when you're using a class versus an interface. If you follow the best practice of always starting your interface names with a capital I
, you will always be conscious of what type of object you are passing around. Hence, the interface is named ICurrentWeather
.
icurrent-weather.ts
to interfaces.ts
src/app/interfaces.ts
export interface ICurrentWeather {
city: string
country: string
date: Date
image: string
temperature: number
description: string
}
This interface and its eventual concrete representation as a class is the Model in MVVM. So far, I have highlighted how various parts of Angular fit the MVVM pattern; going forward, I'll refer to these parts by their actual names.
Now, we can import the interface into the component and start wiring up the bindings in the template of CurrentWeatherComponent
.
ICurrentWeather
templateUrl
and styleUrls
current
with type ICurrentWeather
:
src/app/current-weather/current-weather.component.ts
import { Component, OnInit } from '@angular/core'
import { ICurrentWeather } from '../interfaces'
@Component({
selector: 'app-current-weather',
templateUrl: './current-weather.component.html',
styleUrls: ['./current-weather.component.css'],
})
export class CurrentWeatherComponent implements OnInit {
current: ICurrentWeather
constructor() {}
ngOnInit() {}
}
If you just type current:ICurrentWeather
, you can use the Auto Fixer in VS Code to automatically insert the import
statement.
In the constructor, you need to temporarily populate the current
property with dummy data to test your bindings.
ICurrentWeather
using the as
operator:
src/app/current-weather/current-weather.component.ts
...
constructor() {
this.current = {
city: 'Bethesda',
country: 'US',
date: new Date(),
image: 'assets/img/sunny.svg',
temperature: 72,
description: 'sunny',
} as ICurrentWeather
}
...
In the src/assets
folder, create a subfolder named img
and place an image of your choice to reference in your dummy data.
You may forget the exact properties in the interface you created. You can get a quick peek at them by holding Ctrl + hovering over the interface name with your mouse, as shown:
Figure 3.8: Ctrl + hover over the interface
Now, update the template to wire up your bindings with a basic HTML-based layout.
src/app/current-weather/current-weather.component.html
<div>
...
</div>
div
, define another div
to display the city and country information using binding:
<div>
<span>{{current.city}}, {{current.country}}</span>
...
</div>
Note that within the span
, you can use static text to position the two properties. In this case, the city
and country
are separated by a comma, followed by a space.
city
and country
, display the date
using binding and a DatePipe
to define a display format for the property:
<span>{{current.date | date:'fullDate'}}</span>
To change the display formatting of current.date
, we used the DatePipe
above, passing in 'fullDate'
as the format option. In Angular, various out-of-the-box and custom pipe |
operators can be used to change the appearance of data without actually changing the underlying data. This is a very powerful, convenient, and flexible system to share such user interface logic without writing repetitive boilerplate code.
In the preceding example, we could pass in 'shortDate'
if we wanted to represent the current date in a more compact form. For more information on various DatePipe
options, refer to the documentation at https://angular.io/api/common/DatePipe.
div
to display the temperature information, formatting the value using DecimalPipe
and bind an image of the current weather to an img
tag:
<div>
<img [src]='current.image'>
<span>{{current.temperature | number:'1.0-0'}}˚F</span>
</div>
We bind the image property to the img
tag's src
attribute using the square bracket syntax. Next, we format current.temperature
so that no fractional values are shown, using DecimalPipe
. The documentation is at https://angular.io/api/common/DecimalPipe.
Note that you can render ˚C and ˚F using their respective HTML codes: ℃
for ˚C and ℉
; for ˚F.
div
to display the description property:
<div>
{{current.description}}
</div>
src/app/current-weather/current-weather.component.html
<div>
<div>
<span>{{current.city}}, {{current.country}}</span>
<span>{{current.date | date:'fullDate'}}</span>
</div>
<div>
<img [src]='current.image'>
<span>{{current.temperature | number:'1.0-0'}}˚F</span>
</div>
<div>
{{current.description}}
</div>
</div>
Figure 3.9: App after wiring up bindings with dummy data
Congratulations – you have successfully wired up your first component!
Now let's update the app so that we can pull live weather data from a Web API.
Now you need to connect your CurrentWeather
component to the OpenWeatherMap
APIs to pull live weather data. However, we don't want to insert this code directly into our component. If we did this, we would have to update the component if the API changed. Now imagine an app with dozens or hundreds of views and imagine how this would create a significant maintainability challenge.
Instead, we'll leverage an Angular service, a singleton class, which can provide the current weather information to our component and abstract away the source of the data. The abstraction decouples the UI from the Web API. Leveraging this separation of concerns, in the future, we could enhance our service to pull from multiple APIs or a local cache to load weather information without having to change the UI code.
In the upcoming sections, we'll go over the following steps to accomplish this goal:
HttpClientModule
and injecting it into the serviceOpenWeatherMap
APIget
requestCurrentWeather
componentngOnInit
function of the CurrentWeather
componentICurrentWeather
type using RxJS functions so that your component can consume itAny code that goes outside of the boundaries of a component should exist in a service; this includes inter-component communication (unless there's a parent-child relationship), API calls of any kind, and any code that caches or retrieves data from a cookie or the browser's localStorage
. This is a critical architectural pattern that keeps your application maintainable in the long term. I expand upon this idea in my DevPro MVVM article at link https://www.itprotoday.com/microsoft-visualstudio/mvvm-and-net-great-combo-web-application-development.
To create an Angular service, use the Angular CLI:
npx ng g s weather --flat false
weather
folder that's created:
src/app
...
└── weather
├── weather.service.spec.ts
└── weather.service.ts
A CLI-generated service has two parts:
weather.service.spec.ts
contains Jasmine-based unit tests that you can extend to test your service's functionality.weather.service.ts
contains the @Injectable
decorator above the class definition, which makes it possible to inject this service into other components, leveraging Angular's provider system. This ensures that our service is a singleton, meaning it is instantiated once, no matter how many times it is injected elsewhere.The service is generated as shown here:
src/app/weather/weather.service.ts
import { Injectable } from '@angular/core'
@Injectable({
providedIn: 'root',
})
export class WeatherService {
constructor() {}
}
Note that the providedIn
property ensures that the root module provides the weather service in app.module.ts
.
Next, let's see the dependency injection mechanism in Angular, which allows services and modules to be used by other services, components, or modules without the developer having to manage the instantiation of the shared objects.
To make API calls, you need to leverage the HttpClient
module in Angular. The official documentation (https://angular.io/guide/http) explains the benefits of this module succinctly:
"With HttpClient, @angular/common/http provides a simplified API for HTTP functionality for use with Angular applications, building on top of the XMLHttpRequest interface exposed by browsers. Additional benefits of HttpClient include testability support, strong typing of request and response objects, request and response interceptor support, and better error handling via APIs based on Observables."
Let's start by importing the HttpClientModule
into our app so we can inject the HttpClient
provided by the module into the WeatherService
:
HttpClientModule
to app.module.ts
, as follows:
src/app/app.module.ts
...
import { HttpClientModule } from '@angular/common/http'
...
@NgModule({
...
imports: [..., HttpClientModule]
...
})
HttpClient
, provided by the HttpClientModule
in the WeatherService
, as follows:
src/app/weather/weather.service.ts
import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core'
@Injectable()
export class WeatherService {
constructor(private httpClient: HttpClient) {}
}
Now, httpClient
is ready for use in your service.
Since httpClient
is strongly typed, we need to create a new interface that conforms to the shape of the API we'll call. To be able to do this, you need to familiarize yourself with the Current Weather Data API:
Figure 3.10: OpenWeatherMap Current Weather Data API documentation
You need to use the API named By city name, which allows you to get current weather data by providing the city name as a parameter so that our web request looks as follows:
api.openweathermap.org/data/2.5/weather?q={city name},{country code}
http://samples.openweathermap.org/data/2.5/weather?q=London,uk&appid=b1b15e88fa797225412429c1c50c122a1
{
"coord": {
"lon": -0.13,
"lat": 51.51
},
"weather": [
{
"id": 300,
"main": "Drizzle",
"description": "light intensity drizzle",
"icon": "09d"
}
],
"base": "stations",
"main": {
"temp": 280.32,
"pressure": 1012,
"humidity": 81,
"temp_min": 279.15,
"temp_max": 281.15
},
"visibility": 10000,
"wind": {
"speed": 4.1,
"deg": 80
},
"clouds": {
"all": 90
},
"dt": 1485789600,
"sys": {
"type": 1,
"id": 5091,
"message": 0.0103,
"country": "GB",
"sunrise": 1485762037,
"sunset": 1485794875
},
"id": 2643743,
"name": "London",
"cod": 200
}
Given the existing ICurrentWeather
interface that you have already created, this response contains more information than you need. You need to write a new interface that conforms to the shape of this response, but only specify the pieces of data you intend to use. This interface only exists in the WeatherService
and we won't export it since the other parts of the application don't need to know about this type.
ICurrentWeatherData
in weather.service.ts
between the import
and @Injectable
statementssrc/app/weather/weather.service.ts
interface ICurrentWeatherData {
weather: [{
description: string,
icon: string
}],
main: {
temp: number
},
sys: {
country: string
},
dt: number,
name: string
}
With the ICurrentWeatherData
interface, we are defining new anonymous types by adding children objects to the interface with varying structures. Each of these objects can be individually extracted out and defined as their own named interface. Especially note that weather
is an array of the anonymous type that has the description
and icon
properties.
Next, let's learn how you can introduce environment variables into your Angular application, so the test and production versions of your app can rely on different values.
It's easy to miss, but the sample URL in the previous sections—http://samples.openweathermap.org/data/2.5/weather?q=London,uk&appid=b1b15e88fa797225412429c1c50c122a1
—contains a required appid
parameter. You must store this key in your Angular app. You can store it in the weather service, but in reality, applications need to be able to target different sets of resources as they move from development to testing, staging, and production environments. Out of the box, Angular provides two environments: one prod
and the other one as the default.
Before you can continue, you need to sign up for a free OpenWeatherMap
account and retrieve your appid
. You can read the documentation for appid
at http://openweathermap.org/appid for more detailed information.
appid
, which is a long string of characters and numbersappid
in environment.ts
baseUrl
for later use:
src/environments/environment.ts
export const environment = {
production: false,
appId: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
baseUrl: 'http://',
}
In code, we use a camel-case appId
to keep our coding style consistent.
Since URL parameters are case-insensitive, appId
works as well as appid
.
Next, let's implement an HTTP GET to get the current weather data.
Now, we can implement the GET call in the WeatherService
class:
WeatherService
class named getCurrentWeather
environment
objecthttpClient.get
functionsrc/app/weather/weather.service.ts
import { HttpClient } from '@angular/common/http'
import { environment } from '../../environments/environment'
...
export class WeatherService {
constructor(
private httpClient: HttpClient
) { }
getCurrentWeather(city: string, country: string) {
return this.httpClient
.get<ICurrentWeatherData>(
`${environment.baseUrl}api.openweathermap.org/data/2.5/weather?` +
`q=${city},${country}&appid=${environment.appId}`
)
}
}
Note the use of ES2015's String Interpolation feature. Instead of building your string by appending variables to one another like environment.baseUrl + 'api.openweathermap.org/data/2.5/weather?q=' + city + ',' + country + '&appid=' + environment.appId
, you can use the backtick syntax to wrap `your string`
. Inside the backticks, you can have newlines and directly embed variables in the flow of your string by wrapping them with the ${dollarbracket}
syntax. However, when you introduce a newline in your code, it is interpreted as a literal newline \n
. To break up the string in your code, you can add a backslash \
, but then the next line of your code can have no indentation. It is easier to just concatenate multiple templates, as shown in the preceding code sample.
Using a long and complicated string is an error-prone process. Instead, we can use the HttpParams
object to build the URL programmatically.
HttpParams
to simplify the URL:
src/app/weather/weather.service.ts
import { HttpClient, HttpParams } from '@angular/common/http'
import { environment } from '../../environments/environment'
...
export class WeatherService {
constructor(private httpClient: HttpClient) { }
getCurrentWeather(city: string, country: string) {
const uriParams = new HttpParams()
.set('q', `${city},${country}`)
.set('appid', environment.appId)
return this.httpClient
.get<ICurrentWeatherData>(
`${environment.baseUrl}api.openweathermap.org/data/2.5/weather`,
{ params: uriParams }
)
}
}
Now let's connect the dots so that we can get the current weather data from the CurrentWeather component leveraging the Weather service.
To be able to use the getCurrentWeather
function in the CurrentWeather
component, you need to inject the service into the component:
WeatherService
into the constructor of the CurrentWeatherComponent
classsrc/app/current-weather/current-weather.component.ts
constructor(private weatherService: WeatherService) { }
Note the use of TypeScript generics with the get
function using the caret syntax, like <TypeName>
. Using generics is a development-time quality-of-life feature. By providing the type information to the function, input and/or return variable types of that function display as you write your code and are validated during development and also at compile time.
getCurrentWeather
function inside the ngOnInit
function:
src/app/current-weather/current-weather.component.ts
ngOnInit() {
this.weatherService.getCurrentWeather('Bethesda', 'US')
.subscribe((data) => this.current = data)
}
Fair warning: do not expect this code to be working just yet, because data
is of type ICurrentWeatherData
and current
is of type ICurrentWeather
. You can observe the error, which should say "error TS2322: Type 'Observable<ICurrentWeatherData>' is not assignable to type 'Observable<ICurrentWeather>'
." Let's look at what's goes in the next segment.
Angular components have a rich collection of life cycle hooks that allow you to inject your custom behavior when a component is being rendered, refreshed, or destroyed. ngOnInit()
is the most common life cycle hook you're going to use. It is only called once, when a component is first instantiated or visited. This is where you want to perform your service calls. For a deeper understanding of component life cycle hooks, check out the documentation at https://angular.io/guide/lifecycle-hooks.
Note that the anonymous function you have passed to subscribe is an ES2015 arrow function. If you're not familiar with arrow functions, it may be confusing at first. Arrow functions are quite elegant and simple. Consider the following arrow function:
(data) => { this.current = data }
You can rewrite it simply as:
function(data) { this.current = data }
There's a special condition—when you write an arrow function that transforms a piece of data, such as:
(data) => { data.main.temp }
This function effectively takes ICurrentWeatherData
as an input and returns the temp
property. The return statement is implicit. If you rewrite it as a regular function, it looks as follows:
function(data) { return data.main.temp }
When the CurrentWeather
component loads, ngOnInit
fires once, which calls the getCurrentWeather
function, which returns an object with the type Observable<ICurrentWeatherData>
.
An Observable is the most basic building block of RxJS and represents an event emitter, which emits any data received over time with the type of ICurrentWeatherData
as described in the official documentation.
The Observable object by itself is benign and won't send a request over the network unless it is being listened to. You can read more about Observables at https://reactivex.io/rxjs/class/es6/Observable.js~Observable.html.
By calling .subscribe
on the Observable, you're essentially attaching a listener to the emitter. You've implemented an anonymous function within the subscribe
method, which gets executed whenever a new piece of data is received and an event is emitted. The anonymous function takes a data object as a parameter, and the specific implementation, in this case, assigns the piece of data to the local variable named current
. Whenever current
is updated, the template bindings you implemented earlier pull in the new data and render it on the View. Even though ngOnInit
executes only once, the subscription to the Observable persists. So, whenever there's new data, the current variable updates and the View rerenders to display the latest data.
The root cause of the error at hand is that the data that is being emitted is of type ICurrentWeatherData
; however, our component only understands data that is shaped as described by the ICurrentWeather
interface. In the next section, you'll need to dig deeper into RxJS to understand how best to accomplish that task.
Beware, VS Code and the CLI sometimes stop working. As previously noted, as you code, the npm start
command is running in the integrated terminal of VS Code. The Angular CLI, in combination with the Angular Language Service plugin, continuously watches for code changes and transpiles your TypeScript code to JavaScript so that you can observe your changes with live-reloading in the browser. The great thing is that when you make coding errors, in addition to the red underlining in VS Code, you also see some red text in the terminal, or even the browser, because the transpilation has failed. In most cases, when correcting the error, the red underlining goes away and the Angular CLI automatically re-transpiles your code and everything works. However, in specific scenarios, note that VS Code fails to pick typing changes in the IDE so that you won't get autocompletion help, or the CLI tool may get stuck with a message saying webpack: Failed to compile.
You have two main strategies to recover from such conditions:
npm start
.Let's resolve the type mismatch issue by transforming the shape of the data.
We are going to use an RxJS reactive pipe (or data stream) to reshape the structure of data coming from the external API to fit the shape of the data we expect within our Angular app. If we don't do this, then our code will fail due to a type mismatch error.
Refer to Chapter 1, Introduction to Angular and Its Concepts, to get a deeper understanding of RxJS and reactive programming.
To avoid future mistakes such as returning an unintended type of data from your service, you need to update the getCurrentWeather
function to define the return type as Observable<ICurrentWeather>
and import the Observable
type, as shown:
src/app/weather/weather.service.ts
import { Observable } from 'rxjs'
import { ICurrentWeather } from '../interfaces'
...
export class WeatherService {
...
getCurrentWeather(city: string, country: string):
Observable<ICurrentWeather> {
}
...
}
Now, VS Code lets you know that the type Observable<ICurrentWeatherData>
is not assignable to the type Observable<ICurrentWeather>
:
transformToICurrentWeather
that can convert ICurrentWeatherData
to ICurrentWeather
convertKelvinToFahrenheit
that converts the API-provided Kelvin temperature to Fahrenheit:
src/app/weather/weather.service.ts
export class WeatherService {
...
private transformToICurrentWeather(data: ICurrentWeatherData): ICurrentWeather {
return {
city: data.name,
country: data.sys.country,
date: data.dt * 1000,
image:
`http://openweathermap.org/img/w/${data.weather[0].icon}.png`,
temperature: this.convertKelvinToFahrenheit(data.main.temp),
description: data.weather[0].description,
}
}
private convertKelvinToFahrenheit(kelvin: number): number
{
return kelvin * 9 / 5 - 459.67
}
}
Note that you need to be converting the icon property to an image URL at this stage. Doing this in the service helps preserve encapsulation; binding the icon value to the URL in the View template breaks the Separation of Concerns (SoC) principle. If you wish to create truly modular, reusable, and maintainable components, you must remain vigilant and strict in terms of enforcing SoC. The documentation for Weather Icons and details of how the URL should be formed, including all the available icons, can be found at http://openweathermap.org/weather-conditions.
On a separate note, the argument could be made that Kelvin to Fahrenheit conversion is a View concern, but we have implemented it in the service. This argument holds water, especially considering that we have a planned feature to be able to toggle between Celsius and Fahrenheit. A counter-argument would be that, at this time, we only need to display temperatures in Fahrenheit and it is part of the job of the weather service to be able to convert the units. This argument makes sense as well. The ultimate implementation is to write a custom Angular pipe and apply it in the template. A pipe can easily bind with the planned toggle button as well.
However, at this time, we only need to display temperatures in Fahrenheit, and I would err on the side of not over-engineering a solution.
ICurrentWeather.date
to the number
typeWhile writing the transformation function, note that the API returns the date as a number. This number represents the amount of time in seconds since the Unix epoch (timestamp), which is January 1, 1970 00:00:00 UTC. However, ICurrentWeather
expects a Date
object. It is easy enough to convert the timestamp by passing it into the constructor of the Date
object like a new Date(data.dt)
. This is fine, but also unnecessary since Angular's DatePipe
can directly work with the timestamp. In the name of relentless simplicity and maximally leveraging the functionality of the frameworks we use, we update ICurrentWeather
to use number. There's also a performance and memory benefit to this approach if you're transforming massive amounts of data, but that concern is not applicable here. There's one caveat—JavaScript's timestamp is in milliseconds, but the server value is in seconds, so a simple multiplication during the transformation is still required.
map
operator right below the other import
statements:
src/app/weather/weather.service.ts
import { map } from 'rxjs/operators'
It may seem odd to have to manually import the map
operator. RxJS is a capable framework with a wide API surface. An Observable
alone has over 200 methods attached to it. Including all of these methods by default creates development time issues with too many functions to choose from and also negatively impacts the size of the final deliverable, including app performance and memory use. You must add each operator you intend to use individually.
map
function to the data stream returned by the httpClient.get
method through a pipe
transformToICurrentWeather
function:
src/app/weather/weather.service.ts
...
return this.httpClient
.get<ICurrentWeatherData>(
`${environment.baseUrl}api.openweathermap.org/data/2.5/weather`,
{ params: uriParams }
)
.pipe(map(data => this.transformToICurrentWeather(data)))
...
Now incoming data can be transformed as it flows through the stream, ensuring that the OpenWeatherMap
Current Weather API data is in the correct shape so that the CurrentWeather
component can consume it.
Figure 3.11: Displaying live data from OpenWeatherMap
You should see that your app is able to pull live data from OpenWeatherMap
and correctly transform server data into the format you expect.
You have completed the development of Feature 1: Display Current Location weather information for the current day. Commit your code!
Figure 3.12: GitHub project Kanban board status
Great work! You're now familiar with the fundamental architecture of Angular. You also started implementing code in the reactive paradigm by leveraging RxJS.
Now let's increase the resiliency of our app by guarding against null or undefined values that can break your application code.
In JavaScript, the undefined
and null
values are a persistent issue that must be proactively dealt with every step of the way. This is especially critical when dealing with external APIs and other libraries. If we don't deal with undefined
and null
values, then your app may present badly rendered views, console errors, issues with business logic, or even a crash of your entire app.
There are multiple strategies to guard against null values in Angular:
?.
*ngIf
You may use one or more of these strategies. However, in the next few sections I demonstrate why the *ngIf
strategy is the optimal one to use.
To simulate the scenario of getting an empty response from the server, go ahead and comment out the getCurrentWeather
call in ngOnInit
of CurrentWeatherComponent
:
src/app/current-weather/current-weather.component.ts
ngOnInit(): void {
// this.weatherService
// .getCurrentWeather('Bethesda', 'US')
// .subscribe(data => (this.current = data))
}
Let's start with implementing the property initialization strategy to guard against null values.
In statically-typed languages such as Java, it is drilled into you that proper variable initialization/instantiation is the key to error-free operation. So, let's try that in CurrentWeatherComponent
by initializing current
with default values:
src/app/current-weather/current-weather.component.ts
constructor(private weatherService: WeatherService) {
this.current = {
city: '',
country: '',
date: 0,
image: '',
temperature: 0,
description: '',
}
}
The outcome of these changes reduces the number of console errors from two to zero. However, the app itself is not in a presentable state, as you can see here:
Figure 3.13: Results of property initialization
To make this View presentable to the user, we have to code with default values on every property on the template. So, by fixing the null guarding issue with initialization, we created a default value handling issue. Both the initialization and the default value handling are O(n) scale tasks for developers. At its best, this strategy is annoying to implement and at its worst, highly ineffective and error-prone, requiring, at a minimum, O(2n) effort per property.
Next, let's learn about Angular's safe navigation operator, which comes in handy when dealing with objects that are external to our application when we can't control which properties may be null or undefined.
Angular implements the safe navigation operation, ?.
, to prevent unintended traversals of undefined objects. So, instead of writing initialization code and having to deal with template values, we can just update the template.
Remove the property initialization code from the constructor and instead update the template as shown:
src/app/current-weather/current-weather.component.html
<div>
<div>
<span>{{current?.city}}, {{current?.country}}</span>
<span>{{current?.date | date:'fullDate'}}</span>
</div>
<div>
<img [src]='current?.image'>
<span>{{current?.temperature}}℉</span>
</div>
<div>
{{current?.description}}
</div>
</div>
This time, we didn't have to make up defaults, and we let Angular deal with displaying undefined bindings. The app itself is in somewhat better shape. There's no more confusing data being displayed; however, it still is not in a presentable state, as shown here:
Figure 3.14: Results of using the safe navigation operator
You can probably imagine ways in which the safe navigation operator could come in handy, in far more complicated scenarios. However, when deployed at scale, this type of coding still requires, at a minimum, O(n) level of effort to implement.
When presenting data to the user, we don't want to present empty values. The easiest way to clean up the UI would be to leverage the ngIf
directive to hide the entire div
.
The ideal strategy is to use *ngIf
, which is a structural directive, meaning Angular stops traversing DOM tree elements beyond a falsy statement.
In the CurrentWeather
component, we can easily check to see whether the current
variable is null or undefined before attempting to render the template:
div
element with *ngIf
to check whether current
is an object, as shown:
src/app/current-weather/current-weather.component.html
<div *ngIf="!current">
no data
</div>
<div *ngIf="current">
...
</div>
Now observe the console log and that no errors are being reported. You should always ensure that your Angular application reports zero console errors. If you're still seeing errors in the console log, ensure that you have correctly reverted the OpenWeather
URL to its correct state or kill and restart your npm start
process. I highly recommend that you resolve any console errors before moving on.
Figure 3.15: Results of using null guarding with *ngIf
getCurrentWeather
call in ngOnInit
of CurrentWeatherComponent
:
src/app/current-weather/current-weather.component.ts
ngOnInit(): void {
this.weatherService
.getCurrentWeather('Bethesda', 'US')
.subscribe(data => (this.current = data))
}
With null guarding, you can ensure that your UI always looks professional.
Congratulations! In this chapter, you created your first Angular application with a flexible architecture while avoiding over-engineering. This was possible because we first built a roadmap and codified it in a Kanban board that is visible to your peers and colleagues. We stayed focused on implementing the first feature we put in progress and didn't deviate from the plan.
You learned how to avoid coding mistakes by proactively declaring the input and return types of functions and working with generic functions. You used the date and decimal pipes to ensure that data is formatted as desired while keeping formatting-related concerns mostly in the template, where this kind of logic belongs.
Finally, you used interfaces to communicate between components and services without leaking the external data structure to internal components. By applying all these techniques in combination, which Angular, RxJS, and TypeScript allowed us to do, you ensured proper separation of concerns and encapsulation. As a result, the CurrentWeather
component is now truly reusable and composable; this is not an easy feat to achieve.
If you don't ship it, it never happened. In the next chapter, we'll prepare this Angular app for a production release by troubleshooting application errors, ensuring automated unit and e2e tests pass, and containerizing the Angular app with Docker so that it can be published on the web.
Answer the following questions as best as you can to ensure that you've understood the key concepts from this chapter without Googling. Do you need help answering the questions? See Appendix D, Self-Assessment Answers online at https://static.packt-cdn.com/downloads/9781838648800_Appendix_D_Self-Assessment_Answers.pdf or visit https://expertlysimple.io/angular-self-assessment.