NexusCS

Angular 2

JavaScript libraries
Angular 2 is the TypeScript-based rewrite of AngularJS, introducing components, modules, and a new template syntax.
archived

Getting started

Component

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

@Component({
  selector: "my-app",
  template: `<h1>{{ title }}</h1>`,
  styles: [
    `
      h1 {
        color: blue;
      }
    `,
  ],
})
export class AppComponent {
  title = "My App";
}

Defines reusable UI components

Module

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

@NgModule({
  imports: [BrowserModule],
  declarations: [AppComponent],
  providers: [HeroService],
  bootstrap: [AppComponent],
})
export class AppModule {}

Groups related components/services

Bootstrap

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

platformBrowserDynamic().bootstrapModule(AppModule);

Launches the application

Template Syntax

Interpolation

<h1>{{title}}</h1>
<p>{{hero.name}}</p>
<div>{{1 + 1}}</div>

Binds component data

Property Binding

<button [disabled]="isDisabled">Click</button>
<img [src]="heroImageUrl" />
<app-hero [hero]="selectedHero"></app-hero>

One-way data binding

Event Binding

<button (click)="onSave()">Save</button>
<input (keyup)="onKey($event)" />
<app-hero (deleted)="onHeroDeleted($event)"></app-hero>

Responds to user events

Two-Way Binding

<input [(ngModel)]="hero.name" />

Combines property + event binding

Attribute Binding

<button [attr.aria-label]="help">Help</button>
<td [attr.colspan]="colSpan">Content</td>

Binds to HTML attributes

Class Binding

<div [class.active]="isActive">Content</div>
<div [ngClass]="{'active': isActive, 'disabled': isDisabled}"></div>

Toggles CSS classes

Style Binding

<div [style.color]="color">Colored text</div>
<div [style.font-size.px]="fontSize">Text</div>
<div [ngStyle]="{'color': color, 'font-size.px': fontSize}"></div>

Sets inline styles dynamically

Directives

*ngIf

<div *ngIf="hero">{{hero.name}}</div>
<div *ngIf="heroes.length > 0">Has heroes</div>
<div *ngIf="condition; else elseBlock">Content</div>
<ng-template #elseBlock>Else content</ng-template>

Conditional rendering

*ngFor

<div *ngFor="let hero of heroes">{{hero.name}}</div>
<div *ngFor="let hero of heroes; let i = index">{{i}}: {{hero.name}}</div>
<div *ngFor="let hero of heroes; trackBy: trackByHeroId"></div>

Loop over collections

ngSwitch

<div [ngSwitch]="hero?.emotion">
  <div *ngSwitchCase="'happy'">Happy</div>
  <div *ngSwitchCase="'sad'">Sad</div>
  <div *ngSwitchDefault>Confused</div>
</div>

Switch statement for templates

Template References

<input #heroInput /> <button (click)="callHero(heroInput.value)">Call</button>

Reference DOM elements

Decorators

Class Decorators

Decorator Purpose
@Component Component metadata
@NgModule Module definition
@Injectable DI service
@Directive Directive definition
@Pipe Pipe definition

Property Decorators

Decorator Purpose
@Input Input property
@Output Event emitter
@ViewChild Query view element
@ContentChild Query projected content
@HostListener Host event listener
@HostBinding Host property binding

@Input / @Output

import { Component, Input, Output, EventEmitter } from "@angular/core";

@Component({
  selector: "app-hero",
  template: `
    <h2>{{ hero.name }}</h2>
    <button (click)="delete()">Delete</button>
  `,
})
export class HeroComponent {
  @Input() hero: Hero;
  @Output() deleted = new EventEmitter<Hero>();

  delete() {
    this.deleted.emit(this.hero);
  }
}

Parent-child communication

Lifecycle Hooks

Hook Order

Hook When Called
ngOnChanges Input property changes
ngOnInit After first ngOnChanges
ngDoCheck Custom change detection
ngAfterContentInit After content projection
ngAfterContentChecked After content check
ngAfterViewInit After view init
ngAfterViewChecked After view check
ngOnDestroy Before destroy

Implementation

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

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

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

Implement interface for type safety

Services & Dependency Injection

Injectable Service

import { Injectable } from "@angular/core";
import { Http } from "@angular/http";
import { Observable } from "rxjs/Observable";
import "rxjs/add/operator/map";

@Injectable()
export class HeroService {
  constructor(private http: Http) {}

  getHeroes(): Observable<Hero[]> {
    return this.http.get("api/heroes").map((res) => res.json());
  }
}

@Injectable() required for all services

Provider Registration

// Module-wide
@NgModule({
  providers: [HeroService],
})
export class AppModule {}

// Component-level
@Component({
  providers: [HeroService],
})
export class HeroListComponent {}

Register in module or component

Injection

export class HeroListComponent {
  heroes: Hero[];

  constructor(private heroService: HeroService) {}

  ngOnInit() {
    this.heroService.getHeroes().subscribe((heroes) => (this.heroes = heroes));
  }
}

Inject via constructor

HTTP

GET Request

import { Http } from '@angular/http';
import 'rxjs/add/operator/map';

constructor(private http: Http) { }

getData() {
  return this.http.get('/api/data')
    .map(res => res.json());
}

Returns Observable

POST Request

import { Http, Headers, RequestOptions } from '@angular/http';

postData(data: any) {
  let headers = new Headers({ 'Content-Type': 'application/json' });
  let options = new RequestOptions({ headers });

  return this.http.post('/api/data', JSON.stringify(data), options)
    .map(res => res.json());
}

Set headers and options

Error Handling

import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';

getData() {
  return this.http.get('/api/data')
    .map(res => res.json())
    .catch(this.handleError);
}

private handleError(error: any) {
  console.error('Error:', error);
  return Observable.throw(error.json().error || 'Server error');
}

Use catch operator

Routing

Route Configuration

import { RouterModule, Routes } from "@angular/router";

const routes: Routes = [
  { path: "heroes", component: HeroListComponent },
  { path: "hero/:id", component: HeroDetailComponent },
  { path: "", redirectTo: "/heroes", pathMatch: "full" },
  { path: "**", component: PageNotFoundComponent },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}

Define application routes

Router Outlet

<nav>
  <a [routerLink]="['/heroes']" routerLinkActive="active">Heroes</a>
  <a [routerLink]="['/crisis-center']">Crisis</a>
</nav>
<router-outlet></router-outlet>

Display routed components

Programmatic Navigation

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

constructor(private router: Router) { }

gotoHero(hero: Hero) {
  this.router.navigate(['/hero', hero.id]);
}

Navigate in component code

Route Parameters

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

constructor(private route: ActivatedRoute) { }

ngOnInit() {
  this.route.params.subscribe(params => {
    let id = +params['id'];
    this.getHero(id);
  });
}

Access route parameters

Forms

Template-Driven Form

<form (ngSubmit)="onSubmit()" #heroForm="ngForm">
  <input [(ngModel)]="model.name" name="name" required #name="ngModel" />
  <div [hidden]="name.valid || name.pristine">Name is required</div>
  <button type="submit" [disabled]="!heroForm.form.valid">Submit</button>
</form>

name attribute required with ngModel

Reactive Form

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

export class HeroFormComponent {
  heroForm: FormGroup;

  constructor(private fb: FormBuilder) {
    this.heroForm = this.fb.group({
      name: ["", Validators.required],
      power: ["", Validators.required],
    });
  }

  onSubmit() {
    console.log(this.heroForm.value);
  }
}

More control and testability

Reactive Form Template

<form [formGroup]="heroForm" (ngSubmit)="onSubmit()">
  <input formControlName="name" />
  <div *ngIf="heroForm.get('name').invalid && heroForm.get('name').touched">
    Name is required
  </div>
  <button [disabled]="heroForm.invalid">Submit</button>
</form>

Binds to FormGroup

Validation States

State Meaning
valid All validations pass
invalid At least one fails
pristine User hasn't changed value
dirty User changed value
touched User blurred field
untouched User hasn't blurred

Pipes

Built-in Pipes

<p>{{ birthday | date }}</p>
<p>{{ birthday | date:'MM/dd/yyyy' }}</p>
<p>{{ price | currency:'USD':true }}</p>
<p>{{ ratio | percent:'2.1-2' }}</p>
<p>{{ message | uppercase }}</p>
<p>{{ message | lowercase }}</p>
<p>{{ object | json }}</p>
<p>{{ promise | async }}</p>
<p>{{ collection | slice:1:3 }}</p>

Transform displayed values

Custom Pipe

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

@Pipe({ name: "exponentialStrength" })
export class ExponentialStrengthPipe implements PipeTransform {
  transform(value: number, exponent: string): number {
    let exp = parseFloat(exponent);
    return Math.pow(value, isNaN(exp) ? 1 : exp);
  }
}

Implement PipeTransform interface

Using Custom Pipes

<p>{{ 2 | exponentialStrength:10 }}</p>
@NgModule({
  declarations: [ ExponentialStrengthPipe ]
})

Register in module declarations

AngularJS Migration

Syntax Comparison

AngularJS Angular 2
Controllers @Component
$scope Class properties
ng-app bootstrapModule()
ng-repeat *ngFor
ng-if *ngIf
ng-show/ng-hide [hidden]
ng-class [ngClass]
ng-model [(ngModel)]
Filters Pipes
angular.module() @NgModule

Module Definition

// AngularJS
angular.module("myApp", []).controller("MyCtrl", function ($scope) {
  $scope.title = "Hello";
});

// Angular 2
@Component({
  selector: "my-app",
  template: "<h1>{{title}}</h1>",
})
export class AppComponent {
  title = "Hello";
}

Components replace controllers

Gotchas

Zone.js

Zone.js runs change detection automatically after async operations (setTimeout, promises, HTTP). For manual control, use ChangeDetectorRef.

RxJS Operators

Must import operators explicitly in Angular 2:

import "rxjs/add/operator/map";
import "rxjs/add/operator/catch";
import "rxjs/add/observable/throw";

Observable Subscriptions

Always unsubscribe from Observables in ngOnDestroy to prevent memory leaks, or use the async pipe:

// Manual subscription
this.subscription = this.service.getData().subscribe(data => {});

ngOnDestroy() {
  this.subscription.unsubscribe();
}

// Or use async pipe
// template: <div>{{ data$ | async }}</div>

Module Types

Angular modules (@NgModule) are different from ES6 modules (import/export). Don't confuse them.

Forms and ngModel

The name attribute is REQUIRED when using [(ngModel)] in template-driven forms:

<!-- Required -->
<input [(ngModel)]="hero.name" name="name" />

Injectable Decorator

@Injectable() is needed for ALL services, even those without dependencies, for future-proofing.

Structural Directives

Always use * prefix with structural directives:

<div *ngIf="condition"></div>
<div *ngFor="let item of items"></div>

Also see