NexusCS

Angular 4

JavaScript libraries
Angular 4 introduced animations as a separate package, HttpClient (in 4.3), enhanced *ngIf with else/as syntax, and Renderer2.
archived

Getting started

What's New in Angular 4

Angular 4 major improvements.

Enhanced *ngIf

// if/else syntax
<div *ngIf="user$ | async; else loading; let user">
  {{ user.name }}
</div>
<ng-template #loading>Loading...</ng-template>
// as keyword
<div *ngIf="user$ | async as user">
  {{ user.name }}
</div>

ng-template

<!-- OLD (deprecated) -->
<template ngFor let-hero [ngForOf]="heroes"> {{ hero.name }} </template>

<!-- NEW -->
<ng-template ngFor let-hero [ngForOf]="heroes"> {{ hero.name }} </ng-template>

Replaces <template> tag.

Version Notice

Angular 3 was skipped (v2 → v4). TypeScript 2.1+ required.

Animations

Setup

import { BrowserAnimationsModule } from "@angular/platform-browser/animations";

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

Animations moved to separate package.

Basic Animation

import { trigger, state, style, transition, animate } from '@angular/animations';

@Component({
  selector: 'app-hero',
  animations: [
    trigger('heroState', [
      state('inactive', style({
        backgroundColor: '#eee',
        transform: 'scale(1)'
      })),
      state('active', style({
        backgroundColor: '#cfd8dc',
        transform: 'scale(1.1)'
      })),
      transition('inactive => active', animate('100ms ease-in')),
      transition('active => inactive', animate('100ms ease-out'))
    ])
  ]
})

Enter/Leave Transitions

trigger("flyInOut", [
  transition(":enter", [
    style({ transform: "translateX(-100%)" }),
    animate("100ms"),
  ]),
  transition(":leave", [
    animate("100ms", style({ transform: "translateX(100%)" })),
  ]),
]);
<div [@flyInOut]="state"></div>

Keyframes

import { keyframes } from "@angular/animations";

animate(
  300,
  keyframes([
    style({ opacity: 0, transform: "translateY(-100%)", offset: 0 }),
    style({ opacity: 1, transform: "translateY(15px)", offset: 0.3 }),
    style({ opacity: 1, transform: "translateY(0)", offset: 1.0 }),
  ]),
);

Animation Callbacks

<div
  (@flyInOut.start)="animStart($event)"
  (@flyInOut.done)="animDone($event)"
  [@flyInOut]="state"
></div>
animStart(event: AnimationEvent) {
  console.log('Animation started', event);
}

animDone(event: AnimationEvent) {
  console.log('Animation done', event);
}

Group Animations

import { group } from "@angular/animations";

transition("* => *", [
  group([
    animate("1s", style({ opacity: 0 })),
    animate("1s", style({ transform: "translateX(100%)" })),
  ]),
]);

HttpClient (4.3+)

Setup

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

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

Introduced in Angular 4.3.

Basic Requests

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

constructor(private http: HttpClient) {}

// Typed GET
this.http.get<Hero[]>('/api/heroes')
  .subscribe(heroes => {
    this.heroes = heroes;
  });

// POST
this.http.post<Hero>('/api/heroes', hero)
  .subscribe(result => {
    console.log(result);
  });

// PUT
this.http.put<Hero>('/api/heroes/1', hero)
  .subscribe();

// DELETE
this.http.delete('/api/heroes/1')
  .subscribe();

Error Handling

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

this.http
  .get<Hero[]>("/api/heroes")
  .pipe(
    catchError((err) => {
      console.error(err);
      return of([]);
    }),
  )
  .subscribe((heroes) => {});

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("limit", "10");

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

Interceptors

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

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

Register Interceptor

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

@NgModule({
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthInterceptor,
      multi: true
    }
  ]
})

Response Type

// JSON (default)
this.http.get<any>("/api/data").subscribe();

// Text
this.http.get("/api/text", { responseType: "text" }).subscribe();

// Blob
this.http.get("/api/file", { responseType: "blob" }).subscribe();

// Full response
this.http.get("/api/data", { observe: "response" }).subscribe((resp) => {
  console.log(resp.headers);
  console.log(resp.body);
});

Renderer2

Replacing Renderer

import { Renderer2, ElementRef } from '@angular/core';

constructor(
  private renderer: Renderer2,
  private el: ElementRef
) {}

Renderer deprecated, use Renderer2.

Style Manipulation

// Set style
this.renderer.setStyle(this.el.nativeElement, "color", "red");

// Remove style
this.renderer.removeStyle(this.el.nativeElement, "color");

Class Manipulation

// Add class
this.renderer.addClass(this.el.nativeElement, "active");

// Remove class
this.renderer.removeClass(this.el.nativeElement, "inactive");

Attribute Manipulation

// Set attribute
this.renderer.setAttribute(this.el.nativeElement, "role", "button");

// Remove attribute
this.renderer.removeAttribute(this.el.nativeElement, "disabled");

Element Creation

// Create element
const div = this.renderer.createElement("div");

// Create text
const text = this.renderer.createText("Hello");

// Append child
this.renderer.appendChild(this.el.nativeElement, div);

// Remove child
this.renderer.removeChild(this.el.nativeElement, div);

Event Listeners

const unlisten = this.renderer.listen(
  this.el.nativeElement,
  "click",
  (event) => {
    console.log("Clicked", event);
  },
);

// Cleanup
unlisten();

Properties

// Set property
this.renderer.setProperty(this.el.nativeElement, "value", "New Value");

Router

ParamMap

import { ActivatedRoute, ParamMap } from '@angular/router';
import { switchMap } from 'rxjs/operators';

constructor(
  private route: ActivatedRoute,
  private service: HeroService
) {}

Route Parameters

ngOnInit() {
  this.route.paramMap.pipe(
    switchMap((params: ParamMap) =>
      this.service.getHero(params.get('id'))
    )
  ).subscribe(hero => {
    this.hero = hero;
  });
}

Query Parameters

ngOnInit() {
  this.route.queryParamMap.subscribe(params => {
    const filter = params.get('filter');
    const page = params.get('page');
  });
}

Get All Parameters

// All route params
params.keys.forEach((key) => {
  console.log(key, params.get(key));
});

// Check if param exists
if (params.has("id")) {
  const id = params.get("id");
}

// Get all values for key
const ids = params.getAll("id");

Navigate with Params

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

constructor(private router: Router) {}

// Navigate with route params
this.router.navigate(['/hero', heroId]);

// Navigate with query params
this.router.navigate(['/heroes'], {
  queryParams: { filter: 'active', page: 1 }
});

Lifecycle Hooks

Interfaces vs Abstract Classes

import { OnInit, OnDestroy, OnChanges } from "@angular/core";

export class MyComponent implements OnInit, OnDestroy {
  ngOnInit() {
    console.log("Component initialized");
  }

  ngOnDestroy() {
    console.log("Component destroyed");
  }
}

Now interfaces instead of abstract classes.

Hook Order

Hook Description
ngOnChanges Input property changes
ngOnInit First change detection
ngDoCheck Every change detection
ngAfterContentInit Content projection done
ngAfterContentChecked Content checked
ngAfterViewInit View initialization done
ngAfterViewChecked View checked
ngOnDestroy Before component destroyed

Forms

Template-driven Forms

import { FormsModule } from '@angular/forms';

@NgModule({
  imports: [FormsModule]
})
<form #heroForm="ngForm">
  <input [(ngModel)]="hero.name" name="name" required #name="ngModel" />

  <div *ngIf="name.invalid && name.touched">Name is required</div>

  <button [disabled]="heroForm.invalid">Submit</button>
</form>

Reactive Forms

import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';

@NgModule({
  imports: [ReactiveFormsModule]
})
export class HeroFormComponent {
  heroForm = this.fb.group({
    name: ["", Validators.required],
    power: ["", Validators.required],
  });

  constructor(private fb: FormBuilder) {}

  onSubmit() {
    console.log(this.heroForm.value);
  }
}
<form [formGroup]="heroForm" (ngSubmit)="onSubmit()">
  <input formControlName="name" />
  <input formControlName="power" />
  <button [disabled]="heroForm.invalid">Submit</button>
</form>

Pipes

Built-in Pipes

// Date
{{ birthday | date:'medium' }}
{{ birthday | date:'yyyy-MM-dd' }}

// Currency
{{ price | currency:'USD':true }}
{{ price | currency:'EUR':'symbol':'1.2-2' }}

// Decimal
{{ pi | number:'3.1-5' }}

// Percent
{{ ratio | percent:'2.1-2' }}

// Uppercase/Lowercase
{{ name | uppercase }}
{{ name | lowercase }}

// JSON
{{ object | json }}

// Slice
{{ [1,2,3,4,5] | slice:1:3 }}

// Async
{{ promise | async }}
{{ observable$ | async }}

Custom Pipe

import { Pipe, PipeTransform } from "@angular/core";

@Pipe({ name: "exponential" })
export class ExponentialPipe implements PipeTransform {
  transform(value: number, exponent = 1): number {
    return Math.pow(value, exponent);
  }
}
{{ 2 | exponential:10 }}

Directives

Structural Directives

<!-- *ngIf -->
<div *ngIf="condition">Content</div>
<div *ngIf="condition; else elseBlock">Content</div>
<ng-template #elseBlock>Else content</ng-template>

<!-- *ngFor -->
<div *ngFor="let item of items; let i = index">{{ i }}: {{ item }}</div>

<!-- *ngSwitch -->
<div [ngSwitch]="value">
  <div *ngSwitchCase="1">One</div>
  <div *ngSwitchCase="2">Two</div>
  <div *ngSwitchDefault>Other</div>
</div>

Attribute Directives

<!-- ngClass -->
<div [ngClass]="{'active': isActive, 'disabled': isDisabled}">
  <!-- ngStyle -->
  <div [ngStyle]="{'color': color, 'font-size': size + 'px'}">
    <!-- ngModel -->
    <input [(ngModel)]="name" />
  </div>
</div>

Custom Directive

import { Directive, ElementRef, HostListener } from "@angular/core";

@Directive({
  selector: "[appHighlight]",
})
export class HighlightDirective {
  constructor(private el: ElementRef) {}

  @HostListener("mouseenter") onMouseEnter() {
    this.el.nativeElement.style.backgroundColor = "yellow";
  }

  @HostListener("mouseleave") onMouseLeave() {
    this.el.nativeElement.style.backgroundColor = null;
  }
}
<p appHighlight>Hover me</p>

Gotchas

Migration Issues

HttpClient (4.3+) is in @angular/common/http, old Http is in @angular/http.

Animations moved to @angular/animations (separate install).

<template> deprecated → use <ng-template>.

Breaking Changes

Renderer deprecated → use Renderer2.

Lifecycle hooks changed from abstract classes to interfaces.

TypeScript 2.1+ required.

Also see

Angular 4 Cheatsheet - NexusCS