Web Application development with Angular

Learn the basics and intermediate topics of Angular.

by Gion Kunz

Gion Kunz

  • Angular GDE since 2018
  • Front-end developer, web standards advocate
  • Specialized in spa development using Angular
  • Onsite ramp-ups, coaching and implementation support in projects
  • Author of the book "Mastering Angular Components"
  • Frequent speaker at conferences and meetups
  • Teacher for web development and UX engineering at the SAE Institute in Zurich
  • Tutor at O'Reilly Safari Online Training
  • Contributor on the Angular project, author of Chartist.js library

Founder, UI Developer, UX Enthusiast at syncrea GmbH

If I am not in the Web universe, I  to observe the real one.

M31 Andromeda Galaxy, ~2.5 million light-years from Earth and the nearest large galaxy to the Milky Way

Content

  • TypeScript Refresher (optional)
  • Tooling (Node, Angular CLI, Webpack) and Local Development Setup
  • Components, templates and bootstrapping
  • Property and Event Bindings
  • View Variables, Pipes and Template Elements
  • Input and Output properties
  • Routing
  • HttpClient Service and simple RESTful interactions
  • Testing (optional)
  • Creating a simple checklist app with Nx Workspace and Angular

TypeScript Refresher

Prerequisites

  1. You have your Laptop with internet access
  2. Access to https://stackblitz.com
  3. If you want to keep your history, create an account!

Let's Hack!

TypeScript Base

No Implicit Any

// tsconfig.json
{
  "compilerOptions": {
    "noImplicitAny": true
  }
}

Any and Unknown

let x: any = 10;
x = 'anything';

let y: unknown = 5;
y = 'unknown';

let z: number;
z = x;
// Not possible!
z = y;

Strict Null-Checks

// tsconfig.json
{
  "compilerOptions": {
     "noImplicitAny": true,
     "strictNullChecks": true
  }
}

How to Deal with possible null values

  • Non-Null Assertion Operator !
  • die() helper function
let x: string | null;

x!.toLowerCase();

function die(message: string): never {
  throw new Error(message);
}

(x || die('Ooops...')).toLowerCase();

Type Declarations

Use "Native" Types

let n: number;
// Not this -> let n: Number;

let b: boolean;
// Not this -> let b: Boolean;

let s: string;
// Not this -> let s: String;

Skip Declaration when Initialized with Value

const n = 100;
// Not this -> const n: number = 100;

const x = 'Hello World';
// Not this -> const x: string = 'Hello World';

Tuple Type Declarations

type NameAgeTuple = [string, number];

const nameAndAge: NameAgeTuple = ['Peter', 52];

Literal Types

type AnswerToAllQuestions = 42;
// Error!
const n: AnswerToAllQuestions = 13;

Union Literal Types

type FibonacciNumber = 1 | 2 | 3 | 5 | 8 | 13;
// Error!
const n: FibonacciNumber = 4;

String Literal Union Types as Enumeration Type

type OrderStatus = 'Ordered' | 'Paid' | 'Shipped' | 'Delivered';

interface Order {
  id: number;
  status: OrderStatus;
}

const order: Order = {
  id: 1,
  status: 'Ordered'
};

Intersection Types

interface Cat {
  name: string;
  meow: () => void;
}

interface Dog {
  name: string;
  bark: () => void;
}

export type CatDog = Cat & Dog;

ECMAScript 2015 - 2019

ECMAScript Modules

// a.ts
const magicNumber = 13;

export const modulePublic = 'This is public';

export function multiplyWithMagicNumber(n: number): number {
  return magicNumber * n;
}

// Default export
export default [1, 2, 3, 5, 8, 13];
// index.ts
import {
  modulePublic as moduleAText, 
  multiplyWithMagicNumber
} from './a';
import fibonacci from './a';
import * as A from './a';

console.log(moduleAText);
console.log(multiplyWithMagicNumber(7));
console.log(fibonacci);

console.log(A.default === fibonacci);
console.log(A.modulePublic === moduleAText);

Var vs. Let vs. Const

function closure() {
  var closureScoped = 1;
  for(let i = 0; i < 10; i++) {

  }
  // Error
  console.log(i);
}

Template Strings

const peter = {
  name: 'Peter'
};

console.log(`My name is ${peter.name}!`);

Loops Revived

const list = [1, 2, 3, 4, 5, 6, 7];

// Traditional but block scope counter
for(let i = 0; i < list.length; i++) {
  console.log(i);
}

// Functional
list.forEach(console.log);

// for...of loop
for(let item of list) {
  console.log(item);
}

Arrow Functions

const multiply = (a: number, b: number) => a * b;

Destructuring

// Array destructuring
const list = [1, 2, 3];
const [first, second, third] = list;

// Object destructuring
const peter = {
  firstName: 'Peter',
  lastName: 'Griffin'
};
const {firstName, lastName} = peter;

Parameter Destructuring and Defaults

export interface ConfigurationOptions {
  delay: number;
  host: string;
  limit: number;
}

function configure({delay = 1000, 
                    host = 'localhost',
                    limit = 300}: Partial<ConfigurationOptions>) {
  console.log(host);
  console.log(delay);
  console.log(limit);
}

Spreading

// Array spreading
const list = [1, 2, 3, 4];
const appended = [...list, 5, 6, 7];

// Object property spreading
const peter = {
  firstName: 'Peter',
  lastName: 'Griffin'
};
const peterWithAge = {
  ...peter,
  age: 52
};

Async / Await

// Traditional promise chain
function invalidateUsersLame() {
  return fetch('/api/users')
    .then(usersResponse => usersResponse.json())
    .then((users: User[]) => 
      Promise.all(users.map(user => 
        fetch(`/api/users/${user.id}/invalidate`).then(() => user)
      ))
    );
}

Async / Await

// Async await löve!
async function invalidateUsers() {
  const usersResponse = await fetch('/api/users');
  const users: User[] = await usersResponse.json();
  for(let user of users) {
    await fetch(`/api/users/${user.id}/invalidate`);
  }
  return users;
}

Functional Primer

Pure Functions

// Pure
function multiply(a: number, b: number): number {
  return a * b;
}

// Impure
const name = 'Peter';

function getNameLength() {
  return name.length;
}

As Much Pureness as Possible!

  • Very simple to reason about
  • No dependencies other than parameter
  • Reproducable
  • Highly deterministic
  • Simple to test
  • Cachable!

Immutability - What's wrong with mutability?

  • Object property mutation changes state, but consumers are not notified.
  • Mutable data is inherently complex, because it can change.
  • Mutable data can lead to distributed dependencies, which would need to be notified about change.
  • Harder to be confident about correctness of the code.
  • Reference Problem: The nested references

Immutability by using readonly

interface Person {
  readonly name: string;
  readonly age: number;
}

const person: Person = {
  name: 'Peter',
  age: 52
};

// Error!
person.name = 'Pete';

Convert mutable structures with Readonly<T>

interface Person {
  name: string;
  age: number;
}

const person: Readonly<Person> = {
  name: 'Peter',
  age: 52
};

// Error!
person.name = 'Pete';

Immutable Arrays, Sets and Maps

const list: ReadonlyArray<number> = [1, 2, 3, 4];
// Error!
list[0] = 10;
// Error!
list.sort();
// Error!
list.fill(42);

OOP > FP > Pragmatic TypeScript

Interfaces with Object Literals

export interface Person {
  firstName: string;
  lastName: string;
}

const person: Person = {
  firstName: 'Peter',
  lastName: 'Griffin'
};

Interfaces with Object Literals

  • Use interfaces to define your model / data.
  • If too much boilerplate for construction, use object factory functions.
  • Implement any logic outside your model using pure helper functions.

What's Wrong with OOP and Encapsulation?

  • One does not combine data and behavior to create living objects...
  • Mostly the types with behaviors metaphor is not taking you that far.
  • We have learned that Inheritance is a bad thing to begin with and we should avoid it.
  • Implementing behavior classes and using composition is suggested but it's just a work-around.
  • Encapsulation ≠ Data Hiding

The Multiple Inheritance Dilemma

export class DesigningDeveloper {
  design(): void {
    // Can design
  }

  develop(): void {
    // Can develop
  }
}

Angular Basics & Intermediate

Angular Overview

Key Features

  • CLI Tool
  • TypeScript
  • Multi-Platform Support (Web Workers, Server, NativeScript)
  • Build on Web Components Concepts (Shadow DOM)
  • Component System using composition
  • Ahead of time compilation (AoT)
  • Server-Side Rendering (SSR)
  • PWA Support
  • Router with Lazy Loading

Angular

React

Vue.js

Component Rendering

Router

TypeScript Friendliness

Animation

Multi-Platform

Server Side Rendering

State Management

Core Support

Core Support

Core Support

Written in TypeScript

TypeScript JSX and good support

Good support

Core Support

react-router

vue-router

Core Support

react-move, react-motion and others

Core Support

vue-server-renderer

Core Support

Virtual DOM, react native

Render Adapter, Web Worker, Server, Native

Weex, supported but limited

Virual DOM, roll-your-own, Next.js

ngrx store 3rd party

Redux 3rd party

vuex

Angular is an all-round carefree package with optional customization

Angular Lifecycle

Angular Tooling

Node.js & NPM

  • Used for Tooling (TypeScript, Webpack, SystemJS, AoT Compiler etc.)
  • Can be used for Server-Side-Rendering
  • Angular CLI runs on Node.js
  • Version > 12.x is currently used in Angular

Angular CLI

  • Quickly get started with Angular
  • Whole tooling ecosystem including
  • TypeScript, Karma, Protractor E2E, AoT Compilation and more
  • Generator for project maintenance

Lab 1

Complete local setup and create first project using Angular CLI.

Prerequisites

First Steps

# Install Angular CLI
npm install -g @angular/cli

# Create new project
ng new my-first-app
cd my-first-app

# Create components with generator
ng generate component my-component

# Start dev server
ng serve

# Create production build
ng build

Ngmodule, Components and Bootstrap

The @Component decorator

import {Component} from '@angular/core';

@Component({
  selector: 'my-component',
  template: '<p>Hello World!</p>'
})
export class MyComponent {}

NgModules and Components

Module A

Module B

import

1

2

3

4

5

6

7

8

9

Components

Components

runtime

1

2

3

4

5

6

7

8

9

The @NgModule decorator

import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';

@NgModule({
  imports: [BrowserModule],
  declarations: [Comp1, Comp2, Comp3, ...],
  exports: [Comp1, Comp2, Comp3, ...],
  bootstrap: [Component]
})
export class AppModule {}

Bootstrapping

import {platformBrowserDynamic} 
  from '@angular/platform-browser-dynamic';
import {AppModule} from './app-module';

platformBrowserDynamic().bootstrapModule(AppModule);

Component Template Syntax

Expression Bindings

@Component({
  selector: 'app'
  template: '<div>{{message}}</div>'
})
class AppComponent {
  message: string = 'Hello World!';
}
  • Using double curly braces (mustache)
  • Scope is limited to component properties only
  • Supports simple expressions (accessors, operators, function calls etc.)

Property Bindings

@Component({
  selector: 'counter'
  template: '<input type="text" [value]="num">'
})
class AppComponent {
  num: number = 0;

  constructor() {
    setInterval(() => this.num++, 1000);
  }
}

Different Property Binding Types

<p [title]="title"></p>
<p [attr.role]="role"></p>
<p [class.is-active]="isActive()"></p>
<p [style.display]="!isActive() ? 'none' : null"></p>

Regular DOM property

Attribute binding (setAttribute)

CSS class binding (classList)

CSS style binding

Event Bindings

@Component({
  selector: 'app'
  template: `
    <button (click)="onClick()">Click me</button>
  `
})
class AppComponent {
  onClick() {
    alert('Cool!');
  }
}

Local View Variable $event

<input (input)="handleInput($event.target.value)">
  • Use native DOM Event Object
  • No new API to learn
  • Can be used to stop propagation / prevent default browser behavior

Some event bindings support selectors

@Component({
  selector: 'app'
  template: `
    <input (keydown.enter)="submit($event.target.value)">
  `
})
class AppComponent {
  submit(value: string) {
    console.log(value);
  }
}

Repeating template using the NgFor directive

<ul>
  <li *ngFor="let item of items">
    {{item}}
  </li>
</ul>
  • Using *asterisk notation to indicate template elements
  • NgFor DSL (let x of x)

Conditional template using the NgIf directive

<p>Sheeps: {{sheepCount}}</p>
<p *ngIf="sheepCount > 1000">
  You should be asleep now!
</p>
  • Also uses *asterisk syntax, to create template element
  • Will be attached to DOM when condition is truthy, removed when falsey

Lab 2

Create simple click counter component.

Component Communication

Component Input

import {Input} from '@angular/core';

@Component({
  selector: 'app-message',
  template: '{{message}}'
})
export class MessageComponent {
  @Input() message: string;
}
  • Using the @Input decorator on component properties
  • Input can be passed to component by parent component

Binding to Input

@Component({
  selector: 'app-root',
  template: '<app-message [message]="message"></app-message>'
})
export class AppComponent {
  message: string = 'Hello World!';
}

Component Output

import {Output, EventEmitter} from '@angular/core';

@Component({
  selector: 'timer',
  template: '<button (click)="startTimer()">Start</button>'
})
export class TimerComponent {
  @Output() timeout = new EventEmitter();

  startTimer() {
    setTimeout(
      () => this.timeout.emit('I timed out!'),
      5000
    );
  }
}

Capturing Output

@Component({
  selector: 'app',
  template: '<timer (timeout)="onTimeout($event)"></timer>'
})
export class AppComponent {
  onTimeout(message) {
    console.log(message);
  }
}

Communication in Component Tree

Example: Use component communication to create composition

Lab 3

A small component communication exercise.

The Router

A Router serves three main purposes

  • Navigablity & Bookmarkability
  • UI Composition
  • State

Configuration

import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {RouterModule} from '@angular/router';
import {MainComponent} from './main.component.ts';
import {ChildlComponent} from './child1/child1.component.ts';
import {Child2Component} from './child2/child2.component.ts';

@NgModule({
  imports: [
    BrowserModule,
    RouterModule.forRoot([
      {path: 'childl', component: ChildlComponent},
      {path: 'child2', component: Child2Component}
    ])
  ],
  declarations: [MainComponent, ChildlComponent, Child2Component],
  bootstrap: [MainComponent]
})
export class MainModule {}

Redirects and Fallback Component

RouterModule.forRoot([{
  path: 'child1',
  component: Child1Component
},{
  path: '',
  pathMatch: 'full',
  redirectTo: '/child1'
},{
  path: '**',
  component: PageNotFoundComponent
}];
  • Supports default (empty) path match
  • Supports redirects
  • Supports fallback routes
  • Sequence matters, first match wins strategy

Router Directives

Router Outlet

<h1>Website</h1>

<main class="content">
  <router-outlet></router-outlet>
</main>
  • Marks the position where Angular will put routed components
  • Can be nested
  • Can be named for auxiliary routes

Router Link Directives

<h1>Welcome!</h1>
<nav>
  <a routerLink="/child1"
     routerLinkActive="active">Child1</a>
  <a routerLink="/child2"
     routerLinkActive="active">Child2</a>
</nav>
<router-outlet></router-outlet>
  • Generate links to routed components
  • Use RouterLinkActive to attach CSS classes on active routes
  • Supports Router DSL

Route Parameters

Route Configuration with Parameters

RouterModule.forRoot([{
  path: 'child/:id',
  component: ChildComponent
}])
  • Use placeholders in route configurations
  • Parameters in the URL will be parsed and passed to the activated components

Accessing Parameters in Routed Components using Snapshot

import {ActivatedRoute} from '@angular/router';

@Component({
  selector: 'ngc-child',
  template: 'Child:<p>Param: {{params.id}}</p>'
})
export class ChildComponent {
  constructor(route: ActivatedRoute) {
    this.params = route.snapshot.params;
  }
}
  • Access route parameters by injecting ActivatedRoute
  • Use snapshot to get current parameter value

By default, the router re-uses a component instance when it navigates to the same component.

Using Observable Parameters

@Component({
  selector: 'ngc-child',
  template: `
   Child:
    <p>Param: {{params.id}}</p>`
})
export class ChildComponent {
  params: any;

  constructor(route: ActivatedRoute) {
    route.params.subscribe((params) => {
      this.params = params;
    });
  }
}
  • Params is an RxJS Observable
  • We can subscribe to parameter changes using a callback

Lab 4

Using the router to make an existing application routable.

Angular HttpClient Service

HttpClient returns Observables

@Component({
  selector: 'my-component',
  template: `
   <ul>
    <li *ngFor="let item of items | asnyc">{{item}}</li>
   </ul>`
})
export class MyComponent {
  items: Observable<Item[]>;

  constructor(http: HttpClient) {
    this.items = this.http
      .get<Item[]>('/api/items');
  }
}

HttpClient API

class HttpClient {
  delete<T>(url: string, options?: {...}): Observable<T>
  get<T>(url: string, options?: {...}): Observable<T>
  head<T>(url: string, options?: {...}): Observable<T>
  jsonp<T>(url: string, callbackParam?: string): Observable<T>
  options<T>(url: string, options?: {...}): Observable<any>
  patch<T>(url: string, body: any | null, options?: {...}): Observable<T>
  post<T>(url: string, body: any | null, options?: {...}): Observable<T>
  put<T>(url: string, body: any | null, options?: {...}): Observable<T>
}

Lab 5

Use HttpClient service to load data asynchronously.

TESTING

Testing a simple Counter Component

Testing with TestBed

import {
  TestBed, 
  ComponentFixture} from '@angular/core/testing';
import {MainComponent} from './main.component';
import {CounterService} from './counter.service';

describe('MainComponent', () => {
  let fixture: ComponentFixture<MainComponent>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [MainComponent],
      providers: [CounterService]
    });

    fixture = TestBed.createComponent(MainComponent);
  });
});
  • Use TestBed to create dynamic test module

  • Include Components and dependencies within the test module

Component Fixture

class ComponentFixture<T> {
    debugElement: DebugElement
    componentInstance: T
    nativeElement: any
    elementRef: ElementRef
    changeDetectorRef: ChangeDetectorRef
    componentRef: ComponentRef<T>
    detectChanges(): void
    isStable(): boolean
    whenStable(): Promise<any>
    destroy(): void
}
  • Wrapper around instantiated component

  • Access debug element, native host element and change detector

Debug Element

class DebugElement {
    nativeElement: any
    query(predicate: Predicate<DebugElement>): DebugElement
    queryAll(predicate: Predicate<DebugElement>): DebugElement[]
    children: DebugElement[]
    triggerEventHandler(eventName: string, eventObj: any)
}
  • Wrapper around component host element

  • Allows to travers through DebugElement tree (DOM like)

  • Can be used to tigger events

Query for DebugElements

import {DebugElement} from '@angular/core';
import {By} from '@angular/platform-browser';

const countElement = fixture.debugElement
  .query(By.css('.count'))
  .nativeElement;

console.log(countElement.textContent);

Triggering Events on DebugElements

import {DebugElement} from '@angular/core';

const buttonDebugElement: DebugElement = 
  fixture.debugElement.query(By.css('button'));

buttonDebugElement.triggerEventHandler('click', null);

Mocking dependencies

import {CounterService} from './counter.service';

export class CounterServiceMock() {
  increment() {}
}

TestBed.configureTestingModule({
  declarations: [MainComponent],
  providers: [{
    provide: CounterService, 
    useClass: CounterServiceMock
  }]
});

Lab 6

Testing our ToDo App.

Checklist App

Nx to manage your workspace

# Create a nx workspace
npx create-nx-workspace

# Install Nx globally
npm install -g nx

# Start the application
cd my-workspace
nx serve my-app

Let's create together.

Thanks!

Ressources