In this chapter, as we update Angular, we’ll learn the following:
By the end of this chapter, you’ll understand:
Let’s get started!
To support a thriving ecosystem, Angular needs to be both stable and evolutionary.
On one hand, Angular aims to provide developers with maximum stability for mission-critical applications. On the other hand, it constantly needs to adapt and advance to support the latest changes in web technologies.
Therefore, the Angular team has decided to use a time-based release cycle with semantic versioning.
A time-based release cycle means that we can expect new versions of Angular (Angular 5, Angular 6, Angular 7, etc.) every couple of weeks or months.
Semantic versioning means that the version number of Angular allows us to predict whether or not it will break our application if we upgrade to it.
In essence, a semantic version looks like this: Major.Minor.Patch
.
So version v1.3.8
has a major component with a value of 1, a minor component with a value of 3 and a patch component with a value of 1.
When a new version is released, the new version implicitly indicates the type of change that was made to the code.
The following rules are applied when a semantic version is increased:
Each increment happens numerically with an increment of 1.
When a bug is fixed and the code stays backwards compatible, the patch component is increased:
v0.0.3 // Before bugfix
v0.0.4 // After bugfix
When functionality is added and the code stays backwards compatible, the minor component is increased and the patch component is reset to zero:
v0.2.4 // Before addition of new functionality
v0.3.0 // After addition of new functionality
When a change is implemented that causes the code to become backwards incompatible, also known as a breaking change, the major component is increased and the minor and patch components are reset to zero:
v7.3.5 // Before implementing backwards incompatible changes
v8.0.0 // After implementing backwards incompatible changes
If you’re not familiar with semantic versioning, make sure to check out this simple guide to semantic versioning.
The Angular team combines semantic versioning with a time-based release cycle to aim at:
The release schedule is not set in stone, as there may be holidays or special events, but it’s a good indicator of what we can expect in terms of upcoming versions.
You can follow the official Angular blog and the official change log to stay up to date on the latest developments.
A huge benefit of semantic versions is that we can safely update Angular applications with patch or minor releases without having to worry about breaking our applications.
But what if there’s a new major release?
We already learned that a major release can come with breaking changes. So how do we know if our existing application will break or not if we update it?
One way would be to read the official change log and go through the list of changes.
A much easier way is to use the Angular Update Guide to update Angular. You select your current version of Angular and the version you wish to upgrade to and the application tells you the exact steps you need to take.
For our Angular Todo application, we wish to upgrade from Angular 4.0 to Angular 5.0.
Let’s select app complexity level Advanced so we see all the possible measures we need to take:
We get a complete overview of all the steps we need to take to update our application.
How sweet is that!
The Before Updating list contains 12 items. None of the items apply to our Angular Todo application, so we can safely proceed to the next step.
From the During the Update list, only the last item applies to our application. We need to update our dependencies, so let’s run the proposed commands in the root of our project:
$ npm install @angular/{animations,common,compiler,compiler-cli,core,forms,http,platform-browser,platform-browser-dynamic,platform-server,router}@'^5.0.0' [email protected] rxjs@'^5.5.2'
$ npm install [email protected] --save-exact
Because we updated our Angular CLI to the latest version in the Up and Running section, we also update our local version:
$ npm install @angular/cli@latest --save-dev
To verify that our application runs correctly, we run:
$ ng serve
If ng serve
fails to start, try deleting your node_modules
directory and package-lock.json
file and run npm install
to recreate a clean node_modules
directory and package-lock.json
file.
The After the Update list contains four items, of which the first and the last apply to our application:
HttpModule
to HttpClientModule
rxjs/operators
and use the RxJS pipe operatorLet’s tackle them one by one.
The Angular Update Guide tells us that we should switch from from HttpModule
to HttpClientModule
.
If we inspect the Angular version 5.0.0 release notes, we learn that Angular 4.3 and later ships with a new HttpClient
that automatically handles JSON responses and supports HTTP Interceptors.
It states that, to update our code, we must replace HttpModule
with HttpClientModule
, inject the HttpClient
service and remove all map(res => res.json())
calls because the new HttpClient
automatically parses JSON responses.
Let’s open src/app/app.module.ts
and replace HttpModule
:
// ...
import { HttpModule } from '@angular/http';
@NgModule({
declarations: [
// ...
],
imports: [
// ...
HttpModule,
],
providers: [
// ...
],
bootstrap: [AppComponent]
})
export class AppModule {
}
with HttpClientModule
:
// ...
import { HttpClientModule } from '@angular/common/http';
@NgModule({
declarations: [
// ...
],
imports: [
// ...
HttpClientModule,
],
providers: [
// ...
],
bootstrap: [AppComponent]
})
export class AppModule {
}
Next, we must use the HttpClient
service instead of the Http
service and remove all map(res => res.json())
calls in our code because the new HttpClient
automatically parses the responses for us.
In Chaprter 3, we centralized all HTTP related code in a service called ApiService
, and we now reap the benefits of that approach.
As a result, we only have to update one file, so let’s open up src/app/api.service.ts
and replace:
import {
Http,
Headers,
RequestOptions,
Response
} from '@angular/http';
// ...
@Injectable()
export class ApiService {
constructor(
private http: Http,
private session: SessionService
) {
}
public signIn(username: string, password: string) {
return this.http
.post(API_URL + '/sign-in', {
username,
password
})
.map(response => response.json())
.catch(this.handleError);
}
public getAllTodos(): Observable<Todo[]> {
const options = this.getRequestOptions();
return this.http
.get(API_URL + '/todos', options)
.map(response => {
const todos = response.json();
return todos.map((todo) => new Todo(todo));
})
.catch(this.handleError);
}
public createTodo(todo: Todo): Observable<Todo> {
const options = this.getRequestOptions();
return this.http
.post(API_URL + '/todos', todo, options)
.map(response => {
return new Todo(response.json());
})
.catch(this.handleError);
}
public getTodoById(todoId: number): Observable<Todo> {
const options = this.getRequestOptions();
return this.http
.get(API_URL + '/todos/' + todoId, options)
.map(response => {
return new Todo(response.json());
})
.catch(this.handleError);
}
public updateTodo(todo: Todo): Observable<Todo> {
const options = this.getRequestOptions();
return this.http
.put(API_URL + '/todos/' + todo.id, todo, options)
.map(response => {
return new Todo(response.json());
})
.catch(this.handleError);
}
public deleteTodoById(todoId: number): Observable<null> {
const options = this.getRequestOptions();
return this.http
.delete(API_URL + '/todos/' + todoId, options)
.map(response => null)
.catch(this.handleError);
}
private handleError(error: Response | any) {
console.error('ApiService::handleError', error);
return Observable.throw(error);
}
private getRequestOptions() {
const headers = new Headers({
'Authorization': 'Bearer ' + this.session.accessToken
});
return new RequestOptions({ headers });
}
}
with
import {
HttpClient,
HttpErrorResponse,
HttpHeaders
} from '@angular/common/http';
// ...
@Injectable()
export class ApiService {
constructor(
private http: HttpClient,
private session: SessionService
) {
}
public signIn(username: string, password: string) {
return this.http
.post(API_URL + '/sign-in', {
username,
password
})
.catch(this.handleError);
}
public getAllTodos(): Observable<Todo[]> {
const options = this.getRequestOptions();
return this.http
.get(API_URL + '/todos', options)
.map(response => {
const todos = <any[]> response;
return todos.map((todo) => new Todo(todo));
})
.catch(this.handleError);
}
public createTodo(todo: Todo): Observable<Todo> {
const options = this.getRequestOptions();
return this.http
.post(API_URL + '/todos', todo, options)
.map(response => {
return new Todo(response);
})
.catch(this.handleError);
}
public getTodoById(todoId: number): Observable<Todo> {
const options = this.getRequestOptions();
return this.http
.get(API_URL + '/todos/' + todoId, options)
.map(response => {
return new Todo(response);
})
.catch(this.handleError);
}
public updateTodo(todo: Todo): Observable<Todo> {
const options = this.getRequestOptions();
return this.http
.put(API_URL + '/todos/' + todo.id, todo, options)
.map(response => {
return new Todo(response);
})
.catch(this.handleError);
}
public deleteTodoById(todoId: number): Observable<null> {
const options = this.getRequestOptions();
return this.http
.delete(API_URL + '/todos/' + todoId, options)
.map(response => null)
.catch(this.handleError);
}
// ...
}
We replace the old classes from HttpModule
with their new counterparts from HttpClientModule
.
More specifically, we replace:
import { Http, Headers, RequestOptions, Response } from '@angular/http';
with import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
Response
with HttpErrorResponse
Headers
with HttpHeaders
return new RequestOptions({ headers });
with return { headers };
If we run:
$ ng serve
and navigate our browser to http://localhost:4200
, we see that our application still works as expected, but now uses the HttpClientModule
behind the scenes.
Time to tackle item 2: import RxJS operators from rxjs/operators
and use the RxJS pipe operator.
Angular 5 was updated to use RxJS 5.5.2 or later.
As of version 5.5, RxJS ships with pipeable operators. The official documentation says:
A pipeable operator is any function that returns a function with the signature:
<T, R>(source: Observable<T>) => Observable<R>
…You pull in any operator you need from one spot, under
rxjs/operators
(plural!). It’s also recommended to pull in the Observable creation methods you need directly as shown below with range:import { range } from >'rxjs/observable/range'; import { map, filter, scan } from >'rxjs/operators'; const source$ = range(0, 10); source$.pipe( filter(x => x % 2 === 0), map(x => x + x), scan((acc, x) => acc + x, 0) ) .subscribe(x => console.log(x))
Although this sounds complicated, it essentially means that where we previously used chained methods:
source$
.operatorOne()
.operatorTwo()
.subscribe()
we should now import operators from rxjs/operators
and use the .pipe()
method to apply them:
source$
.pipe(
operatorOne(),
operatorTwo()
)
.subscribe()
The main benefits of pipeable operators are:
The .pipe()
method reduces the impact on our code to a minimum.
We have two items in our application that need to be refactored: our ApiService
and TodosComponent
.
First, let’s open up src/app/api.service.ts
to update our ApiService
:
// import operators from rxjs/operators
import { map } from 'rxjs/operators';
// ...
@Injectable()
export class ApiService {
constructor(
private http: HttpClient,
private session: SessionService
) {
}
// ...
// update .map() to .pipe(map())
public getAllTodos(): Observable<Todo[]> {
const options = this.getRequestOptions();
return this.http
.get(API_URL + '/todos', options)
.pipe(
map(response => {
const todos = <any[]> response;
return todos.map((todo) => new Todo(todo));
})
)
.catch(this.handleError);
}
// update .map() to .pipe(map())
public createTodo(todo: Todo): Observable<Todo> {
const options = this.getRequestOptions();
return this.http
.post(API_URL + '/todos', todo, options)
.pipe(
map(response => {
return new Todo(response);
})
)
.catch(this.handleError);
}
// update .map() to .pipe(map())
public getTodoById(todoId: number): Observable<Todo> {
const options = this.getRequestOptions();
return this.http
.get(API_URL + '/todos/' + todoId, options)
.pipe(
map(response => {
return new Todo(response);
})
)
.catch(this.handleError);
}
// update .map() to .pipe(map())
public updateTodo(todo: Todo): Observable<Todo> {
const options = this.getRequestOptions();
return this.http
.put(API_URL + '/todos/' + todo.id, todo, options)
.pipe(
map(response => {
return new Todo(response);
})
)
.catch(this.handleError);
}
}
We import the map
pipeable operator from rxjs/operators
and update all occurrences from .map(fn)
to .pipe(map(fn))
.
Next, let’s open up src/app/todos/todos.component.ts
to apply the same changes to TodosComponent
:
// import operators from rxjs/operators
import { map } from 'rxjs/operators';
// ...
@Component({
selector: 'app-todos',
templateUrl: './todos.component.html',
styleUrls: ['./todos.component.css']
})
export class TodosComponent implements OnInit {
// ...
// update .map() to .pipe(map())
public ngOnInit() {
this.route.data
.pipe(
map((data) => data['todos'])
)
.subscribe(
(todos) => {
this.todos = todos;
}
);
}
// ...
}
Again, we import the map
pipeable operator from rxjs/operators
and update .map(fn)
to .pipe(map(fn))
.
That’s it! The chained operators in our application have been replaced by pipeable operators, just as the Angular Update Guide instructed us to.
If we navigate our browser to http://localhost:4200
, we see that our application still works perfectly.
To verify that we are really running Angular 5, we can open up the element inspector:
Angular adds an ng-version
attribute to app-root
with a value of the version it’s running. We see ng-version="5.2.9"
, indicating that we’re running Angular 5.2.9.
Mission accomplished! Our application has been successfully upgraded to Angular 5.2.9.
We covered quite a lot, so let’s recap what we’ve learned.
In this chapter, we learned:
HttpModule
with HttpClientModule
ng-version
attribute lets us verify which version of Angular we’re running.In upcoming versions, Angular CLI will introduce the ng update
command to help update Angular applications. As soon as more details are available, we’ll provide you with a follow-up article on how this new command can make our lives even easier.
Until then, you can use this chpater as a guide on how to update Angular applications to the latest version.
All code from this article is available on GitHub.
Have a great one!