In this post from the Toptal Developer Blog, you’ll become more familiar with Angular 5 through building a simple notes application using the Angular CLI. For a richer content editing experience, you can check out our Angular rich text editor integration page to learn how you can easily configure TinyMCE into your app.
Angular is a new version of the AngularJS framework, developed by Google. It comes with a complete rewrite, and various improvements including optimized builds and faster compile times. In this Angular 5 tutorial, we are going to build a notes app from scratch. If you’ve been waiting to learn Angular 5, this tutorial is for you.
The final source code for the app can be found here.
There are two major versions of the framework: AngularJS (version 1) and Angular (version 2+). Since version 2, Angular is no longer a JavaScript framework, so there’s a huge difference between them, warranting a fundamental name change.
Should I use Angular?
It depends. Some developers will tell you that it’s better to use React and build your own components without much additional code. But that may be a problem, too. Angular is a fully integrated framework that allows you to start working on your project quickly without thinking about which libraries to select and how to deal with everyday problems. I think of Angular as being for the front-end, as RoR is for the back-end.
TypeScript
If you don’t know TypeScript, don’t be scared. Your JavaScript knowledge is enough to learn TypeScript quickly, and most modern editors are quite effective in helping with that. The most preferable options nowadays are VSCode and any of the JetBrains IntelliJ family (e.g., Webstorm or, in my case, RubyMine). For me, it’s preferable to use a smarter editor than vim
, as it will give you an extra heads-up on any mistakes in the code as TypeScript is strongly typed. Another thing to mention is that Angular CLI with its Webpack takes care of compiling TS to JS, so you shouldn’t let the IDE compile it for you.
Angular CLI
Angular now has its own CLI, or command line interface
, which will do most of the routine operations for you. To start using Angular, we have to install it. It requires Node 6.9.0 or higher as well as NPM 3 or higher. We are not going to cover their installation for your system, as it’s better to find up-to-date documentation for installation on your own. Once they are both installed, we are going to install Angular CLI by running the following:
npm install -g @angular/cli
After the installation is successful, we can generate a new project by running the ng new
command:
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.
After that’s done, we can ask our fresh application to start by running ng serve
out of its directory:
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.
If we navigate our browser to that link, it will be displayed as pictured here:
So, what is actually happening here? Angular CLI runs webpack dev server, which renders our app on the next free port (so that you can run multiple apps on the same machine), with live reload. It also watches for every change in the project source and recompiles all changes, after which it asks the browser to reload the open page. So by using Angular CLI, we are already working in a development environment without writing a line of configuration or actually doing anything. But we’re just getting started here…
Components
We have our empty app running. Let’s talk about app composition in Angular. If you have some background in AngularJS, you know there were controllers, directives, and components that were somehow like directives but simpler, to allow you to upgrade to Angular 2. For those who don’t have that wonderful experience of having to choose between them and figure out what goes where, don’t worry. It’s mostly just components nowadays. The component is the most basic building block in the Angular world. Let’s look at the code that was generated for us by Angular CLI.
First, here’s index.html
:
<!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>
It looks like the kind of markup you see every day. But there’s a special tag, app-root
. How does Angular make this work, and how can we know what’s happening inside it?
Let’s open the src/app
directory and see what’s there. You can look at the ng new
output form earlier here or open it in your chosen IDE. You will see that we have app.component.ts
there with the next bit (this may vary depending on how recent your version of Angular is):
import { Component } from "@angular/core";
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"],
})
export class AppComponent {
title = "app";
}
@Component(...)
here looks like a function call… What is it? This is TypeScript decorator, and we will talk about that a bit later. For now, let’s just try to understand what it’s doing, with passed parameters like selector
being used to generate our component declaration. It’s just going a lot of boilerplate work for us and giving back our component declaration in its working form. We don’t have to implement additional code to support any of the decorator’s params. It’s all handled by the decorator. So, generally, we call it factory methods.
We already saw app-root
in our index.html
. Here’s how Angular knows how to find the component corresponding to our tag. Obviously, templateUrl
and styleUrls
define where Angular should take our markup and CSS from. There are a lot more params for the component decorator, and we are going to use some of them in our new app, but if you want a full reference, you can always look here.
Let’s look into that component’s markup:
<!--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="<a href="https://angular.io/tutorial">https://angular.io/tutorial</a>">Tour of Heroes</a></h2>
</li>
<li>
<h2><a target="_blank" rel="noopener" href="<a href="https://github.com/angular/angular-cli/wiki">https://github.com/angular/angular-cli/wiki</a>">CLI Documentation</a></h2>
</li>
<li>
<h2><a target="_blank" rel="noopener" href="<a href="https://blog.angular.io/">https://blog.angular.io/</a>">Angular blog</a></h2>
</li>
</ul>
So, aside from embedding the Angular Logo as an SVG, which is pretty neat, this seems like typical everyday markup as well. Aside from one thing (Welcome to {{ title }}!
), if we look at our component code again, we will see title = 'app';
. So, if you already have some practice in template languages or have worked with AngularJS, it’s pretty obvious what’s happening here. If you don’t know, this is called Angular Interpolation, by which the expression inside the double curly braces is being pulled from our component (you can think of {{ title }}
as a simplified form of {{ this.title }}
) and displayed on our markup.
We’ve now seen all the parts of our auto-generated Angular app that actually take place in the page displayed in our browser. Let’s recap how it actually works: Angular CLI runs Webpack, which is compiling our Angular app into JavaScript bundles and injecting them into our index.html
. If we take a look at the actual code in our browser using the inspect feature, we see something like this:
Every time we change our code, Angular CLI will recompile, re-inject if needed, and ask our browser to reload the page if it’s open. Angular does it quite quickly, so in most cases, while you’re switching your windows from the IDE to the Browser, it’ll already be reloaded for you.
So, let’s start moving toward our goal and, for a start, let’s switch our project from CSS to Sass and open our .angular-cli.json
and edit styles
and styleExt
properties thusly:
"styles": [
"styles.scss"
],
[...]
"defaults": {
"styleExt": "scss",
"component": {}
}
We also need to add the Sass library to our project and rename styles.css
to styles.scss
. So to add Sass, I am using yarn
:
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.
I also want to use Twitter Bootstrap on our project, so I also run yarn add bootstrap@v4.0.0-beta.2
and edit our styles.scss
to include this:
/* You can add global styles to this file, and also import other style files */
@import "../node_modules/bootstrap/scss/bootstrap";
body {
padding-top: 5rem;
}
We need to edit index.html
to make our page responsive by changing the meta for our markup to this:
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
And now we can replace app.component.html
with this:
<!-- 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>
And now, if we open our browser, we see the following:
And that’s it for the boilerplate. Let’s move on to creating our own components.
Our first component
We are going to display notes as cards in our interface, so let’s start by generating our first component, representing the card itself. For that, let’s use Angular CLI by running the following command:
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)
If we look into src/app/card/card.component.ts
, we can see they are almost the same code, as we have in our AppComponent, with one small difference:
/* ... */
@Component({
selector: "app-card",
})
/* ... */
export class CardComponent implements OnInit {
constructor() {}
ngOnInit() {}
}
I like to mention, at this point, that it’s considered good practice to preface our component selectors with a common prefix, and by default, it’s app-
. You can change it to the prefix of your preference by editing the prefix
property in .angular-cli.json
, so it’s preferable to do so before using ng generate
for the first time.
So, we have a constructor for our component as well as an ngOnInit
function for it. If you’re curious why we did that, you can read about it in Angular’s documentation. But on a basic level, think about these methods like this: A constructor is being called right after the creation of the component, long before data to be passed to it is ready and populated, while ngOnInit
only runs after the first cycle of changes to the data, so you have access to your component inputs. We’ll talk about inputs and component communication pretty soon, but for now, let’s just remember that it is preferable to use the constructor for constants, like things that are actually being hard-coded into your component, and ngOnInit
for everything that depends on external data.
Let’s populate our CardComponent implementation. To start with, let’s just add some markup to it. Default contents for markup are something like this:
<p>
card works!
</p>
Let’s replace it with code so it will behave like a card:
<div class="card">
<div class="card-block">
<p class="card-text">Text</p>
</div>
</div>
Now is a good time to display the card component, but this raises additional questions: Who will be responsible for displaying the cards? AppComponent? But AppComponent will be loaded before anything else in the app, so we have to consider it to be tidy and small. We’d better create one more component to take care of storing a list of cards and displaying it on our page.
As we described our component’s responsibilities, it is clear that this is supposed to be a Card List Component. Let’s ask Angular CLI to generate it for us:
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 we start implementing it, let’s take a look at the thing that we ignored after generating our first component. Angular CLI tells us that it updated app.module.ts
for us. We’ve never looked into it, so let’s correct it:
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 {}
Obviously, BrowserModule
and NgModule
are internal Angular modules. We can read more about both of them in the documentation. AppComponent was here before we started to generate any code, so our new components actually populated the module in two places: First, they are imported from their definition files, and then, they are included in the declarations array of our NgModule decorator. If you are creating a new component from scratch and forget to add a new module to NgModule but try to add it to your markup, your app won’t work with the next error in the JS console:
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. ("
So if your app is not working, for no apparent reason, don’t forget to check your console.
Let’s populate our card list component markup (src/app/card-list/card-list.component.html
):
<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>
If we open it in our browser, we’ll see something like this:
Currently, we display our cards out of the hard-coded markup. Let’s bring our code one step closer to a real case scenario by moving the hard-coded array of cards into our application:
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" },
];
}
We have our initial list, but still, we need to pass it to the component and render it there. For that, we need to create our first input. Let’s add it to our CardList component:
import { Component, Input, OnInit } from "@angular/core";
/* ... */
export class CardListComponent implements OnInit {
@Input() cards: Array<any>;
/* ... */
}
We imported Input
from the Angular code and used it as a decorator for class-level variable cards with type Array of objects of any kind. Ideally, we shouldn’t use any
, but should use strict typing so that we can define something like an interface card, which will contain all the properties of our card, but we will get that working later—for now, we’re using any
just to get a fast and dirty implementation underway.
Now, we have our card array in our CardList. How can we display it instead of our current markup? Let’s take a look at the new code in our card list component:
<app-card class="col-4" *ngFor="let card of cards"></app-card>
This is something new for us, an attribute name that starts from an asterisk. What does it mean? It’s a default convention for naming Angular structural directives. Structural directives control the structure of our template. The asterisk here is actually “syntax sugar,” and you can read further to understand how it works. but for your current example, it’s enough to understand what will happen when we add it to our component. So ngFor
a repeater directive and it will repeat our app card for every element in the array of cards. If we look at the browser, we see this next:
Something isn’t right; we have our array of cards, but we are getting an empty page.
We defined our array of cards on the AppComponent level, but we haven’t passed it to CardList input. Let’s edit our AppComponent template to do that.
<app-card-list [cards]="cards"></app-card-list>
This syntax—the attribute in square brackets—tells Angular that we would like to one-way bind our component variable cards
to our Card List component [cards]
input. As soon as we do that, we get this:
Of course, we want to display the actual contents of our card array, and for that, we need to pass the card object to the card component as well. Let’s extend our Card List component:
<app-card class="col-4" *ngFor="let card of cards" [card]="card"></app-card>
And if we look in the browser right now, we’ll get the next error in the JS console: Can't bind to 'card' since it isn't a known property of 'app-card'.
. Angular is telling us that we still need to define our input in the Card component. So we can edit thusly:
import { Component, Input, OnInit } from "@angular/core";
/* ... */
export class CardComponent implements OnInit {
@Input() card: any;
/* ... */
}
And let’s add our card text property to the Card component template:
/* ... */
<p class="card-text">{{ card.text }}</p>
/* ... */
Let’s see how it works now:
Looks fine, but the styling is a little off. Let’s fix that by adding a new style to card.component.css
:
.card {
margin-top: 1.5rem;
}
And now it looks better:
Component communication
Let’s add a New Card Input component that will allow us to add notes:
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)
And add this next to its template:
<div class="card">
<div class="card-block">
<input placeholder="Take a note..." class="form-control">
</div>
</div>
Next, add this to the component decorator:
@Component({
selector: 'app-new-card-input',
host: {'class': 'col-4'},
/* ... */
})
/* ... */
And add our new component to the AppComponent template:
<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>
Let’s take a look at the browser.
The problem is that our new component isn’t doing anything. Let’s make it work—let’s start by adding a variable that will hold our new card:
/* ... */
export class NewCardInputComponent implements OnInit {
/* ... */
public newCard: any = {text: ''};
/* ... */
How do we populate it with our input? If you’ve worked with AngularJS before, you may know the concept of two-way data binding. Or, you might have seen it in all those fancy AngularJS demos, where you input value to input and it updates the page content for us.
Here’s an interesting tidbit: Two-way data binding is no longer with us in Angular. But that doesn’t mean we have lost access to the behavior. We already saw and used [value]="expression"
, which binds the expression to the input element’s value property. But we also have (input)="expression"
, a declarative way of binding an expression to the input element’s input event. Together, they can be used thusly:
<input [value]="newCard.text" (input)="newCard.text = $event.target.value">
So, every time our newCard.text
value changes, it’ll be passed to our component input. And every time the user inputs data into our input and the browser outputs input $event
, we assign our newCard.text
to the input value.
One more thing before we implement it: This input looks like a little much, doesn’t it? Actually, Angular gives us a little syntax sugar for it, which we can use here, so I started from a different angle to explain how this sugar works.
<input placeholder="Take a note..." class="form-control" [(ngModel)]="newCard.text">
This syntax, ([])
, called banana in a box or ngModel
, is the Angular directive that takes care of getting value out of events and all that. So we can just write simpler code that takes our value and binds it to both the value of the input and our variable in code.
Unfortunately, after we added ngModel
, we are getting the error, Can't bind to 'ngModel' since it isn't a known property of 'input'.
. We need to import ngModel
to our AppModule. But from where? If we check the documentation, we can see that it’s in the Angular Forms module. So we need to edit out AppModule thusly:
/* ... */
import {FormsModule} from "@angular/forms";
@NgModule({
/* ... */
imports: [
BrowserModule,
FormsModule
],
/* ... */
})
Working with native events
So we have our variable populated, but we still need to send that value to the card list in AppComponent. For communicating data to the component Angular, we must have input. It seems that to communicate data outside the component, we have output, and we use it in the same way we would use input—we import it from the Angular code and use a decorator to define it:
import { Component, EventEmitter, OnInit, Output } from "@angular/core";
/* ... */
export class NewCardInputComponent implements OnInit {
/* ... */
@Output() onCardAdd = new EventEmitter<string>();
/* ... */
}
But there is more than just output; we also define something called EventEmitter because the component output is supposed to be an event, but we shouldn’t think about it the same way we did those old JavaScript events. They aren’t bubbles. You don’t need to call preventDefault
in every event listener. To send data from the component, we should use its payload. So we need to subscribe to the events—how do we do that? Let’s change the AppComponent template:
<app-new-card-input (onCardAdd)="addCard($event)"></app-new-card-input>
We are also binding an expression to the event onCardAdd
, just as we mentioned in our NewCardInput
component. Now we need to implement the addCard
method on our AppComponent
.
export class AppComponent {
/* ... */
addCard(cardText: string) {
this.cards.push({ text: cardText });
}
}
But we’re still not outputting it from anywhere. Let’s try to make it happen when the user hits the enter
key. We need to listen for the DOM keypress event in our component and output the Angular event triggered by that. For listening to DOM events, Angular gives us the HostListener
decorator. It’s a function decorator that takes the name of a native event we want to listen for and the function Angular wants to call in response to it. Let’s implement it and discuss how it works:
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 = "";
}
}
So, if the document:keypress
event happens, we check that the key pressed was Enter and our newCard.text
has something in it. After that, we can call our addCard
method, in which we output Angular onCardAdd
with text from our card and reset the card text to an empty string so the user can continue to add new cards without editing the old card’s text.
Working with Forms
There are a couple of approaches to working with forms in Angular—one is template-driven and we are already using the most valuable part of it: ngModel
for two-way binding. But forms in Angular are not only about model values, but also about validity. Currently, we check for validity of NewCardInput in our HostListener function. Let’s move it to a more template-driven form. For that, we can change the template for our component:
<form novalidate #form="ngForm">
<input placeholder="Take a note..." class="form-control" name="text" [(ngModel)]="newCard.text" required>
</form>
Here’s another syntax sugar from Angular. The hash #form
is a template reference variable that we can use to access our form out of our code. Let’s use it to make sure we actually use the required attribute validation instead of the manual check on value length:
import {
Component,
EventEmitter,
OnInit,
Output,
HostListener,
ViewChild,
} from "@angular/core";
import { NgForm } from "@angular/forms";
export class NewCardInputComponent implements OnInit {
/* ... */
@HostListener("document:keypress", ["$event"])
handleKeyboardEvent(event: KeyboardEvent) {
if (event.code === "Enter" && this.form.valid) {
}
}
@ViewChild("form") public form: NgForm;
}
One more new decorator is here: ViewChild. Using that, we can access any element marked by template reference value—in this case, our form, and we actually declare it as our Component public variable form, so we can write this.form.valid
.
Working with template driven forms is absolutely the same as we did before with simple HTML forms. If we need something more complex, there is a different kind of form for that case in Angular: reactive. We’ll cover what they react on after converting our form. For that, let’s add a new import to our AppModule
:
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
/* ... */
imports: [ReactiveFormsModule]; /* ... */
Reactive forms are defined in code instead of template-driven forms, so let’s change the NewCardInput
component code:
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)]),
],
});
}
handler(event) {
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("");
}
}
In addition to importing new modules, some new things are happening here. First of all, we are using dependency injection for FormBuilder on our constructor and building our form with it. The text there is a name of our field, an empty string is the initial value, and Validators.compose
obviously allow us to combine multiple validators on a single field. We use .value
and .setValue('')
to access value for our field.
Let’s look at our markup for this new way of working with forms:
<form [formGroup]="newCardForm" novalidate>
<input placeholder="Take a note..." class="form-control" name="text" formControlName="text">
</form>
We are using FormGroupDirective to tell Angular what form group Angular needs to look in for its definition. By using formControlName, we are telling Angular what field in the reactive form we should use.
For now, the main difference between the previous approach with template-driven forms and the new approach with reactive forms is in more coding on the reactive side. Is it really worth it, if we don’t need to define the form dynamically?
It absolutely is. To understand how it may be helpful, let’s discuss why this approach is called “reactive” in the first place.
Let’s start by adding additional code to our New Card Input component constructor:
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 and developer tools console, and watch what will happen when we input new value into our input:
RxJS
So what’s actually happening here? We are seeing RxJS in action. Let’s discuss it. I guess you all know at least something about promises and building asynchronous code. Promise handling a single event. We ask the browser to make POST
, for example, and it returns us a promise. RxJS operates with Observables, which handle streams of events. Think about that like this: We have just implemented code that is called on every change of our form. If we process user changes with promises, only the first user change will be processed before we need to resubscribe. The Observable, at the same time, is able to process every event in a virtually endless stream of “promises.” We can break that by getting some error along the way or by unsubscribing from the Observable.
What is takeWhile
here? We are subscribing to our Observables in our components. They are used in different part of our app, so they may be destroyed along the way—for example, when we use components as pages in our routing (and we’ll talk about routing later in this guide). But while the promise in place of the Observable will run only a single time and will be disposed after that, the Observable is built to last as long as the stream is updating and we don’t unsubscribe. So our subscription needs to be unsubscribed (if we are not looking for memory leaks) like this:
const subscription = observable.subscribe((value) => console.log(value));
/* ... */
subscription.unsubscribe();
But in our app, we have a lot of different subscriptions. Do we need to do all of that boilerplate code? Actually, we can cheat and use the takeWhile operator. By using it, we make sure that our stream will stop emitting new values as soon as this.alive
becomes false and we just need to set that value in the onDestroy
function of our component.
Working with back-ends
Since we’re not building the server side here, we are going to use Firebase for our API. If you actually do have your own API back-end, let’s configure our back-end in development server. To do that, create proxy.conf.json
in the root of the project and add this content there:
{
"/api": {
"target": 'http://localhost:3000',
"secure": false
}
}
For every request from our app to its host (which, if you remember, is Webpack dev server), the /api
route server should proxy the request to http://localhost:3000/api.
For that to work, we need to add one more thing to our app configuration; in package.json
, we need to replace the start
command for our project:
"scripts": {
"start": "ng serve --proxy-config proxy.conf.json",
Now, we can run our project with yarn start
or npm start
and get proxy configuration in place. How can we work with the API from Angular? Angular gives us HttpClient. Let’s define our CardService for our current application:
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);
}
}
So what does Injectable
here mean? We already established that Dependency Injection helps us to inject our components with services we use. For getting access to our new service, we need to add it to the provider list in our AppModule
:
import { CardService } from './services/card.service';
@NgModule({
/* ... */
providers: [CardService],
Now we can inject it in our AppComponent, for example:
import { CardService } from './services/card.service';
constructor(private cardService: CardService) {
cardService.get().subscribe((cards: any) => this.cards = cards);
}
So let’s configure Firebase now, creating a demo project in Firebase and hitting the Add Firebase to your app
button. Then, we copy credentials that Firebase shows us into the Environment files of our app, here: src/environments/
export const environment = {
/* ... */
firebase: {
apiKey: "[...]",
authDomain: "[...]",
databaseURL: "[...]",
projectId: "[...]",
storageBucket: "[...]",
messagingSenderId: "[...]",
},
};
We need to add it to both environment.ts
and environment.prod.ts
. And just to give you some understanding of what Environment files are here, they are actually included in the project on compilation phase, and .prod.
the part being defined by the --environment
switch for ng serve
or ng build
. You can use values from that file in all parts of your project and include them from environment.ts
while Angular CLI takes care of providing content from the corresponding environment.your-environment.ts
.
Let’s add our Firebase support libraries:
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.
And now let’s change our CardService to support Firebase:
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);
}
}
We see something interesting here, on the first model for the card being imported. Let’s take a look at its composition:
export class Card {
$key: string;
text: string;
constructor(text: string) {
this.text = text;
}
}
So we are structuring our data with classes and, aside from our text, we add key$
from Firebase. Let’s change our AppComponent to work with that service:
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();
}
What is cards$
? We mark our observable variables by adding $
to them to make sure we treat them as we should. Let’s add our cards$
to the AppComponent template:
<app-card-list [cards]="cards$"></app-card-list>
In return, we get this error in the console:
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 so? We are getting observables from the Firebase. But our *ngFor
in the CardList component waits for the array of objects, not observable of such arrays. So we can subscribe to that observable and assign it to a static array of cards, but there is a better option:
<app-card-list [cards]="cards$ | async"></app-card-list>
The async pipe, which is practically another syntax sugar that Angular gives to us, does the same thing we discussed—subscribe to the Observable and return its current value as a result of evaluation of our expression.
Reactive Angular – Ngrx
Let’s talk about our application state, by which I mean all properties of our application that define its current behavior and state literally. State
is a single, immutable data structure—at least the way Ngrx implements it for us. And Ngrx is an “RxJS powered state management library for Angular applications, inspired by Redux.”
Ngrx is inspired by Redux. “Redux is a pattern for managing application state.” So it’s more like set of conventions (for those of you who ever heard of convention over configuration in Ruby on Rails, you will see some similarities a bit later) that allow us to answer the question of how our application should decide it needs to display some interface element (like a collapsible sidebar) or where it is supposed to store its session state after it receives it from the server.
Let’s see how this is achieved. We talked about State
and its immutability, which means we can’t change any of its properties after creating it. This makes it all but impossible to store our application state in our State
. But not completely—every single state is immutable, but the Store
, which is our way of accessing State
, is actually an Observable of the states. So State
is a single value in a stream of Store
values. In order to change the app’s state, we need to make some Action
s that will take our current State
and replace it with a new one. Both are immutable, but the second is based on the first, so instead of mutating values on our State
, we create a new State
object. For that, we use Reducers
as pure functions, meaning that for any given State
and Action
and its payload
reducer, it will return the same state as in any other call of that reducer function with same parameters.
Actions
consist of action type and optional payload:
export interface Action {
type: string;
payload?: any;
}
For our task, let’s view how the action for adding a new card could be:
store.dispatch({
type: "ADD",
payload: "Test Card",
});
Let’s see a reducer for that:
export const cardsReducer = (state = [], action) => {
switch (action.type) {
case "ADD":
return { ...state, cards: [...cards, new Card(action.payload)] };
default:
return state;
}
};
This function is being called for every new Action
event. We’ll cover Action
dispatching a bit later. For now, let’s say that if we dispatch our ADD_CARD
action, it’ll get into that case statement. What is happening there? We are returning our new State
based on our previous State
by using TypeScript spread syntax, so we don’t have to use something like Object.assign
in most cases. We never should change our state outside of those case statements, or it will make life miserable as we waste time searching for the reason why our code is behaving unpredictably.
Let’s add Ngrx to our application. For that, let’s run next in our console:
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.
Now, add our Action
definition (app/actions/cards.ts
):
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;
And our Reducer
definition (app/reducers/cards.ts
):
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;
}
}
Here we can see how you can use spreads and native TypeScript functions like map
to drop the element off our list.
Let’s go one step further and make sure that if our application state will contain more than one type of data, we are composing it from a separate isolated state for each kind. For that, let’s use module resolution using (app/reducers/index.ts
):
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);
We also include a logger for our Ngrx in the development environment and create a selector function for our card array. Let’s include it in our AppComponent
:
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);
}
}
Now, we see how we dispatch our actions using our store. But this code is still non-usable, as we don’t include our reducers (reducer and metaReducer) into our app. Let’s do it by changing our AppModule
:
import { StoreModule } from "@ngrx/store";
import { reducers, metaReducers } from "./reducers/index";
imports: [
/* ... */
StoreModule.forRoot(reducers, { metaReducers }),
/* ... */
];
And now it’s working. Kind of. Remember, we happen to have Firebase integrated into our App. Now it’s lost due to the highly maintainable Ngrx store. That is, it is stored nowhere. We can use something like ngrx-store-localstorage to store our data the browser’s localStore, but how about working with APIs? Maybe we can add our previous API integration into our Reducer? But we can’t, as our Reducer function is supposed to be a pure function. So, “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”… What can we do with that? The answer is actually right in that definition. Side-effects of Ngrx to the rescue.
Ngrx effects
So what is a side effect? Its piece of code that catches our Actions
more or less the same way as our reducers do, but instead of changing something in our state, they actually send API requests and, on the result, dispatch new Actions
. As always, it’s simpler to show you than to tell you. Let’s make our new configuration support Firebase. For that, let’s install the effects
module:
yarn add @ngrx/effects
[...]
success Saved 1 new dependency.
└─ @ngrx/effects@4.1.1
✨ Done in 11.28s.
Now we will add new actions to our Card Actions for loading support (src/app/actions/cards.ts
):
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;
So we have three new actions, one for loading the card list and two for dealing with successful and unsuccessful responses. Let’s implement our effects (src/app/effects/cards.ts
):
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) {}
}
So we have injectable CardsEffects, which use the @Effect
decorator for defining effects on top of our Actions
and filtering only necessary actions by using the ofType
operator. You may use ofType to create an effect that will be fired on multiple action types. But for now, we only need two out of our three actions. For the Load
action, we are transforming every action into a new observable on the result of our getCardList method call. In the case of success, the observable will be mapped to a new action LoadSuccess
with a payload of our request results, and in the case of error, we’ll return a single ServerFailure
action (mind the of
operator there—it converts a single value or array of values to the observable).
So our Effects dispatch new Actions after making something that depends on the external system (our Firebase, to be precise). But within the same code, we see another effect, which handles the ServerFailure
action using the decorator parameter dispatch: false
. What does this mean? As we can see from its implementation, it also maps our ServerFailure
action to its payload, and then displays this payload (our server error) to console.log
. Clearly, in that case, we should not change state contents, so we don’t have to dispatch anything. And that’s how we make it work without any need for empty actions.
So, now that we’ve covered two of our three actions, let’s move on to LoadSuccess
. From what we know so far, we are downloading a list of cards from the server and we need to merge them into our State
. So we need to add it to our reducer (src/app/reducers/cards.ts
):
switch (action.type) {
case cards.LOAD_SUCCESS:
return {
...state,
cards: [...state.cards, ...action.payload],
};
}
So same story as before, we open our object and card array in it by using the spread operator and join it with the spread payload (cards from the server, in our case). Let’s add our new Load action to our AppComponent:
/* ... */
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);
}
}
That should load our cards from Firebase. Let’s take a look at the browser:
Something is not working. We are clearly dispatching the Action, as can be seen from our logs, but no server request is here for us. What’s wrong? We forgot to load our Effects to our AppModule. Let’s do that:
import { EffectsModule } from "@ngrx/effects";
import { CardsEffects } from "./effects/cards.effects";
imports: [
/* ... */
EffectsModule.forRoot([CardsEffects]),
];
And now, back to the browser…
Now it’s working. So that’s how you integrate effects into loading data from the server. But we still need to send it back there on our card creation. Let’s make that work as well. For that, let’s change our CardService
createCard method:
createCard(card: Card): Card {
const result = this.cardsRef.push(card);
card.$key = result.key;
return card;
}
And add an effect for the Adding card:
@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]));
}
})
);
So, if the card is to be created, it’ll get $key
from Firebase and we’ll merge it into our card array. We also need to remove the case cards.ADD:
branch from our reducer. Let’s try it in action:
For some reason, we are getting duplicated data on the card add operation. Let’s try to figure out why. If we look closely at the console, we’ll see two LoadSuccess
actions first being dispatched with our new card as it is supposed to be, and the second one is being dispatched with both of our cards. If not in effects, where in our action is it being dispatched?
Our Load effect on cards has this code:
return this.cardService.getCardsList().pipe(
map(res => new Cards.LoadSuccess(res)),
And our getCardsList is observable. So when we add a new card to our card collection, it is output. So either we don’t need to add that card on our own, or we need use a take(1)
operator in that pipe. It’ll take a single value and unsubscribe. But having live subscription seems more reasonable (presumably, we will have more than one user in the system), so let’s change our code to deal with the subscription.
Let’s add a non-dispatching element to our effect:
@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);
})
);
Now we only need to change the reducer LoadSuccess
to replace cards, not combine them:
case cards.LOAD_SUCCESS:
return {
...state,
cards: action.payload
};
And now it’s working as it should:
You can implement remove action
the same way now. As we get data out of that subscription, you only have to implement the Remove
effect. But I will leave that to you.
Routing and modules
Let’s speak about our application composition. What if we need an About
page in our application? How can we add that to our current codebase? Obviously, the page is supposed to be a component (as anything else in Angular, mostly). Let’s generate that component.
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)
And add the next markup to it:
@Component({
selector: 'app-about',
template: `
<div class="jumbotron">
<h1 class="display-3">Cards App</h1>
</div>
`,
/* ... */
});
So now, we have our About page. How we can access it? Let’s add some more code to our AppModule
:
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})
]
})
What is MainComponent
here? For now, just generate it the same way we did with AboutComponent
and we’ll populate it later. As for route structure, it more or less speaks for itself. We define two routes: /cards
and /about
. And we make sure that empty path redirects for /cards
.
Now let’s move our cards handling code to MainComponent
:
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);
}
}
And let’s remove it from AppComponent
:
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() {}
}
And from markup as well:
<!-- 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>
As you can see, we added some more things. First of all, we added router directives for RouterLinkActive, which is setting a class when our route is active, and routerLink, which replaces href
for us. And here is routerOutlet, which tells Router
where to display its contents on a current page. So, combining those, we now have the menu on every page, along with two pages with different content:
For more details, please read the Router Guide.
As our application grows, we may start thinking of optimization. For example, what if we like to load the About component for default and only load additional components after the user implicitly asks for it by clicking on the Cards link. For that, we can use lazy loading of modules. Let’s start by generating CardsModule
:
ng g module cards --flat
create src/app/cards.module.ts (189 bytes)
By using the flat
flag, we are telling Angular to not create the separate directory for our module. Let’s transfer all card-related things into our new module:
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 {}
Previously, we saw a lot of forRoot
calls in our import, but here, we call for a lot of forFeature
or forChild
. That’s how we tell our components that we are extending our configuration, not creating it from scratch.
Let’s see what is still in our AppModule
:
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 {}
Here, we still define EffectsModule.forRoot
or it won’t work in our loaded module (as it will be nowhere to add on lazy load). We also see new syntax here for the router loadChildren
that tells our router to lazy load CardsModule
located in the ./cards.module
file when we ask for the cards
route. And we include meta reducers from the new ./reducers/root.ts
file—let’s take a look at it:
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]
: [];
On a root level, we currently don’t have any state, but we still need to define the empty state so we can extend it in the progress of lazy loading. That also means that our state of cards has to be defined somewhere else, and for this example, we define it in src/app/reducers/index.ts
:
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
);
So we extend our root state by cards key. And that gives us key nesting duplication at the end (as both a module and an array called cards
).
If we open our app now and look into the network tab of the developer console, we’ll see that cards.module.chunk.js
is being loaded only after we click on the /cards
link.
Preparing for production
So let’s build our app for production use. And for that, let’s run the build command:
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]
So what’s happening here? We are building our application to static assets that could be served from any web server (if you want to serve from subdirectory ng build
, have the option --base-href
). By using -prod
, we are telling AngularCLI that we need the production build. And --aot
is telling it that we like to have ahead-of-time compilation. In most cases, we prefer that, as it allows us to get the smaller bundle and faster code. Also, keep in mind that AoT is way too strict on your code quality, so it may produce errors that you haven’t seen before. Run the build earlier so it’s easier to fix.
I18n
Another reason to build our app is how Angular handles i18n or, speaking in plain terms, internationalization. Instead of dealing with it at runtime, Angular does it at compilation. Let’s configure it for our app. For that, let’s add the i18n attribute to our AboutComponent.
<div class="jumbotron">
<h1 class="display-3" i18n>Cards App</h1>
</div>
By using that, we are telling the Angular compiler that the tag’s contents need to be translated. It’s not the Angular directive, and it is removed by the compiler in the process of compilation and replaced by the translation for a given language. So we marked our first translated message, but what next? How can we actually translate that? For that, Angular offers us the ng xi18n
command:
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>
So we have a translation file mapping out our messages to their actual locations in the source code. Now, we can give the file to Phrase. Or, we can just add our translation manually. For that, let’s create a new file in src, messages.ru.xlf
:
<?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 now can serve our app—in Russian, for example—by running this command ng serve --aot --locale=ru --i18n-file=src/messages.ru.xlf
. Let’s see if it works:
Now, let’s automate our build script so we can make our app build in two languages on every production build and call its corresponding directories en or ru. For that let’s add the build-i18n command to the scripts
section of our package.json
:
"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
Now let’s package our app for production use, and use Docker for that. Let’s start with Dockerfile
:
#### STAGE 1: Build ###
## We label our stage as 'builder'
FROM <a href="node:8.6-alpine">node:8.6-alpine</a> 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 <a href="nginx:1.13.3-alpine">nginx:1.13.3-alpine</a>
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;"]
So we are using a multistage build for our app with a Node-based image, and then we build the server package with an Nginx-based image. We also use Gulp to compress our artifacts, as Angular CLI no longer does it for us. I find that strange, but okay, let’s add Gulp and compression scripts.
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.
Lets add gulpfile.js
in our app root:
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}/`));
}
});
Now we only need our Nginx config to build our container. Let’s add it to nginx/default.conf
:
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/;
}
}
So we are serving our build application from directories en
or ru
and by default, we are redirecting from root URL to /en/
.
Now we can build our app by using the docker build -t app .
command:
docker build -t app .
Sending build context to Docker daemon 347MB
Step 1/17 : FROM <a href="node:8.6-alpine">node:8.6-alpine</a> as builder
---> b7e15c83cdaf
Step 2/17 : ENV APP_PATH /app
[...]
Removing intermediate container 1ef1d5b8d86b
Successfully built db57c0948f1e
Successfully tagged <a href="app:latest">app:latest</a>
And then we can serve it using Docker from a local machine by running docker run -it -p 80:80 app
. And it’s working:
Mind the /en/
in URL.
Summary
Congratulations on completing this tutorial. You can now join the ranks of other Angular developers. You’ve just created your first Angular app, used Firebase as a backend and served it via Nginx in a Docker container.
As with any new framework, the only way to get good at it is to keep practicing. Hopefully you’ve come to understand just how powerful Angular is. When you’re ready to proceed, the Angular documentation is a wonderful resource and comes with a whole section on advanced techniques.
If you feel like taking on something more advanced, try Working with Angular 4 Forms: Nesting and Input Validation by fellow Toptaler Igor Geshoki.
The original post was published by Sergey Moiseev on the Toptal Engineering Blog.