Step by Step Guide to Creating Your First Angular 5 App: An Angular 5 Tutorial

Angular, a Google-developed framework, is a complete rewrite of AngularJS. This updated framework boasts various enhancements, including improved builds and faster compilation times. This Angular 5 tutorial guides you through constructing a note-taking application from scratch, making it ideal for those eager to learn Angular 5.

An Angular 5 Tutorial: Step by Step Guide to Your First Angular 5 App

The complete source code for the app is accessible here.

AngularJS (version 1) and Angular (version 2+) are the two primary versions of this framework. The significant differences between these versions, particularly Angular’s transition from a JavaScript framework, justify the name change.

Should I Use Angular?

The answer depends on your needs. Some developers advocate for using React and creating custom components with minimal extra code. However, this approach has its own challenges. Angular provides a fully integrated framework that streamlines project initiation. It eliminates concerns about library selection and simplifies common development hurdles. Angular can be considered the front-end counterpart to Ruby on Rails for back-end development.

TypeScript

Don’t be intimidated if you’re unfamiliar with TypeScript. Your JavaScript knowledge is sufficient for a quick transition to TypeScript, especially with assistance from modern editors. Currently, VSCode and JetBrains IntelliJ editors (like Webstorm or, in my preference, RubyMine) are favored choices. Opting for a more intelligent editor over vim proves beneficial by highlighting code errors as TypeScript is strongly typed. Notably, Angular CLI, powered by Webpack, handles the TypeScript to JavaScript compilation, so manual compilation within the IDE is unnecessary.

Angular CLI

Angular now includes a dedicated CLI, allowing it to manage most routine tasks automatically. Installing Angular, which necessitates Node 6.9.0 or later and NPM 3 or higher, is the first step. For the most current installation instructions for your system, consult the relevant documentation. Once these prerequisites are in place, execute the following command to install Angular CLI:

1
npm install -g @angular/cli

Upon successful installation, use the ng new command to generate a new project:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
ng new getting-started-ng5
  create getting-started-ng5/README.md (1033 bytes)
  create getting-started-ng5/.angular-cli.json (1254 bytes)
  create getting-started-ng5/.editorconfig (245 bytes)
  create getting-started-ng5/.gitignore (516 bytes)
  create getting-started-ng5/src/assets/.gitkeep (0 bytes)
  create getting-started-ng5/src/environments/environment.prod.ts (51 bytes)
  create getting-started-ng5/src/environments/environment.ts (387 bytes)
  create getting-started-ng5/src/favicon.ico (5430 bytes)
  create getting-started-ng5/src/index.html (304 bytes)
  create getting-started-ng5/src/main.ts (370 bytes)
  create getting-started-ng5/src/polyfills.ts (2405 bytes)
  create getting-started-ng5/src/styles.css (80 bytes)
  create getting-started-ng5/src/test.ts (1085 bytes)
  create getting-started-ng5/src/tsconfig.app.json (211 bytes)
  create getting-started-ng5/src/tsconfig.spec.json (304 bytes)
  create getting-started-ng5/src/typings.d.ts (104 bytes)
  create getting-started-ng5/e2e/app.e2e-spec.ts (301 bytes)
  create getting-started-ng5/e2e/app.po.ts (208 bytes)
  create getting-started-ng5/e2e/tsconfig.e2e.json (235 bytes)
  create getting-started-ng5/karma.conf.js (923 bytes)
  create getting-started-ng5/package.json (1324 bytes)
  create getting-started-ng5/protractor.conf.js (722 bytes)
  create getting-started-ng5/tsconfig.json (363 bytes)
  create getting-started-ng5/tslint.json (3040 bytes)
  create getting-started-ng5/src/app/app.module.ts (316 bytes)
  create getting-started-ng5/src/app/app.component.css (0 bytes)
  create getting-started-ng5/src/app/app.component.html (1141 bytes)
  create getting-started-ng5/src/app/app.component.spec.ts (986 bytes)
  create getting-started-ng5/src/app/app.component.ts (207 bytes)
Installing packages for tooling via yarn.
yarn install v1.3.2
info No lockfile found.
[1/4] πŸ”  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] πŸ”—  Linking dependencies...
warning "@angular/cli > @schematics/angular@0.1.10" has incorrect peer dependency "@angular-devkit/schematics@0.0.40".
warning "@angular/cli > @angular-devkit/schematics > @schematics/schematics@0.0.10" has incorrect peer dependency "@angular-devkit/schematics@0.0.40".
[4/4] πŸ“ƒ  Building fresh packages...
success Saved lockfile.
✨  Done in 44.12s.
Installed packages for tooling via yarn.
Successfully initialized git.
Project 'getting-started-ng5' successfully created.

Navigate to the newly created project directory and run ng serve to launch the application:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
ng serve
** NG Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **
Date: 2017-12-13T17:48:30.322Z
Hash: d147075480d038711dea
Time: 7425ms
chunk {inline} inline.bundle.js (inline) 5.79 kB [entry] [rendered]
chunk {main} main.bundle.js (main) 20.8 kB [initial] [rendered]
chunk {polyfills} polyfills.bundle.js (polyfills) 554 kB [initial] [rendered]
chunk {styles} styles.bundle.js (styles) 34.1 kB [initial] [rendered]
chunk {vendor} vendor.bundle.js (vendor) 7.14 MB [initial] [rendered]

webpack: Compiled successfully.

Accessing the provided link in your browser displays the following:

Angular App Welcome Page

What’s happening behind the scenes? Angular CLI utilizes webpack dev server to render the app on an available port (enabling multiple applications to run concurrently) with live reload functionality. It monitors project source files, recompiling changes and prompting browser refreshes. Angular CLI provides a development-ready environment without manual configuration. But this is merely the beginning.

Components

With a basic app up and running, let’s delve into Angular’s application structure. Those familiar with AngularJS development will recall controllers, directives, and simplified directive-like components intended to ease the transition to Angular 2. For those without this experience, rest assured – components are now the primary focus. They serve as the fundamental building blocks within Angular. Examining the code generated by Angular CLI provides insights.

Starting with index.html:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>GettingStartedNg5</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <app-root></app-root>
</body>
</html>

The markup appears familiar, except for the app-root tag. How does Angular interpret and utilize this tag?

Exploring the src/app directory reveals its contents. Refer to the previous ng new output or open the project in your IDE. Inside, you’ll find app.component.ts with the following (note that this may differ slightly depending on your Angular version):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'app';
} 

While @Component(...) resembles a function call, it’s actually a TypeScript decorator. More on that later. For now, focus on understanding its purpose. Parameters like selector contribute to generating component declarations. Essentially, it streamlines boilerplate code generation, providing a functional component declaration without requiring additional code for decorator parameter support. These are commonly referred to as factory methods.

Recall the app-root tag from index.html. The selector parameter is how Angular maps the tag to its corresponding component. Similarly, templateUrl and styleUrls specify the locations of the component’s markup and CSS, respectively. The component decorator offers numerous parameters, some of which we’ll utilize in our application. Refer to here for a comprehensive reference.

Examining the component’s markup:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<!--The content below is only a placeholder and can be replaced.-->
<div style="text-align:center">
  <h1>
    Welcome to {{ title }}!
  </h1>
  <img width="300" alt="Angular Logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg==">
</div>
<h2>Here are some links to help you start: </h2>
<ul>
  <li>
    <h2><a target="_blank" rel="noopener" href="https://angular.io/tutorial">Tour of Heroes</a></h2>
  </li>
  <li>
    <h2><a target="_blank" rel="noopener" href="https://github.com/angular/angular-cli/wiki">CLI Documentation</a></h2>
  </li>
  <li>
    <h2><a target="_blank" rel="noopener" href="https://blog.angular.io/">Angular blog</a></h2>
  </li>
</ul>

Apart from the embedded Angular logo as an SVG, the markup remains familiar, except for Welcome to {{ title }}!. Referring back to the component code reveals title = 'app';. This is Angular Interpolation, where expressions within double curly braces are dynamically populated from the component (consider {{ title }} as shorthand for {{ this.title }}).

We’ve now covered the auto-generated elements within our Angular app responsible for rendering the browser content. Let’s recap the process: Angular CLI employs JavaScript bundles through Webpack, compiling the Angular app and injecting it into index.html. Inspecting the browser’s source code reveals something similar to this:

Angular through a web inspector

Code modifications trigger recompilation by Angular CLI, which re-injects updated code and refreshes the browser. This efficient process ensures a smooth development experience. Let’s shift focus and transition our project from CSS to Sass. Open .angular-cli.json and modify the styles and styleExt properties as follows:

1
2
3
4
5
6
7
8
"styles": [
  "styles.scss"
],
[...]
"defaults": {
  "styleExt": "scss",
  "component": {}
}

Next, integrate the Sass library and rename styles.css to styles.scss. Use yarn to add Sass:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
yarn add sass 
yarn add v1.3.2
[1/4] πŸ”  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] πŸ”—  Linking dependencies...
[...]
[4/4] πŸ“ƒ  Building fresh packages...
success Saved lockfile.
success Saved 1 new dependency.
└─ sass@1.0.0-beta.4
✨  Done in 12.06s.
yarn add node-sass@4.7.2 --dev
✨  Done in 5.78s.

Incorporate Twitter Bootstrap by running yarn add bootstrap@v4.0.0-beta.2 and include the following in styles.scss:

1
2
3
4
5
6
/* You can add global styles to this file, and also import other style files */
@import "../node_modules/bootstrap/scss/bootstrap";
 
body {
  padding-top: 5rem;
}

Ensure page responsiveness by updating the viewport meta tag in index.html:

1
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">

Finally, replace the contents of app.component.html with:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<!-- Fixed navbar -->
<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
  <a class="navbar-brand" href="#">Angular Notes</a>
</nav>
<div class="container-fluid text-center pb-5">
  <div style="text-align:center">
    <h1>
      Welcome to {{title}}!
    </h1>
  </div>
</div>

The browser now displays the following:

Welcome to the app

With the boilerplate set up, let’s proceed to creating custom components.

Our first component

Since notes will be displayed as cards, begin by generating a component to represent a single card. Utilize Angular CLI by executing:

1
2
3
4
5
6
ng generate component Card
  create src/app/card/card.component.scss (0 bytes)
  create src/app/card/card.component.html (23 bytes)
  create src/app/card/card.component.spec.ts (614 bytes)
  create src/app/card/card.component.ts (262 bytes)
  update src/app/app.module.ts (390 bytes)

Inspecting src/app/card/card.component.ts reveals code similar to AppComponent with a minor difference:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[...]
@Component({
  selector: 'app-card',
[...]
export class CardComponent implements OnInit {

  constructor() { }

  ngOnInit() {
  }
}

It’s considered good practice to prefix component selectors for consistency, typically with app-. Customize this prefix in .angular-cli.json before using ng generate for the first time.

Observe the component’s constructor and the ngOnInit function. While a detailed explanation can be found in Angular’s documentation, understanding their basic roles is key. The constructor is invoked immediately upon component creation, potentially before data is ready. Conversely, ngOnInit executes after the initial data change cycle, providing access to component inputs. We’ll explore inputs and component communication shortly. For now, remember: use the constructor for constants (hard-coded values) and ngOnInit for data-dependent operations.

Populate the CardComponent implementation, starting with its markup. The default content is:

1
2
3
<p>
  card works!
</p>

Replace it with code resembling a card:

1
2
3
4
5
<div class="card">
  <div class="card-block">
    <p class="card-text">Text</p>
  </div>
</div>

While displaying the card component is tempting, it raises questions about responsibility. Should AppComponent handle card display? Since AppComponent loads first, maintaining its simplicity is crucial. Creating a separate component to manage and display a list of cards is a better approach.

This new component’s role clearly defines it as a CardListComponent. Use Angular CLI to generate it:

1
2
3
4
5
6
ng generate component CardList
  create src/app/card-list/card-list.component.scss (0 bytes)
  create src/app/card-list/card-list.component.html (28 bytes)
  create src/app/card-list/card-list.component.spec.ts (643 bytes)
  create src/app/card-list/card-list.component.ts (281 bytes)
  update src/app/app.module.ts (483 bytes)

Before proceeding, address the overlooked aspect after generating the first component. Angular CLI modified app.module.ts. Let’s examine its contents:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';


import { AppComponent } from './app.component';
import { CardComponent } from './card/card.component';
import { CardListComponent } from './card-list/card-list.component';


@NgModule({
  declarations: [
    AppComponent,
    CardComponent,
    CardListComponent,
  ],
  imports: [
    BrowserModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

BrowserModule and NgModule are internal Angular modules (refer to the documentation for details). While AppComponent predates our custom code, the newly generated components are integrated into the module in two ways. First, they are imported from their respective definition files. Second, they are included in the declarations array within the NgModule decorator. Forgetting to add a new component to the NgModule while attempting to use it in your markup will result in an error:

1
2
3
4
Uncaught Error: Template parse errors:
'app-card-list' is not a known element:
1. If 'app-card-list' is an Angular component, then verify that it is part of this module.
2. If 'app-card-list' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. ("

Always consult the console when troubleshooting unexpected application behavior.

Populate the card list component’s markup (src/app/card-list/card-list.component.html):

1
2
3
4
5
6
7
<div class="container-fluid text-center pb-5">
  <div class="row">
    <app-card class="col-4"></app-card>
    <app-card class="col-4"></app-card>
    <app-card class="col-4"></app-card>
  </div>
</div>

Opening the application in a browser will display:

Hard coded cards

Currently, cards are displayed from hard-coded markup. Let’s move this hard-coded card array into the application logic:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
export class AppComponent {
  public cards: Array<any> = [
    {text: 'Card 1'},
    {text: 'Card 2'},
    {text: 'Card 3'},
    {text: 'Card 4'},
    {text: 'Card 5'},
    {text: 'Card 6'},
    {text: 'Card 7'},
    {text: 'Card 8'},
    {text: 'Card 9'},
    {text: 'Card 10'},
  ];
}

With the initial list in place, pass it to the CardListComponent for rendering. This requires creating the first input. Add the following to the CardListComponent:

1
2
3
4
5
import {Component, Input, OnInit} from '@angular/core';
[...]
export class CardListComponent implements OnInit {
  @Input() cards: Array<any>;
[...]

The code imports Input from Angular and uses it as a decorator for the cards class-level variable, defined as an array of objects of any type. Ideally, strict typing with an interface like card is preferable. However, for this initial implementation, any suffices.

The card array is now available in CardListComponent. Modify the component’s template to utilize it:

1
<app-card class="col-4" *ngFor="let card of cards"></app-card>

The asterisk preceding the attribute name signifies an Angular structural directives. Structural directives govern the structure of the template. While the asterisk serves as “syntax sugar,” a deeper understanding of its mechanics can be gained by you can read further. For this example, understanding its effect is sufficient. ngFor, a repeater directive, will iterate over the cards array, rendering an app-card for each element. The browser now displays:

A blank page? What kind of Angular 5 tutorial is this?

Despite the card array, the page remains empty. The array was defined in AppComponent but hasn’t been passed to the CardListComponent’s input. Update the AppComponent template:

1
<app-card-list [cards]="cards"></app-card-list>

The square brackets around the attribute indicate one-way bind. This binds the cards variable from AppComponent to the [cards] input of the CardListComponent. The result is:

Cards on the page, but the text is all wrong

Displaying the actual card content necessitates passing the card object to the CardComponent. Extend the CardListComponent:

1
<app-card class="col-4" *ngFor="let card of cards" [card]="card"></app-card>

This change triggers a console error: Can't bind to 'card' since it isn't a known property of 'app-card'. Angular reminds us to define the input within the CardComponent. Make the following adjustments:

1
2
3
4
5
import {Component, Input, OnInit} from '@angular/core';
[...]
export class CardComponent implements OnInit {
  @Input() card:any;
[...]

Display the card text by updating the CardComponent template:

1
2
3
[...]
<p class="card-text">{{ card.text }}</p>
[...]

The result:

Cards with the right text

The styling needs improvement. Add a new style to card.component.css:

1
2
3
.card {
    margin-top: 1.5rem;
}

This enhancement results in:

Cards with a little better styling

Component communication

Introduce a NewCardInputComponent to handle note creation:

1
2
3
4
5
6
ng g component NewCardInput
  create src/app/new-card-input/new-card-input.component.scss (0 bytes)
  create src/app/new-card-input/new-card-input.component.html (33 bytes)
  create src/app/new-card-input/new-card-input.component.spec.ts (672 bytes)
  create src/app/new-card-input/new-card-input.component.ts (300 bytes)
  update src/app/app.module.ts (593 bytes)

Add the following to its template:

1
2
3
4
5
<div class="card">
  <div class="card-block">
    <input placeholder="Take a note..." class="form-control">
  </div>
</div>

Update the component decorator:

1
2
3
4
5
6
7
[...]
@Component({
  selector: 'app-new-card-input',
[...]
  host: {'class': 'col-4'}
})
[...]

Include the new component in the AppComponent template:

1
2
3
4
5
6
7
[...]
<div class="container-fluid text-center pb-5">
  <div class="row justify-content-end">
    <app-new-card-input></app-new-card-input>
  </div>
</div>
<app-card-list [cards]="cards"></app-card-list>

The browser now shows:

Adding an input field

The new component remains inactive. Let’s change that. Begin by introducing a variable to store the new card data:

1
2
3
4
5
[...]
export class NewCardInputComponent implements OnInit {
[...]
public newCard: any = {text: ''};
[...]

How do we populate this variable from user input? AngularJS developers might recall the concept of two-way data binding.

Interestingly, two-way data binding is absent in Angular. However, we haven’t lost access to its functionality. We’ve used [value]="expression" to bind expressions to an input’s value property. Similarly, (input)="expression" provides a declarative approach to binding expressions to input events. Combining them:

1
<input [value]="newCard.text" (input)="newCard.text = $event.target.value">

Now, newCard.text updates whenever its value changes, passing the updated value to the component input. Conversely, user input triggers the browser’s input event, assigning the input value to newCard.text.

Angular offers syntax sugar to simplify this:

1
<input placeholder="Take a note..." class="form-control" [(ngModel)]="newCard.text">

This ([]) syntax, known as banana in a box or ngModel, is an Angular directive that handles event value extraction and binding. This simplifies the code, binding the value to both the input element and the component variable.

However, using ngModel introduces an error: Can't bind to 'ngModel' since it isn't a known property of 'input'. We need to import ngModel into AppModule. Referencing the documentation reveals that it resides within the Angular Forms module. Update AppModule:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[...]
import {FormsModule} from "@angular/forms";

@NgModule({
[...]
  imports: [
    BrowserModule,
    FormsModule
  ],
[...]

Working with native events

With the variable populated, we need to send its value to the card list in AppComponent. For component communication in Angular, inputs are essential. To send data outside a component, we use outputs. Similar to inputs, outputs are imported from Angular and defined using decorators:

1
2
3
4
5
6
7
import {Component, EventEmitter, OnInit, Output} from '@angular/core';
[...]
export class NewCardInputComponent implements OnInit {
[...]
@Output() onCardAdd = new EventEmitter<string>();
[...]
}

Note the use of EventEmitter. Component outputs function as events, but not in the traditional JavaScript event sense. These events are bubbles and eliminate the need for constant preventDefault calls in event listeners. To send data from the component, utilize its payload. Subscribing to these events is the next step. Update the AppComponent template:

1
<app-new-card-input (onCardAdd)="addCard($event)"></app-new-card-input>

Similar to the NewCardInput component, an expression is bound to the onCardAdd event. Now, implement the addCard method in AppComponent:

1
2
3
4
5
6
[...]
export class AppComponent {
[...]
addCard(cardText: string) {
  this.cards.push({text: cardText});
}

The data still isn’t being outputted. Let’s trigger this when the user presses the enter key. Listen for the DOM keypress event in the component and output the corresponding Angular event. Angular provides the HostListener decorator for this purpose. This function decorator accepts the name of the native event and the function Angular should execute in response. Implement and analyze its behavior:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import {Component, EventEmitter, OnInit, Output, HostListener} from '@angular/core';
[...]
export class NewCardInputComponent implements OnInit {
[...]
@HostListener('document:keypress', ['$event'])
handleKeyboardEvent(event: KeyboardEvent) {
  if (event.code === "Enter" && this.newCard.text.length > 0) {
    this.addCard(this.newCard.text);
   }
}
[...]
addCard(text) {
  this.onCardAdd.emit(text);
  this.newCard.text = '';
}
[...]

Upon detecting the document:keypress event, the code verifies that the enter key was pressed and newCard.text contains data. If both conditions are met, the addCard method is called, emitting the onCardAdd Angular event with the card text and resetting the card text to an empty string. This enables continuous card creation without requiring manual text clearing.

Working with Forms

Angular offers two approaches to handling forms: template-driven and reactive. We’ve already utilized ngModel for two-way binding, a key aspect of template-driven forms. However, Angular forms encompass more than just model values; validity is crucial. Currently, we’re manually validating NewCardInput within the HostListener function. Let’s enhance this using a more structured template-driven approach. Update the component’s template:

1
2
3
<form novalidate #form="ngForm">
  <input placeholder="Take a note..." class="form-control" name="text" [(ngModel)]="newCard.text" required>
</form>

The hash symbol (#form) introduces a template reference variable, providing code access to the form. Utilize this to leverage the required attribute for validation instead of manual checks:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import {Component, EventEmitter, OnInit, Output, HostListener, ViewChild} from '@angular/core';
import {NgForm} from '@angular/forms';
[...]
export class NewCardInputComponent implements OnInit {
[...]
@ViewChild('form') public form: NgForm;
[...]
  @HostListener('document:keypress', ['$event'])
  handleKeyboardEvent(event: KeyboardEvent) {
    if (event.code === "Enter" && this.form.valid) {
[...]

Another new decorator comes into play: ViewChild. It grants access to elements marked with template reference variables. In this case, it’s our form, declared as a public component variable named form, enabling us to use this.form.valid.

Working with template-driven forms mirrors traditional HTML form handling. For more complex scenarios, Angular offers reactive forms. Let’s explore them after converting our existing form. First, import the necessary module into AppModule:

1
2
3
4
5
6
7
8
[...]
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
[...]
imports: [
[...]
ReactiveFormsModule,
]
[...]

Unlike template-driven forms, reactive forms are defined within the code. Modify the NewCardInput component:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
[...]
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
[...]
export class NewCardInputComponent implements OnInit {
[...]
newCardForm: FormGroup;

constructor(fb: FormBuilder) {
  this.newCardForm = fb.group({
    'text': ['', Validators.compose([Validators.required, Validators.minLength(2)])],
  });
}
[...]
if (event.code === "Enter" && this.form.valid) {
   this.addCard(this.newCardForm.controls['text'].value);
[...]
addCard(text) {
  this.onCardAdd.emit(text);
  this.newCardForm.controls['text'].setValue('');
}

Beyond importing new modules, there are a few key changes. The constructor utilizes dependency injection to inject FormBuilder. The form is then built using this injected service. The text argument represents the field name, an empty string sets its initial value, and Validators.compose enables combining multiple validators for a single field. Access the field’s value using .value and set it using .setValue('').

Now, let’s adjust the markup:

1
2
3
<form [formGroup]="newCardForm" novalidate>
  <input placeholder="Take a note..." class="form-control" name="text" formControlName="text">
</form>

FormGroupDirective informs Angular which form group to associate with the form. formControlName specifies the corresponding field from the reactive form.

Currently, the main difference between the template-driven and reactive approaches lies in the increased code required for the latter. This raises the question: is it worthwhile if dynamic form definition isn’t necessary?

The answer is a resounding yes. To understand why, let’s delve into the “reactive” nature of this approach.

Add the following code to the NewCardInput component constructor:

1
2
3
4
5
6
7
8
9
import { takeWhile, debounceTime, filter } from 'rxjs/operators';
[...]
this.newCardForm.valueChanges.pipe(
    filter((value) => this.newCardForm.valid),
  debounceTime(500),
  takeWhile(() => this.alive)
).subscribe(data => {
   console.log(data);
});

Open the browser’s developer tools console and observe the output as you input data into the form:

The console logs out the new value on each change

RxJS

This demonstrates RxJS in action. Let’s break it down. You’re likely familiar with promises and asynchronous code. Promises handle single events, like a POST request returning a promise upon completion. RxJS, however, works with Observables, which manage event streams. Consider this analogy: the implemented code executes whenever the form changes. Processing these changes with promises would only handle the first change before requiring re-subscription. Observables, on the other hand, handle each event in a continuous stream of “promises.” This stream can be interrupted by errors or explicit unsubscription.

What is the purpose of takeWhile? Observables are subscribed to within components and used throughout the application. Consequently, they might be destroyed during the application’s lifecycle, such as when components represent pages in a routing setup (more on routing later). Promises execute once and are disposed of. In contrast, Observables persist as long as the stream is active and unsubscribed. Therefore, to prevent memory leaks, subscriptions should be explicitly unsubscribed:

1
2
3
const subscription = observable.subscribe(value => console.log(value));
[...]
subscription.unsubscribe();

Managing numerous subscriptions with this boilerplate code can become cumbersome. Thankfully, the takeWhile operator offers a solution. It ensures that the stream halts emission when this.alive becomes false. Simply set this value within the component’s onDestroy function.

Working with back-ends

Since we’re not building the server-side here, we’ll use Firebase for our API. If you have your own API back-end, let’s configure our back-end in development server. Create proxy.conf.json in the project root and add:

1
2
3
4
5
6
{
  "/api": {
    "target": "http://localhost:3000",
    "secure": false
  }
}

This configuration instructs the development server to proxy any requests targeting the /api route on the application’s host (the Webpack dev server) to http://localhost:3000/api. To enable this, modify the start command in package.json:

1
2
3
4
[...]
"scripts": {
[...]
  "start": "ng serve --proxy-config proxy.conf.json",

Now, running the project with yarn start or npm start incorporates the proxy configuration. Let’s explore API interaction within Angular. Angular provides HttpClient. Define a CardService for our application:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { Injectable } from '@angular/core';
import {HttpClient} from '@angular/common/http';

@Injectable()
export class CardService {

  constructor(private http: HttpClient) { }
 
  get() {
    return this.http.get(`/api/v1/cards.json`);
  }

  add(payload) {
    return this.http.post(`/api/v1/cards.json`, {text: trim(payload)});
  }

  remove(payload) {
    return this.http.delete(`/api/v1/cards/${payload.id}.json`);
  }

  update(payload) {
    return this.http.patch(`/api/v1/cards/${payload.id}.json`, payload);
  }
}

The Injectable decorator signifies that this service can be injected into other components through Dependency Injection. To access this service, add it to the provider list in AppModule:

1
2
3
4
5
6
[...]
import { CardService } from './services/card.service';
[...]
@NgModule({
[...]
 providers: [CardService],

Now, inject it into AppComponent, for example:

1
2
3
4
5
import { CardService } from './services/card.service';
[...]
  constructor(private cardService: CardService) {
    cardService.get().subscribe((cards: any) => this.cards = cards);
  }
Firebase

Next, configure Firebase by creating a demo Firebase project and clicking “Add Firebase to your app.” Copy the provided credentials into the application’s environment files (src/environments/):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
export const environment = {
[...]
  firebase: {
    apiKey: "[...]",
    authDomain: "[...]",
    databaseURL: "[...]",
    projectId: "[...]",
    storageBucket: "[...]",
    messagingSenderId: "[...]"
  }
};

Update both environment.ts and environment.prod.ts. Environment files are integrated during compilation. The .prod. portion is determined by the --environment flag used with ng serve or ng build. Utilize values from these files throughout your project, referencing them from environment.ts. Angular CLI will automatically provide the correct content based on the environment.

Add the Firebase support libraries:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
yarn add firebase@4.8.0 angularfire2
yarn add v1.3.2
[1/4] πŸ”  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] πŸ”—  Linking dependencies...
[...]
success Saved lockfile.
success Saved 28 new dependencies.
[...]
✨  Done in 40.79s.

Modify the CardService to integrate with Firebase:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import { Injectable } from '@angular/core';
import { AngularFireDatabase, AngularFireList, AngularFireObject } from 'angularfire2/database';
import { Observable } from 'rxjs/Observable';

import { Card } from '../models/card';

@Injectable()
export class CardService {

  private basePath = '/items';

  cardsRef: AngularFireList<Card>;
  cardRef:  AngularFireObject<Card>;

  constructor(private db: AngularFireDatabase) {
    this.cardsRef = db.list('/cards');
  }

  getCardsList(): Observable<Card[]> {
    return this.cardsRef.snapshotChanges().map((arr) => {
      return arr.map((snap) => Object.assign(snap.payload.val(), { $key: snap.key }) );
    });
  }

  getCard(key: string): Observable<Card | null> {
    const cardPath = `${this.basePath}/${key}`;
    const card = this.db.object(cardPath).valueChanges() as Observable<Card | null>;
    return card;
  }

  createCard(card: Card): void {
    this.cardsRef.push(card);
  }

  updateCard(key: string, value: any): void {
    this.cardsRef.update(key, value);
  }

  deleteCard(key: string): void {
    this.cardsRef.remove(key);
  }

  deleteAll(): void {
    this.cardsRef.remove();
  }

  // Default error handling for all actions
  private handleError(error: Error) {
    console.error(error);
  }
}

The import for the card model introduces an interesting detail. Let’s examine its structure:

1
2
3
4
5
6
7
8
export class Card {
    $key: string;
    text: string;

    constructor(text: string) {
        this.text = text;
    }
}

Data is structured using classes. Alongside the text property, we include key$ from Firebase. Update AppComponent to utilize this service:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
[...]
import { AngularFireDatabase } from 'angularfire2/database';
import {Observable} from 'rxjs/Observable';
import { Card } from './models/card';
[...]
export class AppComponent {
public cards$: Observable<Card[]>;

addCard(cardText: string) {
  this.cardService.createCard(new Card(cardText));
}

constructor(private cardService: CardService) {
  this.cards$ = this.cardService.getCardsList();
}

The $ suffix in cards$ denotes an observable variable. Incorporate cards$ into the AppComponent template:

1
2
[...]
<app-card-list [cards]="cards$"></app-card-list>

This results in a console error:

1
CardListComponent.html:3 ERROR Error: Cannot find a differ supporting object '[object Object]' of type 'object'. NgFor only supports binding to Iterables such as Arrays.

Why does this occur? Firebase returns observables, but *ngFor in CardListComponent expects an array of objects. While subscribing to the observable and assigning its value to a static array is possible, a more elegant solution exists:

1
<app-card-list [cards]="cards$ | async"></app-card-list>

The async pipe, another example of Angular’s syntax sugar, simplifies the process. It subscribes to the observable and returns its current value, eliminating manual subscription management.

Reactive Angular – Ngrx

Let’s delve into application state, referring to the properties that define its current behavior and status. In Ngrx, State is a single, immutable data structure entity. Ngrx is an “RxJS powered state management library for Angular applications, inspired by Redux.”

Ngrx draws inspiration from Redux. “Redux is a pattern for managing application state.” It’s a set of conventions that guide how applications determine when to display UI elements (like a collapsible sidebar) and where to persist session state received from the server.

Let’s break down its implementation. State is immutable, meaning its properties cannot be modified after creation. This seemingly prevents storing application state directly within State. However, every state is immutable, but the Store, providing access to State, is an Observable of states. Therefore, State represents a single value within a stream of Store values. Modifying the application state requires Actions. These actions transform the current State into a new one. Both states remain immutable, but the new state is derived from the previous one. Instead of mutating State directly, a new State object is created. Reducers facilitate this as pure functions. This means that given the same State, Action, and payload, a reducer will consistently return the same new state.

Actions consist of an action type and optional payload:

1
2
3
4
export interface Action {
  type: string;
  payload?: any;
}

Consider an action for adding a new card:

1
2
3
4
store.dispatch({
  type: 'ADD',
  payload: 'Test Card'
});

A corresponding reducer might look like this:

1
2
3
4
5
6
7
8
export const cardsReducer = (state = [], action) => {
  switch(action.type) {
    case 'ADD':
      return {...state, cards: [...cards, new Card(action.payload)]};
    default:
      return state;
  }
}

This function is invoked whenever a new Action event occurs. We’ll cover action dispatching later. For now, assume that dispatching the ADD_CARD action triggers this case statement. Let’s analyze its behavior. A new State is returned based on the previous State using TypeScript spread syntax. This eliminates the need for constructs like Object.assign in most cases. Modifying state outside these case statements should be avoided to maintain predictability and simplify debugging.

Integrate Ngrx into the application using the following command:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
yarn add @ngrx/core @ngrx/store ngrx-store-logger
yarn add v1.3.2
[1/4] πŸ”  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] πŸ”—  Linking dependencies...
[...]
[4/4] πŸ“ƒ  Building fresh packages...
success Saved lockfile.
success Saved 2 new dependencies.
β”œβ”€ @ngrx/core@1.2.0
└─ @ngrx/store@4.1.1
└─ ngrx-store-logger@0.2.0
✨  Done in 25.47s.

Create the Action definition (app/actions/cards.ts):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import { Action } from '@ngrx/store';

export const ADD = '[Cards] Add';

export const REMOVE = '[Cards] Remove';

export class Add implements Action {
    readonly type = ADD;

    constructor(public payload: any) {}
}

export class Remove implements Action {
    readonly type = REMOVE;

    constructor(public payload: any) {}
}

export type Actions
  = Add
| Remove;

Define the Reducer (app/reducers/cards.ts):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import * as cards from '../actions/cards';
import { Card } from '../models/card';

export interface State {
    cards: Array<Card>;
}

const initialState: State = {
    cards: []
}

export function reducer(state = initialState, action: cards.Actions): State {
    switch (action.type) {
      case cards.ADD:
        return {
            ...state, 
            cards: [...state.cards, action.payload]
        };
      case cards.REMOVE:
        const index = state.cards.map((card) => card.$key).indexOf(action.payload);
        return {
            ...state, 
            cards: [...state.cards.slice(0, index), ...state.cards.slice(index+1)]
        };
      default:
        return state;
    }
}

This example demonstrates how spreads and native TypeScript functions like map can be used for array manipulation.

To handle scenarios where application state encompasses multiple data types, compose it from isolated states using module resolution (app/reducers/index.ts):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import * as fromCards from './cards';
import {ActionReducer, ActionReducerMap, createFeatureSelector, createSelector, MetaReducer} from '@ngrx/store';
import {storeLogger} from 'ngrx-store-logger';
import {environment} from '../../environments/environment';

export interface State {
    cards: fromCards.State;
}

export const reducers: ActionReducerMap<State> = {
    cards: fromCards.reducer
}

export function logger(reducer: ActionReducer<State>): any {
    // default, no options
    return storeLogger()(reducer);
}

export const metaReducers: MetaReducer<State>[] = !environment.production
  ? [logger]
  : [];

/**
 * Cards Reducers
 */
export const getCardsState = createFeatureSelector<fromCards.State>('cards');
export const getCards = createSelector(
    getCardsState,
    state => state.cards
);  

This code includes a logger for Ngrx in development and creates a selector function for the card array. Incorporate it into AppComponent:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { Component } from '@angular/core';
import { CardService } from './services/card.service';
import { Observable } from 'rxjs/Observable';
import { Card } from './models/card';
import * as fromRoot from './reducers';
import * as cards from './actions/cards';
import { Store } from '@ngrx/store';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  public cards$: Observable<Card[]>;

  addCard(card: Card) {
    this.store.dispatch(new cards.AddCard(card));
  }

  constructor(private store: Store<fromRoot.State>) {
    this.cards$ = this.store.select(fromRoot.getCards);
  }
}

This illustrates how actions are dispatched using the store. However, the code is still unusable because reducers haven’t been integrated into the application. Modify AppModule:

1
2
3
4
5
6
7
8
[...]
import { StoreModule } from '@ngrx/store';
import {reducers, metaReducers} from './reducers/index';
[...]
imports: [
[...]
  StoreModule.forRoot(reducers, { metaReducers }),
[...]

The application now functions, albeit with a caveat. The Firebase integration is no longer functional due to the introduction of the Ngrx store. Data persistence is absent. While solutions like ngrx-store-localstorage allow storing data in the browser’s localStorage, API interaction requires a different approach. Incorporating the previous API integration directly into the reducer is not possible due to its requirement as a pure function. The definition of a pure function provides a clue: β€œevaluation of the result does not cause any semantically observable side effect or output, such as mutation of mutable objects or output to I/O devices”… So, what’s the solution? Side-effects of Ngrx

Ngrx effects

Side effects are code segments that intercept Actions, similar to reducers. However, instead of modifying the state, they handle tasks like sending API requests and dispatching new Actions based on the results. Let’s implement Firebase support using effects. First, install the effects module:

1
2
3
4
5
yarn add @ngrx/effects
[...]
success Saved 1 new dependency.
└─ @ngrx/effects@4.1.1
✨  Done in 11.28s.

Add new actions to handle loading operations in Card Actions (src/app/actions/cards.ts):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[...]
export const LOAD = '[Cards] Load';

export const LOAD_SUCCESS = '[Cards] Load Success';

export const SERVER_FAILURE = '[Cards] Server failure';
[...]
export class Load implements Action {
    readonly type = LOAD;
}

export class LoadSuccess implements Action {
    readonly type = LOAD_SUCCESS;

    constructor(public payload: any) {}
}

export class ServerFailure implements Action {
    readonly type = SERVER_FAILURE;

    constructor(public payload: any) {}
}
[...]
export type Actions
[...]
    | Load
  | LoadSuccess
  | ServerFailure

Three new actions are introduced: one for initiating card list loading, and two for handling successful and unsuccessful responses. Implement the effects (src/app/effects/cards.ts):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import {Injectable} from '@angular/core';
import {Actions, Effect} from '@ngrx/effects';
import {CardService} from '../services/card.service';
import { of } from 'rxjs/observable/of';

import * as Cards from '../actions/cards';

import {exhaustMap, map, mergeMap, catchError} from 'rxjs/operators';

@Injectable()
export class CardsEffects {
    @Effect()
    loadCards$ = this.actions$
        .ofType(Cards.LOAD).pipe(
            mergeMap(action => {
                return this.cardService.getCardsList().pipe(
                map(res => new Cards.LoadSuccess(res)),
                catchError(error => of(new Cards.ServerFailure(error))))}
            )
        );

    @Effect({dispatch: false})
    serverFailure$ = this.actions$
        .ofType(Cards.SERVER_FAILURE).pipe(
        map((action: Cards.ServerFailure) => action.payload),
        exhaustMap(errors => {
            console.log('Server error happened:', errors);
            return of(null);
        }));        

    constructor(
        private actions$: Actions, 
        private cardService: CardService) {}
}

This code defines an injectable CardsEffects service. The @Effect decorator defines effects based on Actions. The ofType operator filters the relevant actions. While ofType can be used to trigger an effect based on multiple action types, only two are necessary for this example. For the Load action, each action is transformed into a new observable based on the result of the getCardList method call. Success maps the observable to a new LoadSuccess action carrying the request results as its payload. Errors result in a single ServerFailure action (note the use of the of operator, converting a value or array into an observable).

Effects dispatch new Actions after interacting with external systems (Firebase in this case). However, the code includes another effect handling the ServerFailure action with dispatch: false in its decorator. What does this signify? This effect maps the ServerFailure action to its payload and logs the server error to the console. Since this operation doesn’t modify state, dispatching actions is unnecessary. This demonstrates how to implement effects without requiring empty actions.

With two out of three actions covered, let’s address LoadSuccess. The application retrieves a list of cards from the server, which needs to be merged into the State. Update the reducer (src/app/reducers/cards.ts):

1
2
3
4
5
6
7
8
9
[...]
switch (action.type) {
[...]
case cards.LOAD_SUCCESS:
        return {
            ...state,
            cards: [...state.cards, ...action.payload]
        }  
[...]

This code utilizes the spread operator to merge the existing card array with the payload (cards from the server). Now, add the new Load action to AppComponent:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
[...]
export class AppComponent implements OnInit {
  public cards$: Observable<Card[]>;

  addCard(card: Card) {
    this.store.dispatch(new cards.AddCard(card));
  }

  constructor(private store: Store<fromRoot.State>) {
  }

  ngOnInit() {
    this.store.dispatch(new cards.Load());
    this.cards$ = this.store.select(fromRoot.getCards);
  }
}

This should load cards from Firebase. Let’s check the browser:

The cards don't load

Something’s amiss. The action is being dispatched, as evident from the logs, but there’s no server request. What’s wrong? We forgot to integrate the effects into AppModule. Make the following change:

1
2
3
4
5
6
7
[...]
import { EffectsModule } from '@ngrx/effects';
import { CardsEffects } from './effects/cards.effects';
[...]
imports: [
[...]
    EffectsModule.forRoot([CardsEffects]),

Back to the browser…

Ahh, the cards are now loading from firebase

It’s working! That’s how you integrate effects for loading data from the server. Next, implement card creation and data persistence. Modify the createCard method in CardService:

1
2
3
4
5
  createCard(card: Card): Card {
    const result = this.cardsRef.push(card);
    card.$key = result.key;
    return card;
  }

Add an effect for handling card additions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    @Effect()
    addCards$ = this.actions$
        .ofType(Cards.ADD).pipe(
            map((action: Cards.Add) => action.payload),
            exhaustMap(payload => {
              const card = this.cardService.createCard(payload);
                if (card.$key) {
                    return of(new Cards.LoadSuccess([card]));
                }
            })
        );

When creating a card, the code retrieves the $key from Firebase and merges it into the card array. The case cards.ADD: branch in the reducer should be removed. Let’s test its functionality:

Cards are created but the text doesn't change

Unexpectedly, duplicate data appears after adding a card. Let’s investigate. Examining the console reveals two LoadSuccess actions. The first dispatches the new card correctly, while the second includes both cards. If not the effects, where is this second action originating from?

The card loading effect contains the following code:

1
2
return this.cardService.getCardsList().pipe(
  map(res => new Cards.LoadSuccess(res)),

getCardsList returns an observable. Adding a new card to the collection triggers an emission. Therefore, either the manual card addition is unnecessary, or a take(1) operator should be used to limit the subscription to a single value. However, maintaining a live subscription is preferable (assuming multiple users), so let’s modify the code to handle subscriptions effectively.

Add a non-dispatching effect:

1
2
3
4
5
6
7
8
9
@Effect({dispatch: false})
addCards$ = this.actions$
  .ofType(Cards.ADD).pipe(
    map((action: Cards.Add) => action.payload),
    exhaustMap(payload => {
      this.cardService.createCard(payload);
      return of(null);
    })
  );

Finally, update the reducer’s LoadSuccess case to replace cards instead of combining them:

1
2
3
4
5
case cards.LOAD_SUCCESS:
  return {
    ...state,
    cards: action.payload
  };

The application now functions correctly:

Cards are added properly

Implementing a remove action follows a similar pattern. Since data is retrieved from the subscription, only the Remove effect needs to be implemented. Consider that a challenge for you to tackle.

Routing and modules

Let’s discuss application composition. How do you add an About page? In Angular, pages are typically components. Generate the component:

1
2
3
4
ng g component about --inline-template --inline-style
[...]
  create src/app/about/about.component.ts (266 bytes)
  update src/app/app.module.ts (1503 bytes)

Add the following markup:

1
2
3
4
5
6
7
8
9
[...]
@Component({
  selector: 'app-about',
  template: `
<div class="jumbotron">
  <h1 class="display-3">Cards App</h1>
</div>
  `,
[...]

With the About page component ready, how do we make it accessible? Modify AppModule:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
[...]
import { AboutComponent } from './about/about.component';
import { MainComponent } from './main/main.component';
import {Routes, RouterModule, Router} from "@angular/router";


const routes: Routes = [
  {path: '', redirectTo: 'cards', pathMatch: 'full'},
  {path: 'cards', component: MainComponent},
  {path: 'about', component: AboutComponent},
]

@NgModule({
  declarations: [
[...]
    AboutComponent,
    MainComponent,
  ],
  imports: [
[...]   
    RouterModule.forRoot(routes, {useHash: true})

For now, generate MainComponent using the same method as AboutComponent; we’ll populate it later. The route structure is self-explanatory. Two routes are defined: /cards and /about. Additionally, an empty path redirects to /cards.

Move the card handling code from AppComponent to MainComponent:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Card } from '../models/card';
import * as fromRoot from '../reducers';
import * as cards from '../actions/cards';
import { Store } from '@ngrx/store';


@Component({
  selector: 'app-main',
  template: `
<div class="container-fluid text-center pb-5">
  <div class="row justify-content-end">
    <app-new-card-input (onCardAdd)="addCard($event)"></app-new-card-input>
  </div>
</div>
<app-card-list [cards]="cards$ | async"></app-card-list>
  `,
  styles: []
})
export class MainComponent implements OnInit {
  public cards$: Observable<Card[]>;

  addCard(card: Card) {
    this.store.dispatch(new cards.Add(card));
  }

  constructor(private store: Store<fromRoot.State>) {
  }

  ngOnInit() {
    this.store.dispatch(new cards.Load());
    this.cards$ = this.store.select(fromRoot.getCards);
  }
}

Remove it from AppComponent:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  constructor() {
  }

  ngOnInit() {
  }
}

Also, remove it from the markup:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!-- Fixed navbar -->
<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
  <a class="navbar-brand" href="#">Angular Notes</a>
  <ul class="navbar-nav mr-auto">
    <li class="nav-item" [routerLinkActive]="['active']">
      <a class="nav-link" [routerLink]="['cards']">Cards</a>
    </li>
    <li class="nav-item" [routerLinkActive]="['active']">
      <a class="nav-link" [routerLink]="['about']">About</a>
    </li>
  </ul>
</nav>
<router-outlet></router-outlet>

Several changes are introduced. First, router directives are added: RouterLinkActive applies a class to active routes, and routerLink dynamically manages href attributes. routerOutlet instructs the Router where to display content on the current page. These directives collectively create a menu present on all pages, along with two distinct content pages:

Cards are removed on command

To delve deeper, refer to the Router Guide.

As our application expands, we might consider optimization. For instance, we could load the About component by default and only load other components when a user requests them by clicking the Cards link. Lazy loading of modules makes this possible. Let’s begin by creating CardsModule:

1
2
ng g module cards --flat
  create src/app/cards.module.ts (189 bytes)

The flat flag instructs Angular to avoid generating a separate directory for our module. Let’s move all card-related elements into this new module:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CardService } from './services/card.service';
import { CardComponent } from './card/card.component';
import { CardListComponent } from './card-list/card-list.component';
import { NewCardInputComponent } from './new-card-input/new-card-input.component';

import {FormsModule, ReactiveFormsModule} from "@angular/forms";

import { AngularFireModule } from 'angularfire2';
import { AngularFireDatabaseModule } from 'angularfire2/database';
import { AngularFireAuthModule } from 'angularfire2/auth';

import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { reducers } from './reducers';
import { CardsEffects } from './effects/cards.effects';

import { environment } from './../environments/environment';
import { MainComponent } from './main/main.component';

import {Routes, RouterModule, Router} from "@angular/router";

const routes: Routes = [
  {path: '', redirectTo: 'cards', pathMatch: 'full'},
  {path: 'cards', component: MainComponent},
]

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    ReactiveFormsModule,
    StoreModule.forFeature('cards', reducers),
    EffectsModule.forFeature([CardsEffects]),
    RouterModule.forChild(routes),
    AngularFireModule.initializeApp(environment.firebase),
    AngularFireDatabaseModule,
    AngularFireAuthModule,
  ],
  providers: [CardService],
  declarations: [
    CardComponent,
    CardListComponent,
    NewCardInputComponent,
    MainComponent
  ]
})
export class CardsModule { }

While we previously used forRoot calls in our imports, we now use forFeature or forChild. This signifies that we’re extending our configuration rather than starting from scratch.

Let’s examine what remains in our AppModule:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[...]
import { reducers, metaReducers } from './reducers/root';

const routes: Routes = [
  {path: '', redirectTo: 'about', pathMatch: 'full'},
  {path: 'about', component: AboutComponent},
  { path: 'cards', loadChildren: './cards.module#CardsModule'}
]

@NgModule({
  declarations: [
    AppComponent,
    AboutComponent,
  ],
  imports: [
    BrowserModule,
    RouterModule.forRoot(routes, {useHash: true}),
    StoreModule.forRoot(reducers, { metaReducers }),
    EffectsModule.forRoot([]),
  ],
  
  bootstrap: [AppComponent]
})
export class AppModule { }

We still define EffectsModule.forRoot here because its absence would prevent it from functioning in our lazy-loaded module (due to the lack of a target for lazy loading). Additionally, we encounter new syntax for the router’s loadChildren. This tells our router to lazy load CardsModule, situated in the ./cards.module file, when the cards route is requested. Furthermore, we include meta reducers from the updated ./reducers/root.ts file. Let’s take a closer look:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import {ActionReducer, ActionReducerMap, createFeatureSelector, createSelector, MetaReducer} from '@ngrx/store';
import {storeLogger} from 'ngrx-store-logger';
import {environment} from '../../environments/environment';

export interface State {
}

export const reducers: ActionReducerMap<State> = {
}

export function logger(reducer: ActionReducer<State>): any {
    // default, no options
    return storeLogger()(reducer);
}

export const metaReducers: MetaReducer<State>[] = !environment.production
  ? [logger]
  : [];

Presently, we have no state at the root level. However, defining an empty state is necessary for future extension during lazy loading. Consequently, our cards state needs definition elsewhere. In this instance, it resides in src/app/reducers/index.ts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import * as fromCards from './cards';
import {ActionReducer, ActionReducerMap, createFeatureSelector, createSelector, MetaReducer} from '@ngrx/store';
import {storeLogger} from 'ngrx-store-logger';
import {environment} from '../../environments/environment';
import * as fromRoot from './root';

export interface CardsState {
    cards: fromCards.State;
}

export interface State extends fromRoot.State {
  cards: CardsState;
}

export const reducers = {
    cards: fromCards.reducer
}

/**
 * Cards Reducers
 */
export const getCardsState = createFeatureSelector<CardsState>('cards');
export const getCards = createSelector(
    getCardsState,
    state => state.cards.cards
);  

Here, we extend our root state with a cards key. This results in key nesting duplication, as both a module and an array are named cards.

If we launch our app now and inspect the developer console’s network tab, we’ll observe that cards.module.chunk.js only loads after clicking the /cards link.

Production Preparation

Let’s prepare our app for production by building it. We can achieve this by executing the build command:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
ng build --aot -prod
 65% building modules 465/466 modules 1 active ...g/getting-started-ng5/src/styles.scssNode#moveTo was deprecated. Use Container#append.
Date: 2018-01-09T22:14:59.803Z
Hash: d11fb9d870229fa05b2d
Time: 43464ms
chunk {0} 0.657b0d0ea895bd46a047.chunk.js () 427 kB  [rendered]
chunk {1} polyfills.fca27ddf9647d9c26040.bundle.js (polyfills) 60.9 kB [initial] [rendered]
chunk {2} main.5e577f3b7b05660215d6.bundle.js (main) 279 kB [initial] [rendered]
chunk {3} styles.e5d5ef7041b9b072ef05.bundle.css (styles) 136 kB [initial] [rendered]
chunk {4} inline.1d85c373f8734db7f8d6.bundle.js (inline) 1.47 kB [entry] [rendered]

What’s happening here? We’re transforming our application into static assets suitable for deployment on any web server (the --base-href option allows deployment in the ng build subdirectory). The -prod flag signals a production build to AngularCLI. Meanwhile, --aot requests ahead-of-time compilation, which is generally preferable for its smaller bundle size and faster code execution. However, AoT’s strictness on code quality might expose previously unnoticed errors, making early builds crucial for easier debugging.

I18n

Another reason for building our app lies in how Angular handles i18n, or internationalization. Angular addresses this during compilation instead of runtime. Let’s configure it for our app by adding the i18n attribute to our AboutComponent.

1
2
3
<div class="jumbotron">
  <h1 class="display-3" i18n>Cards App</h1>
</div>

This attribute instructs the Angular compiler to translate the tag’s contents. As a compiler directive, it’s removed during compilation and replaced with the appropriate translation for the target language. Now that we’ve marked our first translated message, let’s proceed with the actual translation process. Angular provides the ng xi18n command for this purpose:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
ng xi18n
cat src/messages.xlf
<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
  <file source-language="en" datatype="plaintext" original="ng2.template">
    <body>
      <trans-unit id="80dcbb43f590ee82c132b8c725df2b7b433dc10e" datatype="html">
        <source>Cards App</source>
        <context-group purpose="location">
          <context context-type="sourcefile">app/about/about.component.ts</context>
          <context context-type="linenumber">3</context>
        </context-group>
      </trans-unit>
    </body>
  </file>
</xliff>

This generates a translation file mapping our messages to their source code locations. We can then provide this file to a Phrase or manually add our translations. Let’s create a new file named messages.ru.xlf within the src directory:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<?xml version="1.0" encoding="UTF-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
  <file original="ng2.template" datatype="plaintext" source-language="en" target-language="ru">
    <body>
      <trans-unit id="80dcbb43f590ee82c132b8c725df2b7b433dc10e">
        <source xml:lang="en">Cards App</source>
        <target xml:lang="ru">ΠšΠ°Ρ€Ρ‚ΠΎΡ‚Π΅ΠΊΠ°</target>
      </trans-unit>
  </body>
  </file>
</xliff>

We can now serve our app in Russian, for example, using the command ng serve --aot --locale=ru --i18n-file=src/messages.ru.xlf. Let’s test its functionality:

Serving in the app in Russian

Next, let’s automate our build script to produce builds in two languages (English and Russian) during each production build, placing them in their respective en and ru directories. We can add the build-i18n command to the scripts section of our package.json file:

1
 "build-i18n": "for lang in en ru; do yarn run ng build --output-path=dist/$lang --aot -prod --bh /$lang/ --i18n-file=src/messages.$lang.xlf --i18n-format=xlf --locale=$lang --missing-translation=warning; done"

Docker

Let’s package our app for production using Docker. We’ll start with a Dockerfile:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#### STAGE 1: Build ###
## We label our stage as 'builder'
FROM node:8.6-alpine as builder

ENV APP_PATH /app
MAINTAINER Sergey Moiseev <sergey.moiseev@toptal.com>

COPY package.json .
COPY yarn.lock .

### Storing node modules on a separate layer will prevent unnecessary npm installs at each build
RUN yarn install --production && yarn global add gulp && mkdir $APP_PATH && cp -R ./node_modules .$APP_PATH

WORKDIR $APP_PATH

COPY . .

### Build the angular app in production mode and store the artifacts in dist folder
RUN yarn remove node-sass && yarn add node-sass && yarn run build-i18n && yarn run gulp compress

#### STAGE 2: Setup ###
FROM nginx:1.13.3-alpine

ENV APP_PATH /app
MAINTAINER Sergey Moiseev <sergey.moiseev@toptal.com>

### Copy our default nginx config
RUN rm -rf /etc/nginx/conf.d/*
COPY nginx/default.conf /etc/nginx/conf.d/

### Remove default nginx website
RUN rm -rf /usr/share/nginx/html/*

EXPOSE 80

### From 'builder' stage copy over the artifacts in dist folder to default nginx public folder
COPY --from=builder $APP_PATH/dist/ /usr/share/nginx/html/

CMD ["nginx", "-g", "daemon off;"]

Here, we utilize a multistage build with a Node-based image to build the server package, followed by an Nginx-based image. Since Angular CLI no longer does it for us, we employ Gulp to compress our artifacts. While peculiar, let’s add Gulp and the necessary compression scripts.

1
2
3
4
5
6
yarn add gulp@3.9.1 gulp-zip@4.1.0 --dev
[...]
success Saved 2 new dependencies.
β”œβ”€ gulp-zip@4.1.0
└─ gulp@3.9.1
✨  Done in 10.48s.

Now, let’s create a gulpfile.js in our app’s root directory:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const gulp = require('gulp');
const zip = require('gulp-gzip');

gulp.task('compress', function() {
    for (var lang in ['en', 'ru']) {
        gulp.src([`./dist/${lang}/*.js`, `./dist/${lang}/*.css`])
        .pipe(zip())
        .pipe(gulp.dest(`./dist/${lang}/`));
    }
});

Finally, we need an Nginx configuration for our container. Let’s add it to nginx/default.conf:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
server {
  listen 80;

  sendfile on;

  default_type application/octet-stream;

  client_max_body_size  16m;

  gzip on;
  gzip_disable "msie6";

  gzip_vary on;
  gzip_proxied any;
  gzip_comp_level 6;
  gzip_buffers 16 8k;
  gzip_http_version 1.0; # This allow us to gzip on nginx2nginx upstream.
  gzip_min_length 256;
  gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml image/x-icon;

  root /usr/share/nginx/html;

  location ~* \.(js|css)$ {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
  }

  location ~ ^/(en|ru)/ {
    try_files $uri $uri/ /index.html =404;
  }

  location = / {
     return 301 /en/;
   }
}

This configuration serves our built application from either the en or ru directory, redirecting from the root URL to /en/ by default.

We can build our app using the command docker build -t app .:

1
2
3
4
5
6
7
8
9
docker build -t app .
Sending build context to Docker daemon    347MB
Step 1/17 : FROM node:8.6-alpine as builder
 ---> b7e15c83cdaf
Step 2/17 : ENV APP_PATH /app
[...]
Removing intermediate container 1ef1d5b8d86b
Successfully built db57c0948f1e
Successfully tagged app:latest

Once built, we can serve it locally using Docker with the command docker run -it -p 80:80 app. Upon successful execution, we should see:

It works! This Angular 5 Tutorial is now complete

Note the /en/ in the URL.

Summary

Congratulations on finishing this tutorial! You’ve joined the ranks of Angular developers by creating your first Angular app, incorporating Firebase as a backend, and deploying it via Nginx within a Docker container.

Mastery of any framework requires consistent practice. Hopefully, this tutorial has illuminated the power of Angular. The Angular documentation offers a wealth of information, including a section on advanced techniques, to further your learning.

If you’re feeling adventurous, explore more challenging topics like Working with Angular 4 Forms: Nesting and Input Validation by fellow Toptaler Igor Geshoki.

Licensed under CC BY-NC-SA 4.0