Skip to content

Instantly share code, notes, and snippets.

@ivankisyov
Last active October 8, 2023 09:03
Show Gist options
  • Save ivankisyov/7ebbf82305152fb0f64c0087df5a0b9b to your computer and use it in GitHub Desktop.
Save ivankisyov/7ebbf82305152fb0f64c0087df5a0b9b to your computer and use it in GitHub Desktop.
Learn Angular

Learn Angular

import { FormsModule } from '@angular/forms';
...
// FormsModule  to the imports[]  array in your app.module.ts


Two way binding directive

<input type="text" placeholder="name" [(ngModel)]="name" />

Interfaces

interfaces are like contracts you need to fulfill the contract/implement the interface

interface User {
    username: string;
    password: string;
    confirmPassword?: string; // ? === optional prop
}

// Interfaces for functions

interface CanDrive {
    accelerate(speed: number): void;
}

// whichever item implements the CanDrive interface
// must have accelerate fn!

const myCar:CanDrive = {
    accelerate: function(speed: number) {
        ...
    }
}

Generics

Types which can hold/use several types

let numberArray: Array<number>;

Using bootstrap

npm i --save bootstrap
// angular.json

....

"styles": [
    "node_modules/bootstrap/dist/css/bootstrap.min.css", // pretty sure that there is a cleaner way to do this
    "src/styles.css"
],
....




Creating component using the cli

ng g c <name of component>

DataBinding === Communication

  • String interpolation
{{getStatus()}}
  • Property binding
<button [disabled]="!allowNewServer" type="button" class="btn   btn-primary">
    Add New Server
  </button>
export class ServerComponent {
  private serverId: number = 11;
  private serverStatus: string = 'online';

  public allowNewServer: boolean = false;

  constructor() {
    setTimeout(() => (this.allowNewServer = true), 2000);
  }

  getServerId(): number {
    return this.serverId;
  }
  getServerStatus(): string {
    return this.serverStatus;
  }
}
// another example

<p [innerText]="allowNewServer"></p>

  • Event binding
<button
    (click)="onAddNewServer()"
    [disabled]="!allowNewServer"
    type="button"
    class="btn   btn-primary"
  >
    Add New Server
  </button>

...

export class ServerComponent {
  private serverId: number = 11;
  private serverStatus: string = 'online';

  public allowNewServer: boolean = false;
  public allowNewServerStatus: string = 'not allowed to add new servers';

  constructor() {
    setTimeout(() => (this.allowNewServer = true), 2000);
  }

  getServerId(): number {
    return this.serverId;
  }
  getServerStatus(): string {
    return this.serverStatus;
  }
  onAddNewServer(): void {
    this.allowNewServerStatus = 'you can add new servers now';
  }
}

Access to event data

 <input
    type="text"
    name="serverName"
    id="serverName"
    class="form-control"
    value=""
    required="required"
    pattern=""
    title=""
    (input)="onUpdateServerName($event)"
/>

...

onUpdateServerName(event: Event) {
    this.serverName = (<HTMLInputElement>event.target).value;
  }

  // explicit typeCasting for TS

  • Two-Way-Binding

you need to enable ngModel directive you need to add the FormsModule to the imports[] array in the AppModule

<input
    type="text"
    name="serverName"
    id="serverName"
    class="form-control"
    [(ngModel)]="serverName"
/>

....

// class member
public serverName: string = 'Test Server';


https://www.npmjs.com/package/esm


Directives

Instructions in the DOM

Structural Directives

<div *ngIf="isUsernameSet()">
<div *ngIf="isUsernameSet(); else noUser">
  <strong>User: {{ userName }}</strong>
  <hr />
  <button
    (click)="onResetUsername()"
    [disabled]="!isUsernameSet()"
    type="button"
    class="btn btn-primary"
  >
    Reset name
  </button>
</div>
<ng-template #noUser>
  <p>No user!</p>
</ng-template>

Attribute Directives

<strong [ngStyle]="{ backgroundColor: checkIfUserMatches() }"
<strong
    [ngClass]="{ match: userName === 'ivan' }"  // match here is the css class
    [ngStyle]="{ backgroundColor: checkIfUserMatches() }"
    >User: {{ userName }}</strong
  >
<app-server *ngFor="let server of servers"></app-server>

...

export class ServersComponent implements OnInit {
  servers: Array<string> = ['s1', 's2', 's3'];

  constructor() {}

  ngOnInit() {}
}

Geting the index - *ngFor

<p [ngClass]="{ aboveFour: i >= 4 }" *ngFor="let item of clicks; let i = index">
  logged at: {{ item }}
</p>

model === data


Create a recipe model

// recipe.model.ts

export class Recipe {
  public name: string;
  public description: string;
  public imagePath: string;

  constructor(name: string, desc: string, imagePath: string) {
    this.name = name;
    this.description = desc;
    this.imagePath = imagePath;
  }
}
// use the model

import { Component, OnInit } from '@angular/core';
import { Recipe } from '../recipe.model';

@Component({
  selector: 'app-recipe-list',
  templateUrl: './recipe-list.component.html',
  styleUrls: ['./recipe-list.component.css']
})
export class RecipeListComponent implements OnInit {
  recipes: Recipe[] = [
    new Recipe(
      'A test',
      'This is a simple test',
      'https://www.maxpixel.net/static/photo/1x/Burgers-Burger-Chickpeas-Recipes-Food-Vegetables-2920072.jpg'
    )
  ];

  constructor() {}

  ngOnInit() {}
}

// use the recipes
<a *ngFor="let recipe of recipes" href="#" class="list-group-item clearfix">
      <div class="pull-left">
        <h4 class="list-group-item-heading">{{ recipe.name }}</h4>
        <p class="list-group-item-text">{{ recipe.description }}</p>
      </div>
      <span class="pull-right">
        <img
          src="{{ recipe.imagePath }}"
          alt=""
          class="img-responsive"
          style="max-height: 50px;"
        />
      </span>
    </a>



Create the ingredient model using TS accessors

export class Ingredient {
  constructor(public name: string, public amount: number) {}
}
// TS will set the "this props accordingly"

create and nest a component

ng g c recipes/recipe-list
// recipe-list will be nested inside recipes

Debugging

F12 -> Sources -> webpack -> . -> ....

https://augury.rangle.io/


Data binding - custom props binding

import { Component, OnInit, Input } from '@angular/core';

@Component({
  selector: 'app-server-element',
  templateUrl: './server-element.component.html',
  styleUrls: ['./server-element.component.css']
})
export class ServerElementComponent implements OnInit {
  // expose this prop to parent components
  @Input() element: { type: string; name: string; content: string };

  constructor() {}

  ngOnInit() {}
}

// app.component.html

<div class="container">
  <app-cockpit></app-cockpit>
  <hr />
  <div class="row">
    <div class="col-xs-12">
      <app-server-element
        *ngFor="let serverElement of serverElements"
        [element]="serverElement"
      ></app-server-element>
    </div>
  </div>
</div>


Using an alias for component's class member

...
export class ServerElementComponent implements OnInit {
  // expose this prop to parent components
  @Input('srvElement') element: { type: string; name: string; content: string };

  constructor() {}

  ngOnInit() {}
}
 <app-server-element
        *ngFor="let serverElement of serverElements"
        [srvElement]="serverElement"
      ></app-server-element>

Data binding - custom events binding

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

@Component({
  selector: "app-cockpit",
  templateUrl: "./cockpit.component.html",
  styleUrls: ["./cockpit.component.css"]
})
export class CockpitComponent implements OnInit {
  // events that can be emitted from this component
  // @Output() - parent components can listen for
  // those custom events
  @Output() serverCreated = new EventEmitter<{
    serverName: string;
    serverContent: string;
  }>();
  // <{ serverName: string; serverContent: string }>
  // generic type indicating what data will be emitted
  @Output() blueprintCreated = new EventEmitter<{
    serverName: string;
    serverContent: string;
  }>();

  newServerName = "";
  newServerContent = "";

  constructor() {}

  ngOnInit() {}

  onAddServer() {
    this.serverCreated.emit({
      serverName: this.newServerName,
      serverContent: this.newServerContent
    });
  }

  onAddBlueprint() {
    this.blueprintCreated.emit({
      serverName: this.newServerName,
      serverContent: this.newServerContent
    });
  }
}

// cockpit html
...
<button class="btn btn-primary" (click)="onAddServer()">Add Server</button>
    <button class="btn btn-primary" (click)="onAddBlueprint()">
// parent component html
<app-cockpit
    (serverCreated)="onServerAdded($event)"
    (blueprintCreated)="onBlueprintAdded($event)"
  ></app-cockpit>
// parent component
export class AppComponent {
  serverElements = [
    { type: 'server', name: 'testServer', content: 'test content' }
  ];

  onServerAdded(serverData: { serverName: string; serverContent: string }) {
    this.serverElements.push({
      type: 'server',
      name: serverData.serverName,
      content: serverData.serverContent
    });
  }

  onBlueprintAdded(blueprintData: {
    serverName: string;
    serverContent: string;
  }) {
    this.serverElements.push({
      type: 'blueprint',
      name: blueprintData.serverName,
      content: blueprintData.serverContent
    });
  }
}

Using alias for custom event names

@Output("bpCreated") blueprintCreated = new EventEmitter<{
    serverName: string;
    serverContent: string;
  }>();

Summary - data binding/communication using custom props and events

@Input()

expose component's prop to parent components

@Output

component emits custom events so parent components can listen to those events

20.03

Local references in templates

...
<input type="text" class="form-control" #serverNameInput />
// serverNameInput holds the whole html element

// in order to use its value
serverNameInput.value


Local references used in a component

// template
<input type="text" class="form-control" #serverNameInput />
// component
import { Component, OnInit, EventEmitter, Output, ViewChild, ElementRef } from "@angular/core";

...
@ViewChild('serverNameInput') serverNameInput: ElementRef;

....
// accessing its value
this.serverNameInput.nativeElement.value;


Projecting content into components - ng-content

<app-server-element
        *ngFor="let serverElement of serverElements"
        [srvElement]="serverElement"
      >
      <p>This will be projected!</p>
      </app-server-element>
<div class="panel panel-default">
  <div class="panel-heading">{{ element.name }}</div>
  <div class="panel-body">
     <ng-content></ng-content>
  </div>
</div>

Lifecycle hooks

  • constructor
  • ngOnChanges
// make sure that OnChanges(or any other LF hook interfaces) is imported
// and the class is implementing is
// it could work without those steps, though

import { Component, OnInit, Input, OnChanges } from '@angular/core';

@Component({
  selector: 'app-server-element',
  templateUrl: './server-element.component.html',
  styleUrls: ['./server-element.component.css']
})
export class ServerElementComponent implements OnInit, OnChanges {
  // expose this prop to parent components
  @Input('srvElement') element: { type: string; name: string; content: string };

  constructor() {
    console.log('constructor called');
  }

  ngOnChanges(changes: SimpleChanges): void {
    //Called before any other lifecycle hook. Use it to inject dependencies, but avoid any serious work here.
    //Add '${implements OnChanges}' to the class.
    console.log('ngOnChanges called');
    console.log(changes);
  }

  ngOnInit() {
    console.log('ngOnInit called');
  }
}
  • ngDoCheck // called many times
  • ngAfterContentInit
  • ngAfterContentChecked
  • ngAfterViewInit
  • ngAfterViewChecked
  • ngOnDestroy

@ContentChild decorator

// used inside component
// to get the element which is passed
// to <ng-content></ng-content>

// make sure to import
// ContentChild!
// and then use the decorator

@ContentChild('someRef') someRefName: ElementRef;


Creating custom attribute directive

simple example

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

@Directive({
  selector: '[appBasicHighlight]'
})
export class BasicHighlightDirective implements OnInit {
  constructor(private elementRef: ElementRef) {}
  ngOnInit() {
    this.elementRef.nativeElement.style.backgroundColor = 'green';
  }
}

you need to inform Angular for it

// app.module.ts

@NgModule({
  declarations: [
      BasicHighlightDirective

how to use it

<p appBasicHighlight>Testing directives</p>

better example

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

@Directive({
  selector: '[appBetterHighlight]'
})
export class BetterHighlightDirective implements OnInit {
  constructor(private elRef: ElementRef, private renderer: Renderer2) {}

  ngOnInit() {
    this.renderer.setStyle(
      this.elRef.nativeElement,
      'background-color',
      'blue'
    );
  }
}

another enhancement

export class BetterHighlightDirective implements OnInit {
  constructor(private elRef: ElementRef, private renderer: Renderer2) {}

  ngOnInit() {
    // this.renderer.setStyle(
    //   this.elRef.nativeElement,
    //   'background-color',
    //   'blue'
    // );
  }

  @HostListener('mouseenter') mouseover(eventData: Event) {
    this.renderer.setStyle(
      this.elRef.nativeElement,
      'background-color',
      'blue'
    );
  }

  @HostListener('mouseleave') mouseleave(eventData: Event) {
    this.renderer.setStyle(
      this.elRef.nativeElement,
      'background-color',
      'transparent'
    );
  }
}

yet another enhancement

export class BetterHighlightDirective implements OnInit {
  @Input() defaultColor: string = 'transparent';
  @Input() highlightColor: string = 'blue';

  @HostBinding('style.backgroundColor') backgroundColor: string = this
    .defaultColor;

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

  ngOnInit() {
    this.backgroundColor = this.defaultColor;
  }

  @HostListener('mouseenter') mouseover(eventData: Event) {
    this.backgroundColor = this.highlightColor;
  }

  @HostListener('mouseleave') mouseleave(eventData: Event) {
    this.backgroundColor = this.defaultColor;
  }
}

how to use it

 <p appBetterHighlight [defaultColor]="'pink'" [highlightColor]="'lime'">
        Testing directives
      </p>

Creating custom structural directive

import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[appUnless]'
})
export class UnlessDirective {
  @Input() set appUnless(condition: boolean) {
    if (!condition) {
      this.vcRef.createEmbeddedView(this.templateRef);
    } else {
      this.vcRef.clear();
    }
  }

  constructor(
    private templateRef: TemplateRef<any>,
    private vcRef: ViewContainerRef
  ) {}
}

how to use

<p *appUnless="'mike' === 'smith'">
        default user
      </p>

ngSwitch

<div [ngSwitch]="valueToCheck">
        <p *ngSwitchCase="true">
          The value is true
        </p>
        <p *ngSwitchCase="false">
          The value is false
        </p>
        <p *ngSwitchDefault>
          The value is default
        </p>
      </div>
export class AppComponent {
  loadedFeature: string = 'recipe';
  valueToCheck: boolean;

  onFeatureSelected(feature: string) {
    this.loadedFeature = feature;
  }
}
// it will default in this case

Toggle class on an element using custom directive

import { Directive, HostListener, HostBinding } from '@angular/core';

@Directive({
  selector: '[appDropdown]'
})
export class DropdownDirective {
  @HostBinding('class.open') isOpen = false;

  @HostListener('click') toggleOpen() {
    this.isOpen = !this.isOpen;
  }
}


Service

  • It's a class which acts as a centralized business unit
  • It eases the communication between components as well

Creation of a logging service

// logging.service.ts

export class LoggingService {
  logStatusChange(status: string) {
    console.log(`Status changed, new status: ${status}`);
  }
}


how to use it in a component:

import { Component, EventEmitter, Output } from "@angular/core";
import { LoggingService } from "../shared/services/logging.service"; // !

@Component({
  selector: "app-new-account",
  templateUrl: "./new-account.component.html",
  styleUrls: ["./new-account.component.css"],
  providers: [LoggingService] // !
})
export class NewAccountComponent {
  @Output() accountAdded = new EventEmitter<{ name: string; status: string }>();

  constructor(private loggingService: LoggingService) {} // !

  onCreateAccount(accountName: string, accountStatus: string) {
    this.accountAdded.emit({
      name: accountName,
      status: accountStatus
    });
    this.loggingService.logStatusChange(accountStatus);
  }
}


Creating the AccountsService

export class AccountsService {
  accounts = [
    {
      name: 'Master Account',
      status: 'active'
    },
    {
      name: 'Testaccount',
      status: 'inactive'
    },
    {
      name: 'Hidden Account',
      status: 'unknown'
    }
  ];

  addAccount(name: string, status: string) {
    this.accounts.push({ name, status });
  }

  updateStatus(id: number, status: string) {
    this.accounts[id].status = status;
  }
}

How to use the services properly - the injector

It depends where this service is provided:

  • AppModule: the same instance of the service is available in the whole app
    • an alternative for when using Angular 6+:
    @Injectable({providedIn: 'root'})
    export class MyService { ... }
    
  • AppComponent: the same instance of the service in all components below the AppComponent(not for the other Services)
  • other components: the same instance is available for the component and all its child components

You can overwrite the service instance depending on where it is provided! If you need to have one instance you need to provided on the highest level possible. On ther other hand you can have as many instances based on your intent.

Single instance scenario:

  • Do not include the service in the providers array, only in the constructor!

Injecting services into services

Scenario:

Use the loggingService in the accountsService

// app.module.ts
.......
@NgModule({
  declarations: [AppComponent, AccountComponent, NewAccountComponent],
  imports: [BrowserModule, FormsModule],
  providers: [AccountsService, LoggingService],
  bootstrap: [AppComponent]
})
.....

now you need to inject the service into the other service

// service in which you are injecting another service

import { LoggingService } from './services/logging.service';
import { Injectable } from '@angular/core';

@Injectable()
export class AccountsService {
  accounts = [
    {
      name: 'Master Account',
      status: 'active'
    },
    {
      name: 'Testaccount',
      status: 'inactive'
    },
    {
      name: 'Hidden Account',
      status: 'unknown'
    }
  ];

  constructor(private loggingService: LoggingService) {}

  addAccount(name: string, status: string) {
    this.accounts.push({ name, status });
    this.loggingService.logStatusChange(status);
  }

  updateStatus(id: number, status: string) {
    this.accounts[id].status = status;
    this.loggingService.logStatusChange(status);
  }
}



Emmitting events from a service and subscribe

export class AccountsService {
  accounts = [
    {
      name: 'Master Account',
      status: 'active'
    },
    {
      name: 'Testaccount',
      status: 'inactive'
    },
    {
      name: 'Hidden Account',
      status: 'unknown'
    }
  ];
  statusUpdated = new EventEmitter<string>(); // !

  constructor(private loggingService: LoggingService) {}

  addAccount(name: string, status: string) {
    this.accounts.push({ name, status });
    this.loggingService.logStatusChange(status);
  }

  updateStatus(id: number, status: string) {
    this.accounts[id].status = status;
    this.loggingService.logStatusChange(status);
  }
}
export class AccountComponent {
  @Input() account: { name: string; status: string };
  @Input() id: number;

  constructor(private accoutsService: AccountsService) {}

  onSetTo(status: string) {
    this.accoutsService.updateStatus(this.id, status);
    this.accoutsService.statusUpdated.emit(status); // !
  }
}
export class NewAccountComponent {
  constructor(private accountsService: AccountsService) {
    this.accountsService.statusUpdated.subscribe((status: string) =>
      alert('new status: ' + status) // !
    );
  }

  onCreateAccount(accountName: string, accountStatus: string) {
    this.accountsService.addAccount(accountName, accountStatus);
  }
}

Emmitting events from a service and subscribe - An Example

// create the emitter, also emit custom events
export class ShoppingListService {
  ingredientsChanged = new EventEmitter<Ingredient[]>();

  private ingredients: Ingredient[] = [
    new Ingredient('Apples', 11),
    new Ingredient('Tomatoes', 4)
  ];

  getIngredients() {
    return [...this.ingredients];
  }

  addIngredient(ingredient: Ingredient) {
    this.ingredients.push(ingredient);
    this.ingredientsChanged.emit([...this.ingredients]);
  }

  addIngredients(ingredients: Ingredient[]) {
    this.ingredients.push(...ingredients);
    this.ingredientsChanged.emit([...this.ingredients]);
  }
}
// subscribe
export class ShoppingListComponent implements OnInit {
  ingredients: Ingredient[] = [];

  constructor(private shoppingListService: ShoppingListService) {}

  ngOnInit() {
    this.ingredients = this.shoppingListService.getIngredients();
    this.shoppingListService.ingredientsChanged.subscribe(
      (ingredients: Ingredient[]) => {
        this.ingredients = ingredients;
      }
    );
  }
}

Routing

// app.module.ts

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

import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { UsersComponent } from './users/users.component';
import { ServersComponent } from './servers/servers.component';
import { UserComponent } from './users/user/user.component';
import { EditServerComponent } from './servers/edit-server/edit-server.component';
import { ServerComponent } from './servers/server/server.component';
import { ServersService } from './servers/servers.service';
import { Routes, RouterModule } from '@angular/router'; // !

const appRoutes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'users', component: UserComponent },
  { path: 'servers', component: ServerComponent }
]; // !

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    UsersComponent,
    ServersComponent,
    UserComponent,
    EditServerComponent,
    ServerComponent
  ],
  imports: [BrowserModule, FormsModule, RouterModule.forRoot(appRoutes)], // !
  providers: [ServersService],
  bootstrap: [AppComponent]
})
export class AppModule {}

// app.component.html

<div class="container">
  <div class="row">
    <div class="col-xs-12 col-sm-10 col-md-8 col-sm-offset-1 col-md-offset-2">
      <ul class="nav nav-tabs">
        <li role="presentation" class="active"><a href="#">Home</a></li>
        <li role="presentation"><a href="#">Servers</a></li>
        <li role="presentation"><a href="#">Users</a></li>
      </ul>
    </div>
  </div>
  <div class="row">
    <div class="col-xs-12 col-sm-10 col-md-8 col-sm-offset-1 col-md-offset-2">
      <router-outlet></router-outlet> // !
    </div>
  </div>
</div>



Navigation with router links

you can pass string or an array

<ul class="nav nav-tabs">
  <li role="presentation" class="active"><a routerLink="/">Home</a></li>
  <li role="presentation"><a routerLink="/servers">Servers</a></li>
  <li role="presentation"><a [routerLink]="['/users']">Users</a></li>
</ul>

Adding active class on current page

<ul class="nav nav-tabs">
  <li
    role="presentation"
    routerLinkActive="active"
    [routerLinkActiveOptions]="{ exact: true }"
  >
    <a routerLink="/">Home</a>
  </li>
  <li role="presentation" routerLinkActive="active">
    <a routerLink="/servers">Servers</a>
  </li>
  <li role="presentation" routerLinkActive="active">
    <a [routerLink]="['/users']">Users</a>
  </li>
</ul>

Navigate programmatically

<button type="button" (click)="onLoadServers()" class="btn btn-primary">
  Load Servers
</button>
onLoadServers() {
  // some calculations
  this.router.navigate(['/servers']);
}

Navigate programmatically - Using relative paths

import { Component, OnInit } from '@angular/core';
import { ServersService } from './servers.service';
import { Router, ActivatedRoute } from '@angular/router'; // !

@Component({
  selector: 'app-servers',
  templateUrl: './servers.component.html',
  styleUrls: ['./servers.component.css']
})
export class ServersComponent implements OnInit {
  private servers: { id: number; name: string; status: string }[] = [];

  constructor(
    private serversService: ServersService,
    private router: Router, // !
    private route: ActivatedRoute // !
  ) {}

  ngOnInit() {
    this.servers = this.serversService.getServers();
  }

  onReload() {
    // !
    this.router.navigate(['servers'], { relativeTo: this.route });
    // it will try to navigate to /servers/servers
  }
}

Passing params to routes

// app.module.ts

const appRoutes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'users', component: UsersComponent },
  { path: 'users/:id', component: UserComponent },
  { path: 'servers', component: ServersComponent }
];

Fetching route params

const appRoutes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'users', component: UsersComponent },
  { path: 'users/:id/:name', component: UserComponent },
  { path: 'servers', component: ServersComponent }
];
<button
  type="button"
  [routerLink]="['/users', 10, 'Anna']"
  class="btn btn-primary"
>
  Load Anna
</button>
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';

@Component({
  selector: 'app-user',
  templateUrl: './user.component.html',
  styleUrls: ['./user.component.css']
})
export class UserComponent implements OnInit {
  user: { id: number; name: string };

  constructor(private route: ActivatedRoute) {}

  ngOnInit() {
    this.user = {
      id: this.route.snapshot.params['id'],
      name: this.route.snapshot.params['name']
    };
    // observable
    // this is useful if
    // we are on the component itself and
    // try to use the new params
    this.route.params.subscribe((params: Params) => {
      this.user.id = params['id'];
      this.user.name = params['name'];
    });
  }
}

Passing query params and fragments

const appRoutes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'users', component: UsersComponent },
  { path: 'users/:id/:name', component: UserComponent },
  { path: 'servers', component: ServersComponent },
  { path: 'servers/:id/edit', component: EditServerComponent }
];
// /servers/3/edit?allowEdit=1#loading

<a
  [routerLink]="['/servers', 3, 'edit']"
  [queryParams]="{ allowEdit: 1 }"
  fragment="loading"
  class="list-group-item"
  *ngFor="let server of servers"
>
  {{ server.name }}
</a>

the same idea when inside a component

<button type="button" (click)="onLoadServer(1)" class="btn btn-primary">
  Load Server 1
</button>
export class HomeComponent implements OnInit {
  constructor(private router: Router) {}

  ngOnInit() {}

  onLoadServer(id: number) {
    // some calculations
    this.router.navigate(['/servers', id, 'edit'], {
      queryParams: { allowEdit: 1 },
      fragment: 'loading'
    });
  }
}

Retrieving query params and fragments

export class EditServerComponent implements OnInit {
  server: { id: number; name: string; status: string };
  serverName = '';
  serverStatus = '';

  constructor(
    private serversService: ServersService,
    private route: ActivatedRoute
  ) {}

  ngOnInit() {
    console.log(this.route.snapshot.queryParams);
    console.log(this.route.snapshot.fragment);
    // in order to get the updated values
    // if on the same page
    // you need to use the reactive way
    this.route.queryParams.subscribe(params => console.log(params));
    this.route.fragment.subscribe(params => console.log(params));

    this.server = this.serversService.getServer(1);
    this.serverName = this.server.name;
    this.serverStatus = this.server.status;
  }

  onUpdateServer() {
    this.serversService.updateServer(this.server.id, {
      name: this.serverName,
      status: this.serverStatus
    });
  }
}

setup - nested routes

// before

const appRoutes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'users', component: UsersComponent },
  { path: 'users/:id/:name', component: UserComponent },
  { path: 'servers', component: ServersComponent },
  { path: 'servers/:id', component: EditServerComponent },
  { path: 'servers/:id/edit', component: EditServerComponent }
];
// after

const appRoutes: Routes = [
  { path: "", component: HomeComponent },
  {
    path: "users",
    component: UsersComponent,
    children: [{ path: ":id/:name", component: UserComponent }]
  },
  {
    path: "servers",
    component: ServersComponent,
    children: [
      { path: ":id", component: ServerComponent },
      { path: ":id/edit", component: EditServerComponent }
    ]
  }
];

you need to insert

<router-outlet></router-outlet>

like here:

<div class="row">
  <div class="col-xs-12 col-sm-4">
    <div class="list-group">
      <a
        [routerLink]="['/users', user.id, user.name]"
        class="list-group-item"
        *ngFor="let user of users"
      >
        {{ user.name }}
      </a>
    </div>
  </div>
  <div class="col-xs-12 col-sm-4">
    <!-- <app-user></app-user> -->
    <router-outlet></router-outlet>
  </div>
</div>

Remember to subscribe if you are on the same page and want to retrive the updated params!

 ngOnInit() {
    const serverId = +this.route.snapshot.params["id"];
    this.server = this.serversService.getServer(serverId);
    // !
    this.route.params.subscribe(params => {
      this.server = this.serversService.getServer(+params["id"]);
    });
  }

Navigate to relative path

scenario:

you are on a certain page /servers/:id and want to navigate to /servers/:id/edit

export class ServerComponent implements OnInit {
  server: { id: number; name: string; status: string };

  constructor(
    private serversService: ServersService,
    private route: ActivatedRoute,
    private router: Router
  ) {}

  ngOnInit() {
    const serverId = +this.route.snapshot.params["id"];
    this.server = this.serversService.getServer(serverId);
    this.route.params.subscribe(params => {
      this.server = this.serversService.getServer(+params["id"]);
    });
  }

  // !
  onEditServer() {
    this.router.navigate(["edit"], { relativeTo: this.route });
  }
}

Navigate to relative path and keep the query params

export class ServerComponent implements OnInit {
  server: { id: number; name: string; status: string };

  constructor(
    private serversService: ServersService,
    private route: ActivatedRoute,
    private router: Router
  ) {}

  ngOnInit() {
    const serverId = +this.route.snapshot.params["id"];
    this.server = this.serversService.getServer(serverId);
    this.route.params.subscribe(params => {
      this.server = this.serversService.getServer(+params["id"]);
    });
  }

  onEditServer() {
    this.router.navigate(["edit"], {
      relativeTo: this.route,
      queryParamsHandling: "preserve"
    });
  }
}

Redirect when user navigates to unknown page

const appRoutes: Routes = [
  { path: "", component: HomeComponent },
  {
    path: "users",
    component: UsersComponent,
    children: [{ path: ":id/:name", component: UserComponent }]
  },
  {
    path: "servers",
    component: ServersComponent,
    children: [
      { path: ":id", component: ServerComponent },
      { path: ":id/edit", component: EditServerComponent }
    ]
  },
  { path: "not-found", component: PageNotFoundComponent },
  { path: "**", redirectTo: "/not-found" }
];

Redirection Path Matching

you only get redirected, if the full path is '' (so only if you got NO other content in your path in this example)

{ path: '', redirectTo: '/somewhere-else', pathMatch: 'full' }

Creating new module to hold the routing logic

// app-routing.module.ts

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

import { HomeComponent } from "./home/home.component";
import { UsersComponent } from "./users/users.component";
import { UserComponent } from "./users/user/user.component";
import { ServersComponent } from "./servers/servers.component";
import { ServerComponent } from "./servers/server/server.component";
import { EditServerComponent } from "./servers/edit-server/edit-server.component";
import { PageNotFoundComponent } from "./page-not-found/page-not-found.component";

const appRoutes: Routes = [
  { path: "", component: HomeComponent },
  {
    path: "users",
    component: UsersComponent,
    children: [{ path: ":id/:name", component: UserComponent }]
  },
  {
    path: "servers",
    component: ServersComponent,
    children: [
      { path: ":id", component: ServerComponent },
      { path: ":id/edit", component: EditServerComponent }
    ]
  },
  { path: "not-found", component: PageNotFoundComponent },
  { path: "**", redirectTo: "/not-found" }
];

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


in order to use it in your main module:

...
import { AppRoutingModule } from "./app-routing.module";

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    UsersComponent,
    ServersComponent,
    UserComponent,
    EditServerComponent,
    ServerComponent,
    PageNotFoundComponent
  ],
  imports: [BrowserModule, FormsModule, AppRoutingModule],
  providers: [ServersService],
  bootstrap: [AppComponent]
})
...

Protecting routes using Guards

// auth-guard.service.ts
import {
  CanActivate,
  ActivatedRouteSnapshot,
  RouterStateSnapshot,
  Router
} from '@angular/router';
import { Observable } from 'rxjs';
import { Injectable } from '@angular/core';

import { AuthService } from './auth.service';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean> | Promise<boolean> | boolean {
    return this.authService.isAuthenticated().then((authenticated: boolean) => {
      if (authenticated) {
        return true;
      } else {
        this.router.navigate(['/']);
      }
    });
  }
}

// auth.service.ts
export class AuthService {
  loggedIn = false;

  isAuthenticated() {
    const promise = new Promise(resolve => {
      setTimeout(() => {
        resolve(this.loggedIn);
      }, 500);
    });
    return promise;
  }

  login() {
    this.loggedIn = true;
  }

  logout() {
    this.loggedIn = false;
  }
}

// app-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { HomeComponent } from './home/home.component';
import { UsersComponent } from './users/users.component';
import { UserComponent } from './users/user/user.component';
import { ServersComponent } from './servers/servers.component';
import { ServerComponent } from './servers/server/server.component';
import { EditServerComponent } from './servers/edit-server/edit-server.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
import { AuthGuard } from './auth-guard.service';

const appRoutes: Routes = [
  { path: '', component: HomeComponent },
  {
    path: 'users',
    component: UsersComponent,
    children: [{ path: ':id/:name', component: UserComponent }]
  },
  {
    path: 'servers',
    component: ServersComponent,
    canActivate: [AuthGuard], // ! it will guard the whole server path with child paths too
    children: [
      { path: ':id', component: ServerComponent },
      { path: ':id/edit', component: EditServerComponent }
    ]
  },
  { path: 'not-found', component: PageNotFoundComponent },
  { path: '**', redirectTo: '/not-found' }
];

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


Protecting child routes

// app-routing.module.ts
...
const appRoutes: Routes = [
  { path: '', component: HomeComponent },
  {
    path: 'users',
    component: UsersComponent,
    children: [{ path: ':id/:name', component: UserComponent }]
  },
  {
    path: 'servers',
    component: ServersComponent,
    canActivateChild: [AuthGuard],
    children: [
      { path: ':id', component: ServerComponent },
      { path: ':id/edit', component: EditServerComponent }
    ]
  },
  { path: 'not-found', component: PageNotFoundComponent },
  { path: '**', redirectTo: '/not-found' }
];
...
// auth-guard.service.ts
export class AuthGuard implements CanActivate, CanActivateChild {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean> | Promise<boolean> | boolean {
    return this.authService.isAuthenticated().then((authenticated: boolean) => {
      if (authenticated) {
        return true;
      } else {
        this.router.navigate(['/']);
      }
    });
  }

  canActivateChild(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean> | Promise<boolean> | boolean {
    return this.canActivate(route, state);
  }
}


Navigate one level up

this.router.navigate(['../'], { relativeTo: this.route });

Creating custom interface

import { Observable } from 'rxjs';

export interface CanComponentDeactivate {
  canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}
// simply put
// the class which implements this interface must
// have a canDeactivate method which returns one of the following:
// - Observable<boolean>
// - Promise<boolean>
// - boolean

Controlling navigation with canDeactivate

// can-deactivate-guard.service.ts

import { Observable } from 'rxjs';
import {
  CanDeactivate,
  ActivatedRouteSnapshot,
  RouterStateSnapshot
} from '@angular/router';

export interface CanComponentDeactivate {
  canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}

export class CanDeactivateGuard
  implements CanDeactivate<CanComponentDeactivate> {
  canDeactivate(
    component: CanComponentDeactivate,
    currentRoute: ActivatedRouteSnapshot,
    currentState: RouterStateSnapshot,
    nextState?: RouterStateSnapshot
  ): Observable<boolean> | Promise<boolean> | boolean {
    return component.canDeactivate();
  }
}


// app-routing.module.ts
const appRoutes: Routes = [
  { path: '', component: HomeComponent },
  {
    path: 'users',
    component: UsersComponent,
    children: [{ path: ':id/:name', component: UserComponent }]
  },
  {
    path: 'servers',
    component: ServersComponent,
    canActivateChild: [AuthGuard],
    children: [
      { path: ':id', component: ServerComponent },
      {
        path: ':id/edit',
        component: EditServerComponent,
        canDeactivate: [CanDeactivateGuard] // !
      }
    ]
  },
  { path: 'not-found', component: PageNotFoundComponent },
  { path: '**', redirectTo: '/not-found' }
];
// app.module.ts

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    UsersComponent,
    ServersComponent,
    UserComponent,
    EditServerComponent,
    ServerComponent,
    PageNotFoundComponent
  ],
  imports: [BrowserModule, FormsModule, AppRoutingModule],
  providers: [ServersService, AuthService, AuthGuard, CanDeactivateGuard], // !
  bootstrap: [AppComponent]
})
// edit-server.component.ts
import { Component, OnInit } from '@angular/core';

import { ServersService } from '../servers.service';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { CanComponentDeactivate } from './can-deactivate-guard.service';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-edit-server',
  templateUrl: './edit-server.component.html',
  styleUrls: ['./edit-server.component.css']
})
export class EditServerComponent implements OnInit, CanComponentDeactivate {
  server: { id: number; name: string; status: string };
  serverName = '';
  serverStatus = '';
  allowEdit = false;
  changesSaved = false;

  constructor(
    private serversService: ServersService,
    private route: ActivatedRoute,
    private router: Router
  ) {}

  ngOnInit() {
    // console.log(this.route.snapshot.queryParams);
    // console.log(this.route.snapshot.fragment);
    // in order to get the updated values
    // if on the same page
    // you need to use the reactive way
    this.route.queryParams.subscribe((params: Params) => {
      this.allowEdit = params['allowEdit'] === '1' ? true : false;
    });
    // this.route.fragment.subscribe(params => console.log(params));

    this.server = this.serversService.getServer(1);
    this.serverName = this.server.name;
    this.serverStatus = this.server.status;
  }

  onUpdateServer() {
    this.serversService.updateServer(this.server.id, {
      name: this.serverName,
      status: this.serverStatus
    });
    this.changesSaved = true;
    this.router.navigate(['../'], { relativeTo: this.route });
  }

  // !
  canDeactivate(): Observable<boolean> | Promise<boolean> | boolean {
    if (!this.allowEdit) {
      return true;
    }
    if (
      (this.serverName !== this.server.name ||
        this.serverStatus !== this.server.status) &&
      !this.changesSaved
    ) {
      return confirm('Do you want to discard the changes?');
    } else {
      return true;
    }
  }
}


Passing static data to a route

// component which will receive the static data
export class ErrorPageComponent implements OnInit {
  errorMessage: string;

  constructor(private route: ActivatedRoute) {}

  ngOnInit() {
    this.errorMessage = this.route.snapshot.data['message'];
    // you can subscribe as well
  }
}
const appRoutes: Routes = [
  { path: '', component: HomeComponent },
  {
    path: 'users',
    component: UsersComponent,
    children: [{ path: ':id/:name', component: UserComponent }]
  },
  {
    path: 'servers',
    component: ServersComponent,
    canActivateChild: [AuthGuard],
    children: [
      { path: ':id', component: ServerComponent },
      {
        path: ':id/edit',
        component: EditServerComponent,
        canDeactivate: [CanDeactivateGuard]
      }
    ]
  },
  // { path: 'not-found', component: PageNotFoundComponent },
  {
    path: 'not-found', // !
    component: ErrorPageComponent,
    data: { message: 'Page not found v2' }
  },
  { path: '**', redirectTo: '/not-found' }
];

Observable

interface Server {
  id: number;
  name: string;
  status: string;
}
resolve(
  route: ActivatedRouteSnapshot,
  state: RouterStateSnapshot
): Observable<Server>;

The Observable must return an object which implements the Server interface


Passing dynamic data to routes

// server-resolver.service.ts
import {
  Resolve,
  ActivatedRouteSnapshot,
  RouterStateSnapshot
} from '@angular/router';
import { Observable } from 'rxjs';
import { ServersService } from '../servers.service';
import { Injectable } from '@angular/core';

// create an interface
interface Server {
  id: number;
  name: string;
  status: string;
}

// it will return data in advance
@Injectable()
export class ServerResolver implements Resolve<Server> {
  constructor(private serverService: ServersService) {}

  resolve(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<Server> | Promise<Server> | Server {
    return this.serverService.getServer(+route.params['id']);
  }
}

// app.module.ts
@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    UsersComponent,
    ServersComponent,
    UserComponent,
    EditServerComponent,
    ServerComponent,
    PageNotFoundComponent,
    ErrorPageComponent
  ],
  imports: [BrowserModule, FormsModule, AppRoutingModule],
  providers: [
    ServersService,
    AuthService,
    AuthGuard,
    CanDeactivateGuard,
    ServerResolver // !
  ],
  bootstrap: [AppComponent]
})
const appRoutes: Routes = [
  { path: '', component: HomeComponent },
  {
    path: 'users',
    component: UsersComponent,
    children: [{ path: ':id/:name', component: UserComponent }]
  },
  {
    path: 'servers',
    component: ServersComponent,
    canActivateChild: [AuthGuard],
    children: [
      {
        path: ':id',
        component: ServerComponent,
        resolve: { server: ServerResolver } // !
      },
      {
        path: ':id/edit',
        component: EditServerComponent,
        canDeactivate: [CanDeactivateGuard]
      }
    ]
  },
  // { path: 'not-found', component: PageNotFoundComponent },
  {
    path: 'not-found',
    component: ErrorPageComponent,
    data: { message: 'Page not found v2' }
  },
  { path: '**', redirectTo: '/not-found' }
];
export class ServerComponent implements OnInit {
  server: { id: number; name: string; status: string };

  constructor(
    private serversService: ServersService,
    private route: ActivatedRoute,
    private router: Router
  ) {}

  ngOnInit() {
    // the resolver way
    this.route.data.subscribe((data: Data) => {
      this.server = data['server']; // !
    });

    // the param way
    // const serverId = +this.route.snapshot.params['id'];
    // this.server = this.serversService.getServer(serverId);
    // this.route.params.subscribe(params => {
    //   this.server = this.serversService.getServer(+params['id']);
    // });
  }

  onEditServer() {
    this.router.navigate(['edit'], {
      relativeTo: this.route,
      queryParamsHandling: 'preserve'
    });
  }
}


Observables

Observables - various data sources:

  • events
  • http requests

In Angular, the Observable is an object imported from rxjs

Observable emits Observers subscribes

The Observable wraps some data and emits data

Creating an observable which emits new ascending number each second

ngOnInit() {
    const myNumbers = Observable.interval(1000);
    myNumbers.subscribe((number: number) => {
      console.log(number);
    });
  }

Creating an observable which sends two packages and errors out

const myObs = Observable.create((observer: Observable<string>) => {
      setTimeout(() => {
        observer.next('first package');
      }, 2000);
      setTimeout(() => {
        observer.next('second package');
      }, 4000);
      setTimeout(() => {
        observer.error('not working');
      }, 5000);
    });
    myObs.subscribe(
      // handle data
      (data: string) => {
        console.log(data);
      },
      // handle error
      (error: string) => {
        console.log(error);
      },
      // handle complete
      // this will not fire at all
      () => {
        console.log('completed');
      }
    );
// same thing may happen if you have complete method and
// then try to next some data

Unsubscribe on ngOnDestroy

export class HomeComponent implements OnInit, OnDestroy {
  numbersObsSubscription: Subscription;

  constructor() {}

  ngOnInit() {
    const myNumbers = Observable.interval(1000);
    this.numbersObsSubscription = myNumbers.subscribe((number: number) => {
      console.log(number);
    });
  }

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

support older imports for rxjs

npm i --save rxjs-compat

Subject

Facilitate the communication between components

// users.service.ts
import { Subject } from 'rxjs/Subject';

export class UsersService {
  userActivated = new Subject();
}

// acts as an observer and observable

// user.component.ts
export class UserComponent implements OnInit {
  id: number;

  constructor(private route: ActivatedRoute, private usersService: UsersService) { }

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

  onActivate() {
    this.usersService.userActivated.next(this.id); // !
  }
}
export class AppComponent implements OnInit {
  user1Activated = false;
  user2Activated = false;

  constructor(private usersService: UsersService) {}

  ngOnInit() { // !
    this.usersService.userActivated.subscribe(
      (id: number) => {
        if (id === 1) {
          this.user1Activated = true;
        } else if (id === 2) {
          this.user2Activated = true;
        }
      }
    );
  }
}
<a routerLink="/">Home</a>
      <a [routerLink]="['user', 1]">User 1 {{ user1Activated ? '(activated)' : '' }}</a>
      <a [routerLink]="['user', 2]">User 2 {{ user2Activated ? '(activated)' : '' }}</a>

Operators

Used for performing additional methods on the data before returning it

const myNumbers = Observable.interval(1000)
      .map( // !
        (data: number) => {
          return data * 2;
        }
      );

Update - Operators

const myNumbers = interval(1000)
      .pipe(map(
        (data: number) => {
          return data * 2;
        }
      ));

Forms

Two approaches:

  • template driven
  • reactive approach

Template driven

<form (ngSubmit)="onSubmit(f)" #f="ngForm">
        <div id="user-data">
          <div class="form-group">
            <label for="username">Username</label>
            <input
              type="text"
              id="username"
              name="username"
              class="form-control"
              ngModel
            />
          </div>
          <button class="btn btn-default" type="button">
            Suggest an Username
          </button>
          <div class="form-group">
            <label for="email">Mail</label>
            <input
              ngModel
              type="email"
              name="email"
              id="email"
              class="form-control"
            />
          </div>
        </div>
        <div class="form-group">
          <label for="secret">Secret Questions</label>
          <select ngModel name="secret" id="secret" class="form-control">
            <option value="pet">Your first Pet?</option>
            <option value="teacher">Your first teacher?</option>
          </select>
        </div>
        <button class="btn btn-primary" type="submit">Submit</button>
      </form>
export class AppComponent {
  suggestUserName() {
    const suggestedName = "Superuser";
  }

  onSubmit(form: NgForm) {
    console.log(form);
  }
}

An alternative way of accessing the form using ViewChild

export class AppComponent {
  @ViewChild("f") signupForm: NgForm;

  suggestUserName() {
    const suggestedName = "Superuser";
  }

  // onSubmit(form: NgForm) {
  //   console.log(form);
  // }

  onSubmit() {
    console.log(this.signupForm);
  }
}
<form (ngSubmit)="onSubmit()" #f="ngForm">

TD: Validation

Built-in Validators & Using HTML5 Validation Which Validators do ship with Angular?

Check out the Validators class: https://angular.io/api/forms/Validators - these are all built-in validators, though that are the methods which actually get executed (and which you later can add when using the reactive approach).

For the template-driven approach, you need the directives. You can find out their names, by searching for "validator" in the official docs: https://angular.io/api?type=directive - everything marked with "D" is a directive and can be added to your template.

Additionally, you might also want to enable HTML5 validation (by default, Angular disables it). You can do so by adding the ngNativeValidate to a control in your template.

<button [disabled]="!f.valid" class="btn btn-primary" type="submit">
          Submit
        </button>
input.ng-invalid.ng-touched {
  border: 1px solid red;
}

<input
              type="email"
              id="email"
              class="form-control"
              ngModel
              name="email"
              required
              email
              #email="ngModel">
            <span class="help-block" *ngIf="!email.valid && email.touched">Please enter a valid email!</span>

TD: default values

export class AppComponent {
  @ViewChild("f") signupForm: NgForm;
  defaultQuestion = "pet";
<select
            id="secret"
            class="form-control"
            [ngModel]="defaultQuestion"
            name="secret"
          >
            <option value="pet">Your first Pet?</option>
            <option value="teacher">Your first teacher?</option>
          </select>

TD: Using ngModel with Two-Way-Binding

<div class="form-group">
          <textarea
            name="questionAnswer"
            rows="3"
            class="form-control"
            [(ngModel)]="answer"></textarea>
        </div>
        <p>Your reply: {{ answer }}</p>
export class AppComponent {
  @ViewChild("f") signupForm: NgForm;
  defaultQuestion = "pet";
  answer = "";

TD: Grouping Form Controls

// username and email will be grouped into userData property
<div id="user-data" ngModelGroup="userData"
            #userData="ngModelGroup">
          <div
            class="form-group"
          >
            <label for="username">Username</label>
            <input
              type="text"
              id="username"
              name="username"
              class="form-control"
              ngModel
              required
            />
          </div>
          <button class="btn btn-default" type="button">
            Suggest an Username
          </button>
          <div class="form-group">
            <label for="email">Mail</label>
            <input
              ngModel
              type="email"
              name="email"
              id="email"
              class="form-control"
              required
              email
              #email="ngModel"
            />
            <span class="help-block" *ngIf="!email.valid && email.touched"
              >Please enter a valid email!</span
            >
          </div>
        </div>
        <p *ngIf="!userData.valid && userData.touched">User Data is invalid!</p>

TD: Handling Radio Buttons

export class AppComponent {
  @ViewChild("f") signupForm: NgForm;
  defaultQuestion = "pet";
  answer = "";
  genders = ["male", "female"];
<div class="radio" *ngFor="let gender of genders">
          <label>
            <input
              type="radio"
              name="gender"
              ngModel
              [value]="gender"
              required
            />
            {{ gender }}
          </label>
        </div>

TD: Setting and Patching Form Values

export class AppComponent {
  @ViewChild("f") signupForm: NgForm;
  defaultQuestion = "pet";
  answer = "";
  genders = ["male", "female"];

  suggestUserName() {
    const suggestedName = "Superuser";

    // ! will overwrite all values
    // this.signupForm.setValue({
    //   userData: {
    //     username: suggestedName,
    //     email: "[email protected]"
    //   },
    //   secret: "pet",
    //   questionAnswer: "",
    //   gender: "male"
    // });

    // patch only
    this.signupForm.form.patchValue({
      userData: {
        username: suggestedName
      }
    });
  }
<form (ngSubmit)="onSubmit()" #f="ngForm">
        <div id="user-data">
          <div
            class="form-group"
            ngModelGroup="userData"
            #userData="ngModelGroup"
          >
            <label for="username">Username</label>
            <input
              type="text"
              id="username"
              name="username"
              class="form-control"
              ngModel
              required
            />
          </div>
          <button
            (click)="suggestUserName()"
            class="btn btn-default"
            type="button"
          >
            Suggest an Username
          </button>

TD: Using Form Data

export class AppComponent {
  @ViewChild("f") signupForm: NgForm;
  defaultQuestion = "pet";
  answer = "";
  genders = ["male", "female"];
  user = {
    username: "",
    email: "",
    secretQuestion: "",
    answer: "",
    gender: ""
  };
  submitted = false;
  ..........
   onSubmit() {
    this.submitted = true;
    this.user.username = this.signupForm.value.userData.username;
    this.user.email = this.signupForm.value.userData.email;
    this.user.secretQuestion = this.signupForm.value.secret;
    this.user.answer = this.signupForm.value.questionAnswer;
    this.user.gender = this.signupForm.value.gender;
  }
<div class="row" *ngIf="submitted">
    <div class="col-xs-12">
      <h3>Your Data</h3>
      <p>Username: {{ user.username }}</p>
      <p>Mail: {{ user.email }}</p>
      <p>Secret Question: Your first {{ user.secretQuestion }}</p>
      <p>Answer: {{ user.answer }}</p>
      <p>Gender: {{ user.gender }}</p>
    </div>
  </div>

Resetting the form

 onSubmit() {
    this.submitted = true;
    this.user.username = this.signupForm.value.userData.username;
    this.user.email = this.signupForm.value.userData.email;
    this.user.secretQuestion = this.signupForm.value.secret;
    this.user.answer = this.signupForm.value.questionAnswer;
    this.user.gender = this.signupForm.value.gender;

    // will reset the values and css classes
    // optional: you can pass an object as in setValue method
    this.signupForm.reset();
  }

Reactive Forms

// app.module.ts
@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, ReactiveFormsModule],
  providers: [],
  bootstrap: [AppComponent]
})

key features to watch for:

  • grouping
  • array of form controls
  • creating custom validators
  • using error codes to display different warnings
  • Creating a Custom Async Validator
  • Reacting to Status or Value Changes
    • on the form or an individual input
  • Setting and Patching Values
<form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
        <div formGroupName="userData">
          <div class="form-group">
            <label for="username">Username</label>
            <input
              type="text"
              id="username"
              formControlName="username"
              class="form-control"
            />
            <span
              *ngIf="
                !signupForm.get('userData.username').valid &&
                signupForm.get('userData.username').touched
              "
              class="help-block"
            >
              <span
                *ngIf="
                  signupForm.get('userData.username').errors['nameIsForbidden']
                "
                >This name is invalid!</span
              >
              <span
                *ngIf="signupForm.get('userData.username').errors['required']"
                >This field is required!</span
              >
            </span>
          </div>
          <div class="form-group">
            <label for="email">email</label>
            <input
              type="text"
              id="email"
              formControlName="email"
              class="form-control"
            />
            <span
              *ngIf="
                !signupForm.get('userData.email').valid &&
                signupForm.get('userData.email').touched
              "
              class="help-block"
              >Please enter a valid email!</span
            >
          </div>
        </div>
        <div class="radio" *ngFor="let gender of genders">
          <label>
            <input type="radio" formControlName="gender" [value]="gender" />{{
              gender
            }}
          </label>
        </div>
        <div formArrayName="hobbies">
          <h4>Your Hobbies</h4>
          <button class="btn btn-default" type="button" (click)="onAddHobby()">
            Add Hobby
          </button>
          <div
            class="form-group"
            *ngFor="
              let hobbyControl of signupForm.get('hobbies').controls;
              let i = index
            "
          >
            <input type="text" class="form-control" [formControlName]="i" />
          </div>
        </div>
        <span *ngIf="!signupForm.valid && signupForm.touched" class="help-block"
          >Please enter valid data!</span
        >
        <button class="btn btn-primary" type="submit">Submit</button>
      </form>
export class AppComponent implements OnInit {
  genders = ["male", "female"];
  signupForm: FormGroup;
  forbiddenUsernames = ["Chris", "Anna"];

  constructor() {}

  ngOnInit() {
    this.signupForm = new FormGroup({
      userData: new FormGroup({
        username: new FormControl(null, [
          Validators.required,
          this.forbiddenNames.bind(this)
        ]),
        email: new FormControl(
          null,
          [Validators.required, Validators.email],
          this.forbiddenEmails
        )
      }),
      gender: new FormControl("male"),
      hobbies: new FormArray([])
    });
    // this.signupForm.valueChanges.subscribe(
    //   (value) => console.log(value)
    // );
    this.signupForm.statusChanges.subscribe(status => console.log(status));
    this.signupForm.setValue({
      userData: {
        username: "Max",
        email: "[email protected]"
      },
      gender: "male",
      hobbies: []
    });
    this.signupForm.patchValue({
      userData: {
        username: "Anna"
      }
    });
  }

  onSubmit() {
    console.log(this.signupForm);
    this.signupForm.reset();
  }

  onAddHobby() {
    const control = new FormControl(null, Validators.required);
    (<FormArray>this.signupForm.get("hobbies")).push(control);
  }

  forbiddenNames(control: FormControl): { [s: string]: boolean } {
    if (this.forbiddenUsernames.indexOf(control.value) !== -1) {
      return { nameIsForbidden: true };
    }
    return null;
  }

  forbiddenEmails(control: FormControl): Promise<any> | Observable<any> {
    const promise = new Promise<any>((resolve, reject) => {
      setTimeout(() => {
        if (control.value === "[email protected]") {
          resolve({ emailIsForbidden: true });
        } else {
          resolve(null);
        }
      }, 1500);
    });
    return promise;
  }
}

Pipes

Pipes are used for transforming the Output

{{ server.instanceType | uppercase }}
{{ server.started | date }}

Configure a pipe

{{ server.started | date:'fullDate' }}

Where to find how to configure built-in pipes

https://angular.io/api

Chaining pipes

{{ server.started | date:'fullDate' | uppercase }}

Creating a pipe

// shorten.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'shorten'
})
export class ShortenPipe implements PipeTransform {
  transform(value: any, limit: number) {
    if (value.length > limit) {
      return value.substr(0, limit) + ' ...';
    }
    return value;
  }
}

@NgModule({
  declarations: [
    AppComponent,
    ShortenPipe,
    FilterPipe
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
{{ server.name | shorten:15 }}

Creating a pipe with the cli:

ng g p filter
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'filter',
  pure: false // ! may lead to performance issues
})
export class FilterPipe implements PipeTransform {

  transform(value: any, filterString: string, propName: string): any {
    if (value.length === 0 || filterString === '') {
      return value;
    }
    const resultArray = [];
    for (const item of value) {
      if (item[propName] === filterString) {
        resultArray.push(item);
      }
    }
    return resultArray;
  }

}

<div class="col-xs-12 col-sm-10 col-md-8 col-sm-offset-1 col-md-offset-2">
      <input type="text" [(ngModel)]="filteredStatus"> // !
      <br>
      <button class="btn btn-primary" (click)="onAddServer()">Add Server</button>
      <br><br>
      <h2>App Status: {{ appStatus | async}}</h2>
      <hr>
      <ul class="list-group">
        <li
          class="list-group-item"
          *ngFor="let server of servers | filter:filteredStatus:'status'" // !
          [ngClass]="getStatusClasses(server)">
          <span
            class="badge">
            {{ server.status }}
          </span>
          <strong>{{ server.name | shorten:15 }}</strong> |
          {{ server.instanceType | uppercase }} |
          {{ server.started | date:'fullDate' | uppercase }}
        </li>
      </ul>
    </div>

Using the built-in async pipe

<h2>App Status: {{ appStatus | async}}</h2>
export class AppComponent {
  appStatus = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('stable');
    }, 2000);
  });

HTTP requests - the old way

npm install --save @angular/http
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { FormsModule } from "@angular/forms";
import { HttpModule } from "@angular/http";

import { AppComponent } from "./app.component";
import { ServerService } from "./server.service";

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

import { Injectable } from "@angular/core";
import { Headers, Http, Response } from "@angular/http";

@Injectable()
export class ServerService {
  constructor(private http: Http) {}
  storeServers(servers: any[]) {
    // no request is made yet
    return this.http.post(
      {theUrl},
      servers
    );
  }
}

// app.component.ts

 onSaveServers() {
    this.serverService
      .storeServers(this.servers)
      .subscribe(data => console.log(data), error => console.log(error));
  }

sending headers

// sending headers

export class ServerService {
  constructor(private http: Http) {}
  storeServers(servers: any[]) {
    const headers = new Headers({ "Content-Type": "application/json" });

    return this.http.post(
      "{theUrl}",
      servers,
      { headers }
    );
  }
}

GET request

import { Response } from "@angular/http";


 onGetServers() {
    this.serverService
      .getServers()
      .subscribe(
        (data: Response) => console.log(data.json()),
        error => console.log(error)
      );
  }
getServers() {
    return this.http.get("{theUrl}");
  }

HTTP requests - using HttpClient

import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";

@Injectable()
export class ServerService {
  constructor(private httpClient: HttpClient) {}
  storeServers(servers: any[]) {
    return this.httpClient.post(
      "{theUrl}",
      servers
    );
  }
  getServers() {
    return this.httpClient.get(
      "{theUrl}"
    );
  }
}

import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { FormsModule } from "@angular/forms";
import { HttpClientModule } from "@angular/common/http";

import { AppComponent } from "./app.component";
import { ServerService } from "./server.service";

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

onGetServers() {
    this.serverService
      .getServers()
      .subscribe(data => console.log(data), error => console.log(error));
  }

Auth with Firebase

npm i --save firebase

init firebase

import { Component, OnInit } from "@angular/core";
import * as firebase from "firebase"; // !

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
export class AppComponent implements OnInit {
  title = "test-app";

  ngOnInit() { // !
    firebase.initializeApp({
      apiKey: "{apiKey}",
      authDomain: "{authDomain}"
    });
  }
}

create auth service

import { Injectable } from "@angular/core";
import * as firebase from "firebase";

@Injectable({
  providedIn: "root"
})
export class AuthService {
  constructor() {}

  signupUser(email: string, password: string) {
    firebase
      .auth()
      .createUserWithEmailAndPassword(email, password)
      .catch(error => console.log(error));
  }
}

export class SignUpComponent implements OnInit {
  @ViewChild("f") signupForm: NgForm;

  constructor(private authService: AuthService) {}

  ngOnInit() {}

  onSubmit() {
    const { email, password } = this.signupForm.value;

    this.authService.signupUser(email, password);
  }
}

Get logged in user's token

 signinUser(email: string, password: string) {
    firebase
      .auth()
      .signInWithEmailAndPassword(email, password)
      .then(data => {
        firebase.auth().currentUser.getIdToken()
        .then((token: string) => {
          this.token = token;
        }).catch((err) => {
          console.log(err);
        });
      })
      .catch(error => console.log(error));
  }

Creating Guard for checking logged in user

// auth-guard.service.ts
import {
  CanActivate,
  ActivatedRouteSnapshot,
  RouterStateSnapshot,
  Router
} from "@angular/router";
import { Observable } from "rxjs";
import { Injectable } from "@angular/core";

import { AuthService } from "./auth/auth.service";

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): boolean {
    if (this.authService.isAuthenticated()) {
      return true;
    } else {
      this.router.navigate(["/"]);
    }
  }
}
// auth.service.ts
import { Injectable } from '@angular/core';
import * as firebase from 'firebase';

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  token: string = null;

  constructor() {}

  signupUser(email: string, password: string) {
    firebase
      .auth()
      .createUserWithEmailAndPassword(email, password)
      .catch(error => console.log(error));
  }

  signinUser(email: string, password: string) {
    firebase
      .auth()
      .signInWithEmailAndPassword(email, password)
      .then(data => {
        firebase
          .auth()
          .currentUser.getIdToken()
          .then((token: string) => {
            this.token = token;
          })
          .catch(err => {
            console.log(err);
          });
      })
      .catch(error => console.log(error));
  }

  isAuthenticated() {
    return this.token !== null;
  }

  logout() {
    firebase.auth().signOut();
    this.token = null;
  }
}
// app.module.ts
@NgModule({
  declarations: [
    AppComponent,
    HeaderComponent,
    RecipesComponent,
    ShoppingListComponent,
    PageNotFoundComponent,
    ProfileComponent,
    SignUpComponent,
    SignInComponent
  ],
  imports: [BrowserModule, AppRoutingModule, FormsModule],
  providers: [StaticAuthService, AuthGuard],
  bootstrap: [AppComponent]
})
export class AppModule {}
import { NgModule } from "@angular/core";
import { Routes, RouterModule } from "@angular/router";

import { RecipesComponent } from "./recipes/recipes.component";
import { ShoppingListComponent } from "./shopping-list/shopping-list.component";
import { PageNotFoundComponent } from "./page-not-found/page-not-found.component";
import { ProfileComponent } from "./profile/profile.component";
import { AuthGuard } from "./auth-guard.service";
import { SignUpComponent } from "./auth/sign-up/sign-up.component";
import { SignInComponent } from "./auth/sign-in/sign-in.component";

const appRoutes: Routes = [
  { path: "", component: RecipesComponent },
  {
    path: "shopping-list",
    component: ShoppingListComponent
  },
  {
    path: "profile",
    canActivate: [AuthGuard],
    component: ProfileComponent
  },
  {
    path: "sign-up",
    component: SignUpComponent
  },
  {
    path: "sign-in",
    component: SignInComponent
  },
  { path: "not-found", component: PageNotFoundComponent },
  { path: "**", redirectTo: "/not-found" }
];

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


ngRx

the concept

  • store - source of truth statewise
  • reducers - functions which change the state
  • dispatching an action will trigger reducers
npm i --save @ngrx/store

Material Design

npm install --save @angular/material @angular/cdk @angular/animations
// app.module.ts

// mat
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import {
  MatButtonModule,
  MatCheckboxModule,
  MatIconModule,
  MatSidenavModule
} from '@angular/material';

@NgModule({
  declarations: [
    AppComponent,
    HeaderComponent,
    RecipesComponent,
    ShoppingListComponent,
    PageNotFoundComponent,
    ProfileComponent,
    SignUpComponent,
    SignInComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    FormsModule,
    ReactiveFormsModule,
    HttpClientModule,
    BrowserAnimationsModule, // mat
    MatButtonModule, // mat
    MatCheckboxModule, // mat
    MatIconModule, // mat
    MatSidenavModule // mat
  ],
  providers: [StaticAuthService, AuthGuard, DataStorageService],
  bootstrap: [AppComponent]
})
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>TestApp</title>
    <base href="/" />

    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="icon" type="image/x-icon" href="favicon.ico" />
    <link
      href="https://fonts.googleapis.com/icon?family=Material+Icons"
      rel="stylesheet"
    />
  </head>
  <body>
    <app-root></app-root>
  </body>
</html>

// styles.css

/* You can add global styles to this file, and also import other style files */
@import '~@angular/material/prebuilt-themes/indigo-pink.css';


Modules

create a module

ng g m recipes/recipes
// recipes.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RecipesComponent } from './recipes.component';
import { RecipesRoutingModule } from './recipes-routing.module';
import { ReactiveFormsModule } from '@angular/forms';

@NgModule({
  declarations: [RecipesComponent],
  imports: [CommonModule, RecipesRoutingModule, ReactiveFormsModule]
})
export class RecipesModule {}

// recipes-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { RecipesComponent } from './recipes.component';

const recipeRoutes: Routes = [{ path: 'recipes', component: RecipesComponent }];

@NgModule({
  imports: [RouterModule.forChild(recipeRoutes)], // forChild!
  exports: [RouterModule]
})
export class RecipesRoutingModule {}

// app-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { ShoppingListComponent } from './shopping-list/shopping-list.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
import { ProfileComponent } from './profile/profile.component';
import { AuthGuard } from './auth-guard.service';
import { SignUpComponent } from './auth/sign-up/sign-up.component';
import { SignInComponent } from './auth/sign-in/sign-in.component';

const appRoutes: Routes = [
  {
    path: '',
    redirectTo: '/recipes',
    pathMatch: 'full'
  },
  {
    path: 'shopping-list',
    component: ShoppingListComponent,
    canActivate: [AuthGuard]
  },
  {
    path: 'profile',
    canActivate: [AuthGuard],
    component: ProfileComponent
  },
  {
    path: 'sign-up',
    component: SignUpComponent
  },
  {
    path: 'sign-in',
    component: SignInComponent
  },
  { path: 'not-found', component: PageNotFoundComponent },
  { path: '**', redirectTo: '/not-found' }
];

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

// app.module.ts

@NgModule({
  declarations: [
    AppComponent,
    HeaderComponent,
    ShoppingListComponent,
    PageNotFoundComponent,
    ProfileComponent,
    SignUpComponent,
    SignInComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    FormsModule,
    ReactiveFormsModule,
    HttpClientModule,
    RecipesModule
  ],
  providers: [StaticAuthService, AuthGuard, DataStorageService],
  bootstrap: [AppComponent]
})
export class AppModule {}


as operator

*ngFor="let item of items; index as i"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment