NexusCS

Angular 5

JavaScript libraries
Angular 5 made HttpClient the default, introduced the build optimizer, RxJS pipeable operators, forms updateOn, and i18n pipe improvements.
archived

Getting started

What's New in Angular 5

Angular 5 (November 2017) introduces HttpClient as the default HTTP library, build optimizer for smaller bundles, RxJS pipeable operators, and improved forms validation control.

Key Features

  • HttpClient with automatic JSON parsing
  • Build optimizer (~20% smaller bundles)
  • RxJS pipeable operators (RxJS 5.5)
  • Forms updateOn option
  • Improved i18n pipes (CLDR-based)
  • Service Worker / PWA support
  • AOT fast enough for dev

Installation

npm install -g @angular/cli@1.5
ng new my-app
cd my-app
ng serve

Version Requirements

Requirement Version
TypeScript 2.4+ (2.5 recommended)
RxJS 5.5+
Node.js 6.9+

HttpClient (New Default)

Basic Usage

import { HttpClient } from '@angular/common/http';

constructor(private http: HttpClient) {}

// Auto JSON parsing
this.http.get('/api/users')
  .subscribe(data => console.log(data));

HTTP module deprecated, use HttpClient.

Typed Responses

interface User {
  id: number;
  name: string;
}

this.http.get<User>("/api/user/1").subscribe((user) => {
  console.log(user.name); // Type-safe
});

TypeScript generics for responses.

HTTP Methods

// GET
this.http.get<T>("/api/data");

// POST
this.http.post<T>("/api/data", body);

// PUT
this.http.put<T>("/api/data/1", body);

// DELETE
this.http.delete("/api/data/1");

// PATCH
this.http.patch<T>("/api/data/1", partial);

All return Observable<T>.

Request Options

import { HttpHeaders, HttpParams } from "@angular/common/http";

const headers = new HttpHeaders({
  "Content-Type": "application/json",
  Authorization: "Bearer token",
});

const params = new HttpParams().set("page", "1").set("size", "10");

this.http.get("/api", { headers, params });

Immutable headers and params.

Error Handling

import { catchError } from "rxjs/operators";
import { throwError } from "rxjs";

this.http
  .get("/api")
  .pipe(
    catchError((error) => {
      console.error(error);
      return throwError(error);
    }),
  )
  .subscribe();

Use pipeable catchError operator.

Interceptors

import {
  HttpInterceptor,
  HttpRequest,
  HttpHandler,
} from "@angular/common/http";

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler) {
    const authReq = req.clone({
      headers: req.headers.set("Authorization", "Bearer token"),
    });
    return next.handle(authReq);
  }
}

Modify requests/responses globally.

Module Setup

import { HttpClientModule } from "@angular/common/http";

@NgModule({
  imports: [HttpClientModule],
})
export class AppModule {}

Import in root module.

Migration from Http

// OLD (Angular 4)
import { Http } from "@angular/http";
import "rxjs/add/operator/map";

this.http
  .get("/api")
  .map((res) => res.json())
  .subscribe();

// NEW (Angular 5)
import { HttpClient } from "@angular/common/http";

this.http.get("/api").subscribe(); // Auto-parses JSON

HttpClient auto-parses JSON responses.

RxJS Pipeable Operators

Basic Usage

import { map, filter } from "rxjs/operators";

observable
  .pipe(
    map((x) => x * 2),
    filter((x) => x > 10),
  )
  .subscribe();

Chain operators with pipe().

Migration

// OLD (patch operators)
import "rxjs/add/operator/map";
import "rxjs/add/operator/filter";

observable
  .map((x) => x * 2)
  .filter((x) => x > 10)
  .subscribe();

// NEW (pipeable)
import { map, filter } from "rxjs/operators";

observable
  .pipe(
    map((x) => x * 2),
    filter((x) => x > 10),
  )
  .subscribe();

Tree-shakeable imports.

Common Operators

import {
  map,
  filter,
  tap,
  switchMap,
  mergeMap,
  concatMap,
  catchError,
  retry,
  debounceTime,
  distinctUntilChanged,
  take,
  takeUntil,
  finalize,
} from "rxjs/operators";

Import from rxjs/operators.

Renamed Operators

Old Name New Name
do tap
catch catchError
switch switchAll
finally finalize

Example with HTTP

this.http
  .get<User[]>("/api/users")
  .pipe(
    map((users) => users.filter((u) => u.active)),
    tap((users) => console.log(users)),
    catchError((error) => throwError(error)),
  )
  .subscribe();

Functional composition pattern.

Forms: updateOn Option

Reactive Forms

import { FormControl } from "@angular/forms";

// Update on blur
const ctrl = new FormControl("", {
  updateOn: "blur",
});

// Update on submit
const ctrl = new FormControl("", {
  updateOn: "submit",
});

Control validation timing.

Form Group Level

import { FormGroup, FormControl } from "@angular/forms";

const form = new FormGroup(
  {
    email: new FormControl(""),
    password: new FormControl(""),
  },
  {
    updateOn: "submit",
  },
);

Apply to entire form.

Template-Driven Forms

<input name="email" ngModel [ngModelOptions]="{updateOn: 'blur'}" />

<form #f="ngForm" [ngFormOptions]="{updateOn: 'submit'}">
  <input name="email" ngModel />
  <input name="password" ngModel />
</form>

Use ngModelOptions for controls.

Available Options

Option Description
'change' Default, update on input
'blur' Update on focus loss
'submit' Update on form submit

Build Optimizer

Production Builds

# Build optimizer enabled by default
ng build --prod

# 20% smaller bundles
# Removes decorators from runtime
# Marks pure functions for tree-shaking

Automatic in production mode.

Disable Optimizer

# Not recommended
ng build --prod --build-optimizer=false

Rarely needed.

What It Does

  • Removes Angular decorators at runtime
  • Marks pure function calls (/*@__PURE__*/)
  • Enables aggressive tree-shaking
  • ~20% bundle size reduction

Internationalized Pipes

Currency Pipe

{{ price | currency }}
// Angular 4: USD4.99
// Angular 5: $4.99

{{ price | currency:'CAD' }}
// CA$4.99

{{ price | currency:'CAD':'symbol-narrow' }}
// $4.99

Default changed to 'symbol'.

Date Pipe (CLDR-based)

{{ today | date:'short' }}
// 8/15/17, 9:03 AM

{{ today | date:'medium' }}
// Aug 15, 2017, 9:03:01 AM

{{ today | date:'longDate' }}
// August 15, 2017

CLDR standard formats.

Percent Pipe

{{ 3.14159 | percent }}
// 314% (rounds by default)

{{ 3.14159 | percent:'1.4-4' }}
// 314.1592%

{{ 0.5 | percent }}
// 50%

Auto-rounding enabled.

Number Pipe

{{ 1234.5678 | number }}
// 1,234.568

{{ 1234.5678 | number:'1.0-0' }}
// 1,235

{{ 1234.5678 | number:'3.1-2' }}
// 1,234.57

Format: minIntegerDigits.minFractionDigits-maxFractionDigits.

Service Worker / PWA

Module Setup

import { ServiceWorkerModule } from "@angular/service-worker";
import { environment } from "../environments/environment";

@NgModule({
  imports: [
    ServiceWorkerModule.register("/ngsw-worker.js", {
      enabled: environment.production,
    }),
  ],
})
export class AppModule {}

Enable in production only.

Check for Updates

import { SwUpdate } from '@angular/service-worker';

constructor(private swUpdate: SwUpdate) {
  if (swUpdate.isEnabled) {
    swUpdate.available.subscribe(event => {
      if (confirm('New version available. Load it?')) {
        window.location.reload();
      }
    });
  }
}

Notify users of updates.

Generate Service Worker

ng set apps.0.serviceWorker=true
ng build --prod

Creates ngsw-config.json and worker.

Router Lifecycle Events

New Events

import { Router, GuardsCheckStart, ResolveEnd } from '@angular/router';

constructor(private router: Router) {
  router.events.subscribe(event => {
    if (event instanceof GuardsCheckStart) {
      // Show loading spinner
    }
    if (event instanceof ResolveEnd) {
      // Hide loading spinner
    }
  });
}

Finer-grained navigation tracking.

Event Types

  • GuardsCheckStart / GuardsCheckEnd
  • ResolveStart / ResolveEnd
  • ActivationStart / ActivationEnd
  • ChildActivationStart / ChildActivationEnd

Other Features

preserveWhitespaces

@Component({
  template: ` <p>Text with spaces</p> `,
  preserveWhitespaces: false,
})
export class MyComponent {}

Remove extra whitespace (smaller bundles).

Global Configuration

// tsconfig.json
{
  "angularCompilerOptions": {
    "preserveWhitespaces": false
  }
}

Apply to all components.

Multiple exportAs

@Directive({
  selector: "[appHighlight]",
  exportAs: "highlight, appHighlight",
})
export class HighlightDirective {}

Multiple template reference names.

Usage

<div appHighlight #h1="highlight" #h2="appHighlight">
  {{ h1 === h2 }}
  <!-- true -->
</div>

Both names reference same directive.

StaticInjector

import { Injector } from "@angular/core";

// OLD (Angular 4)
import { ReflectiveInjector } from "@angular/core";
const injector = ReflectiveInjector.resolveAndCreate([MyService]);

// NEW (Angular 5)
const injector = Injector.create({
  providers: [
    {
      provide: MyService,
      deps: [ConfigService, LoggerService],
    },
  ],
});

Explicit dependency array required.

Universal State Transfer

import { TransferState, makeStateKey } from '@angular/platform-browser';

const DATA_KEY = makeStateKey<any>('apiData');

constructor(private state: TransferState) {
  const data = this.state.get(DATA_KEY, null);
  if (data) {
    this.data = data; // Use server data
  } else {
    this.http.get('/api').subscribe(result => {
      this.data = result;
      this.state.set(DATA_KEY, result);
    });
  }
}

Avoid duplicate HTTP requests.

Zone-less Angular

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

platformBrowserDynamic()
  .bootstrapModule(AppModule, {
    ngZone: 'noop'
  });

// Manual change detection
constructor(private appRef: ApplicationRef) {}

updateData() {
  this.data = newData;
  this.appRef.tick(); // Trigger change detection
}

Manual change detection required.

AOT in Development

# Fast enough for dev now
ng serve --aot

# 95% faster incremental builds
# AOT as TypeScript transforms

Recommended for development.

Gotchas

Http Deprecated

// ❌ Don't use (deprecated)
import { Http } from "@angular/http";

// ✅ Use HttpClient
import { HttpClient } from "@angular/common/http";

Migrate to HttpClient.

Pipeable Operators

// ❌ Old style (still works)
import "rxjs/add/operator/map";
observable.map((x) => x);

// ✅ New style (preferred)
import { map } from "rxjs/operators";
observable.pipe(map((x) => x));

Better tree-shaking.

StaticInjector deps

// ❌ Missing deps array
Injector.create({
  providers: [{ provide: MyService }],
});

// ✅ Explicit deps
Injector.create({
  providers: [
    {
      provide: MyService,
      deps: [ConfigService],
    },
  ],
});

Deps array now required.

Currency Pipe Default

// Angular 4
{{ 9.99 | currency }} // USD9.99

// Angular 5
{{ 9.99 | currency }} // $9.99

// Explicit code
{{ 9.99 | currency:'USD':'code' }} // USD9.99

Default changed from 'code' to 'symbol'.

Percent Pipe Rounding

// Angular 4
{{ 3.14159 | percent }} // 314.159%

// Angular 5
{{ 3.14159 | percent }} // 314%

// Explicit precision
{{ 3.14159 | percent:'1.4-4' }} // 314.1592%

Auto-rounding enabled.

Template Tag

<!-- ❌ Deprecated (disabled by default) -->
<template>Content</template>

<!-- ✅ Use ng-template -->
<ng-template>Content</ng-template>

Use <ng-template> instead.

TypeScript Version

Requires TypeScript 2.4+, recommends 2.5+.

Also see