38 min read

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

View all articles

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.

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

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/[email protected]" has incorrect peer dependency "@angular-devkit/[email protected]".
warning "@angular/cli > @angular-devkit/schematics > @schematics/[email protected]" has incorrect peer dependency "@angular-devkit/[email protected]".
[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:

Angular App Welcome Page

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="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>

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:

Angular through a web inspector

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.
└─ [email protected]
✨  Done in 12.06s.
yarn add [email protected] --dev
✨  Done in 5.78s.

I also want to use Twitter Bootstrap on our project, so I also run yarn add [email protected] 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:

Welcome to the app

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:

Hard coded cards

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:

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

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:

Cards on the page, but the text is all wrong

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:

Cards with the right text

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:

Cards with a little better styling

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.

Adding an input field

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 {
[...]
@ViewChild('form') public form: NgForm;
[...]
  @HostListener('document:keypress', ['$event'])
  handleKeyboardEvent(event: KeyboardEvent) {
    if (event.code === "Enter" && this.form.valid) {
[...]

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)])],
  });
}
[...]
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:

The console logs out the new value on each change

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);
  }

Firebase

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 [email protected] 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 Actions 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/[email protected]
└─ @ngrx/[email protected]
└─ [email protected]
✨  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/[email protected]
✨  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:

The cards don't load

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…

Ahh, the cards are now loading from firebase

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:

Cards are created but the text doesn't change

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:

Cards are added properly

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:

Cards are removed on command

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 PhraseApp. 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:

Serving in the app in Russian

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 node:8.6-alpine as builder

ENV APP_PATH /app
MAINTAINER Sergey Moiseev <[email protected]>

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 <[email protected]>

### 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 [email protected] [email protected] --dev
[...]
success Saved 2 new dependencies.
├─ [email protected]
└─ [email protected]
✨  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 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

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:

It works! This Angular 5 Tutorial is now complete

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.

Understanding the Basics

Why do we use Angular?

We use it to develop rich interface client-side applications like single-page applications and mobile applications. The main advantage of Angular is getting a fully integrated web framework that offers its own in-box solutions for building components, routing, and working with remote APIs.

About the author

Sergey Moiseev, Estonia
member since March 23, 2014
Sergey is a full-stack software engineer with experience developing complex web projects. He has extensive skills as an analyst and with gathering requirements for project development. He boasts practical experience in all levels of participation from full-stack developer to team lead/CTO. He is a top 5% answerer for AngularJS on StackOverflow, and top 10% on Ruby on Rails. [click to continue...]
Hiring? Meet the Top 10 Freelance Angular Developers for Hire in December 2018

Comments

Roeefl
Great piece. thank you
papoola
Thanks for great article!
skloke
Fantastic. Thank you. I wanted to mention that you use: ng new getting-started-ng5 but then later state: "it’s considered good practice to preface our component selectors with a common prefix...." It would seem that by doing things this way, the initial component created would have one prefix and others created later would have a different prefix. I see in the Angular documentation: https://github.com/angular/angular-cli/wiki/new There is a flag we can use with the 'new' for solving this problem! --prefix (alias: -p) default value: app I'm going to try it.
skloke
Does it matter what directory you run the yarn command: yarn add sass Should this be done within the getting-started-ng5/ directory?
SergeyMoiseev
It should be done in project directory as this command is actually modifies package.json contents.
SergeyMoiseev
Oh, app here is default prefix (app). I never override it. But you can. Both with switch for ng new or by changing prefix config value here https://github.com/bopm/getting-started-ng5/blob/master/.angular-cli.json#L20 later.
d4rkm3z
After create component card-list you was not add it to app.component.html
Martín Fox
Thanks for this article! I got confuse in one part after "Let’s populate our card list component markup (src/app/card-list/card-list.component.html):" It shows the browser as if the first page is CardList but in the steps it still being AppComponent. I tried set CardListComponent as bootstrap but it crashed the project. Thanks!
SergeyMoiseev
Search for `We defined our array of cards on the AppComponent level, but we haven’t passed it to CardList input.`. Changes are more or less as I was working on code. So I break things from time to time in my work, and then I fix them.
SergeyMoiseev
Look for 'Something isn’t right; we have our array of cards, but we are getting an empty page.' explains why code not working till some additional changes. Also look for answer to d4rkm3z.
Hemaes19
Great job, but I think you forget to show how to display `CardComponent` in the home page of the app, there is missing part
SergeyMoiseev
I never intended to display stand-alone card component on our home page. We building card listing app, so right after we create card component we also create card list component and display card there. Refer to source code, it intend to be stable on every commit.
Martín Fox
In this part <form [formGroup]="newCardForm" novalidate> <input placeholder="Take a note..." class="form-control" name="text" formControlName="text"> </form> It is missing the attribute in form #form="ngForm" Otherwise is giving error when try to do 'this.form.valid'
Martín Fox
I fixed adding <app-card-list></app-card-list> in the app.component.html
SergeyMoiseev
Of course, it's here in source code: https://github.com/bopm/getting-started-ng5/commit/04bae11790e9a7f2f6c15be307542b88509a640d#diff-73cca2b01266d7d49c0d71dfae4fd30eR19
Сергей Гладышев
Hi I find typo in your article, in fragment text: "Looks fine, but the styling is a little off. Let’s fix that by adding a new style to card.component.css:" May be not a *.css but a *.scss ?
IPHS TECHNOLOGIES
it's really awesome blog for me there is lot of information thanks. <a href="https://iphtechnologies.com/mobile-game-development/">Top class Mobile Game Development Company India</a>
akoper
Broke for me at "So to add Sass, I am using yarn:" I didn't have yarn on my computer. I always use npm to add packages and things. Why make tutorial more complicated? Why not use npm? I used npm to install yarn to install sass. Compile always failed form after this. Couldn't do rest of tutorial.
akoper
I had to add ".scss" to this line for it to work "@import "../node_modules/bootstrap/scss/bootstrap.scss";
SergeyMoiseev
Because some months ago npm has some issues with speed, and that make some of us to switch for yarn. Actually, it's not so much different to be concerned.
SergeyMoiseev
Yep, you right: https://github.com/bopm/getting-started-ng5/blob/master/src/app/card/card.component.scss
Walter Torres
I got to this point... <form [formGroup]="newCardForm" novalidate> <input placeholder="Take a note..." class="form-control" name="text" formControlName="text"> </form> I get this error on the console... AppComponent.html:9 ERROR Error: formGroup expects a FormGroup instance. Please pass one in. line 9: <form [formGroup]="newCardForm" novalidate> Looking at the github code doe not help because it has the finished product, and this block is changed later in the tutorial.
Walter Torres
I wiped and re-did this tutorial process 3 times, catching phrases and assumptions, "component" when "template" was referenced. "markup file" vs "template". For a newbie some of the language was a bit confusing. Sometimes a direct file is referenced by name... "You will see that we have app.component.ts" other times its inferred... "Let’s look into that component’s markup:" ,__ again markup vs template This one threw me for a time... "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'}, ... " Yes, it's the app.component.ts file, but to a newbie I didn't catch the class name for a long time, and the github coed doesn't help because it's removed from the finished product. This is a nice tutorial overall, but the author makes many assumptions about the level of experience of the reader, especially for a tutorial targeting "First Angular 5 App"
Kanaka
I would agree with this except for the fact that after you code the CardComponent and CardList you state that it should look different with the card/cardlist but that's totally false. Hemaes19 is correct.
SergeyMoiseev
I am missing your point here. I am providing final code for product that works as described and my intention from a start was to create CardList as container component for CardComponent display. They are supposed to be used that way from a start and if you read my post from: 'Who will be responsible for displaying the cards?' you will be able to see why I think it's right way of dealing with component composition.
SergeyMoiseev
I guess you missed previous step before that: `For that, let’s add a new import to our AppModule:`. If you not import needed classes it's not working. And another point that you probably missed: source code for that post is here: https://github.com/bopm/getting-started-ng5 and it's final and working product with mutliple commits in more or less same levels of implementation as post goes. So you always can find all part there. It mentioned on a start of this post.
SergeyMoiseev
I am making assumption, that reader can tell source code of component from markup of component by extensions of files metioned and it's contents. Unfortunately if you not aware of html/javascript basics, this tutorial is too complex for you. But thanks for all remarks, as it's my first expirience like this one, I am definetely missed some points in making content generic in terms of naming things and all of that. I'll probably ask editors to replace some references soon.
Walter Torres
This is not a matter of JS basics. I've been using JS for 15 years. This is a matter of learning Angular, after your title is "Step by Step Guide to Your First Angular" With a title like that a student would assume that the teach would not make assumptions about what is meant, stated or not stated. A student would assume the teacher would speak in clear and price language. You have fallen into the "first time teacher" trap: writing and knowing what you mean, but your reader might not. Writing HOW-TOs are hard. Here's a suggestion for you: write a HOW-TO make a ham sandwich, write it down and hand it to a friend, ask them to do exactly, no more and no less than what you wrote down to make the sandwich. Your friend can not take any step that you have not defined, ie: "open door to cabinet where the bread resides." Is that simple? Well, I don't know your kitchen so I don't know which cabinet the bread is in, would I? Like I said, I really liked your tutorial, once I knew where your bread was stored..
SergeyMoiseev
Thanks for your explaination. I am going to reevaluate my text soon using your perspective.
awesomenessesity
been wondering about that part. thanks
awesomenessesity
I have to agree with @disqus_q977QKFAXo:disqus and @hemaes19:disqus , As im totally new to everything connected to Angular5, iv been following your guide step by step and i was confused, right after you made CardComp and CardListComp, you inserted 3 CardComps into the list, and said "here is how the web page is suppose to look". however you forgot to mention that you need to change the "<h1> welcome </h1>" part to "<app-card-list></app-card-list>" in the app.component.html file. That part threw me off and i had to come to the comments to find out how to fix it, luckily @martnfox:disqus commented on that exact issue and solved it for me.
Natasha Voloshyna
Hello Sergey. First of all thank you for putting together a great tutorial. It is the first time I am taking a stab at Angular and your tutorial seemed most inviting and thorough that I came across. Now going through it step by step. I was stuck for a while right after adding snippet below, which caused error "Property 'alive' does not exist on type 'NewCardInputComponent'.": this.newCardForm.valueChanges.pipe( filter((value) => this.newCardForm.valid), debounceTime(500), takeWhile(() => this.alive)).subscribe(data => { console.log(data); }); Now after looking into the source code, you define: private alive = true; and then do: ngOnDestroy() { this.alive = false; } But it is not in the tutorial... It would be helpful to mention that this variable needs to be declared.
adam coulon
Great work!!
ScottC
Before adding the extension I received: ./node_modules/raw-loader!./node_modules/postcss-loader/lib??embedded!./src/styles.css Module build failed: Error: Can't resolve '../node_modules/bootstrap/scss/bootstrap' in 'C:\getting-started-ng5\src' After adding the extension I get: ./node_modules/raw-loader!./node_modules/postcss-loader/lib??embedded!./src/styles.css Module build failed: Error: Can't resolve 'functions' in 'C:\getting-started-ng5\node_modules\bootstrap\scss' I'm trying to track this down, but not luck so far. Should there be a functions directory under ..\node_modules\bootstrap\scss? Here's the output from the bootstrap install: C:\getting-started-ng5>"c:\Program Files (x86)\Yarn\bin\yarn" add [email protected] yarn add v1.6.0 [1/4] Resolving packages... [2/4] Fetching packages... info [email protected]: The platform "win32" is incompatible with this module. info "[email protected]" is an optional dependency and failed compatibility check. Excluding it from installation. [3/4] Linking dependencies... warning " > [email protected]" has unmet peer dependency "[email protected] - 3". warning " > [email protected]" has unmet peer dependency "[email protected]^1.12.3". [4/4] Building fresh packages... success Saved lockfile. success Saved 1 new dependency. info Direct dependencies └─ [email protected] info All dependencies └─ [email protected] Done in 10.10s.
GBiscuits
Terrible. It like it's on the right path and has good content but chunks of it are missing. When did you create 'model/card'? There already 4-5 pieces of code missing and I'm not even half-way thru. Absolutely frustrating.
Bill Ward
I too was lost at this spot because my app wasn't showing the cards. Thanks @awesomenessesity:disqus and @martnfox:disqus for pointing in the right direction.
Dae Choi
This is by far the most comprehensive tutorial on angular. I enjoyed reading and trying out the ngRx section. Mind = blown. Thank you!
Gilbert
did you solve the issue? @SergeyMoiseev:disqus Please help I have the same issue. I think they restructure the angular.json please see screenshots: https://prnt.sc/jt3rmf http://i.prntscr.com/r5iObq-GSgWng1Dkugh62w.png
Gilbert
agreed
Phillip Walker
This was my first introduction to Angular5(6) and my second experience working with any Node.js based website and I just managed to make it through your tutorial. Thank you Sergey this was really great. Also, until now I have coded exclusively using plain ES5 Javascript and so everything: TypeScript, Firebase, ES6 features, etc. was new to me but I do have some previous experience coding Java/Python apps. I had some hiccup's here and there (for example wrong dependencies when using Angular 6, and issues using rxjs V6.0 with its newest changes, etc) but this tutorial is still totally doable. One thing I am not sure about. When I implemented the "Remove" card function as suggested, my implementation was such that if you clicked on the card it would be deleted. I had to setup a click @HostListener in 'card.component' to get this to work and propagate the event upward via EventEmitters. Is this the correct way? I read some literature on passing two-way messages between components using a Subject but I am not sure if this would be the proper way in this instance.
ula_usmanov
Thanks for this article. Tried to see result, cloned project from github and it doesn't run. There is angular.json file is missing and doesn't want to serve
ula_usmanov
Long story. Current version in 6, this was created in 5. Downgraded CLI to 1.6.0, changed "@angular/cli": "1.6.0", to "@angular/cli": "^1.6.0", and run npm update + npm install
comments powered by Disqus
Subscribe
Free email updates
Get the latest content first.
No spam. Just great articles & insights.
Free email updates
Get the latest content first.
Thank you for subscribing!
Check your inbox to confirm subscription. You'll start receiving posts after you confirm.
Trending articles
Relevant Technologies
About the author
Sergey Moiseev
Full-stack Angular and Ruby on Rails Developer
Sergey is a full-stack software engineer with experience developing complex web projects. He has extensive skills as an analyst and with gathering requirements for project development. He boasts practical experience in all levels of participation from full-stack developer to team lead/CTO. He is a top 5% answerer for AngularJS on StackOverflow, and top 10% on Ruby on Rails.