Angular Basic (E)

h232ch·2023년 8월 20일
0
post-thumbnail

Property & Event Binding


HTML Elements & Event Binding

HTML Elements

<img alt="item" [src]="itemImageUrl">

The [src] property is bound to the <img> HTML element. We can utilize this property binding in our template code.

<!-- Bind button disabled state to the `isUnchanged` property -->
<button type="button" [disabled]="isUnchanged">Disabled Button</button>

We can also use the [disabled] property to control whether the button is enabled or not. This property corresponds to the HTML attribute disabled. As seen, property binding enables us to control HTML elements dynamically.

HTML Events

<button (click)="onSave()">Save</button>

Clicking the above button triggers the execution of the onSave() method in the component. The (click) event binding in Angular functions similarly to the onClick event in HTML. Angular provides this method of event binding to manage HTML events.

Directive

Attribute Directives

NgClass

<!-- Toggle the "special" class on/off with a property -->
<div [ngClass]="isSpecial ? 'special' : ''">This div is special</div>

We can alter class state using ngClass. In the above example, the state of the [ngClass] value changes based on the value of isSpecial.

currentClasses: Record<string, boolean> = {};
/* . . . */
setCurrentClasses() {
  // CSS classes: added/removed based on the current state of component properties
  this.currentClasses =  {
    saveable: this.canSave,
    modified: !this.isUnchanged,
    special:  this.isSpecial
  };
}

The currentClasses object is updated based on the values of the properties. These properties determine which CSS classes are added or removed.

<div [ngClass]="currentClasses">This div is initially saveable, unchanged, and special.</div>

The class of the div element is determined by the currentClasses object.

NgStyle

currentStyles: Record<string, string> = {};
/* . . . */
setCurrentStyles() {
  // CSS styles: set based on the current state of component properties
  this.currentStyles = {
    'font-style':  this.canSave      ? 'italic' : 'normal',
    'font-weight': !this.isUnchanged ? 'bold'   : 'normal',
    'font-size':   this.isSpecial    ? '24px'   : '12px'
  };
}

Similar to ngClass, ngStyle is used to dynamically apply styles. In the currentStyles object, various styles change based on the values of canSave, isUnchanged, and isSpecial.

<div [ngStyle]="currentStyles">
  This div is initially italic, normal weight, and extra large (24px).
</div>

NgModel

The ngModel directive is used to display a data property and update it when the user makes changes. Before using this directive, ensure you add FormsModule to the imports list of your NgModule.

import { FormsModule } from '@angular/forms';
/* . . . */
@NgModule({
  /* . . . */
  imports: [
    BrowserModule,
    FormsModule
  ],
  /* . . . */
})
export class AppModule { }

Afterward, you can use the ngModel directive like so:

<label for="example-ngModel">[(ngModel)]:</label>
<input [(ngModel)]="currentItem.name" id="example-ngModel">

This enables two-way binding between the input element and the currentItem.name property.

Customizing the directive is also possible:

<input [ngModel]="currentItem.name" (ngModelChange)="setUppercaseName($event)" id="example-uppercase">

Structural Directives

NgIf

To conditionally add or remove HTML elements, use ngIf. The app-item-detail component will be displayed or hidden based on the value of the isActive property. If isActive is true, the app-item-detail component will be visible; otherwise, it will not be displayed.

<app-item-detail *ngIf="isActive" [item]="item"></app-item-detail>

NgFor

To display a list of items, use ngFor. In the following example, the items array is iterated over and each item's name is displayed.

<div *ngFor="let item of items">{{item.name}}</div>

You can also pass each item to the child component using ngFor:

<app-item-detail *ngFor="let item of items" [item]="item"></app-item-detail>

To display the index of the ngFor iteration, use the following syntax:

<div *ngFor="let item of items; let i=index">{{i + 1}} - {{item.name}}</div>

You can use <ng-container> it's especially useful when we need to avoid interfering with styles or layout because Angular doesn't render it in the DOM.

<p>
  I turned the corner
  <ng-container *ngIf="hero">
    and saw {{hero.name}}. I waved
  </ng-container>
  and continued on my way.
</p>

Custom Property and Event Binding

In this section, I will demonstrate how to bind custom properties and events. We will create an application that allows the addition and deletion of servers using the Add Server and Add Server Blueprint functionalities.

The application comprises three components:

  • AppComponent: This component contains the following sub-components.
  • CockpitComponent: Responsible for managing server changes.
  • ServerElementComponent: Displays the status of servers.

app.component.ts

import {
  Component,
  OnDestroy
} from '@angular/core';
import { Server } from './server.model';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnDestroy {
  title = 'deep-dive-angular';
  serverElements: Server[] = [{
    type: 'server',
    name: 'Test Server',
    content: 'Just a test',
  }];

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

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

  onChangeFirst() {
    this.serverElements[0].name = 'Changed!';
  }

  onDestroyFirst() {
    this.serverElements.splice(0, 1);
  }

  ngOnDestroy(): void {
    // Cleanup logic if needed
  }
}

app.component.html

<div class="container">
  <app-cockpit
    (serverCreated)="onServerAdded($event)"
    (bpCreated)="onServerBlueprintAdded($event)"
  ></app-cockpit>
  <hr>
  <div class="row">
    <div class="col-xs-12">
      <button class="btn btn-primary" (click)="onChangeFirst()">Change first element</button>
      <button class="btn btn-danger" (click)="onDestroyFirst()">Destroy first component</button>
      <app-server-element
        *ngFor="let element of serverElements"
        [svrElement]="element"
        [name]="element.name">
        <p #contentParagraph>
          <strong *ngIf="element.type === 'server'" style="color: red">
            {{ element.content }}
          </strong>
          <em *ngIf="element.type === 'blueprint'">
            {{ element.content }}
          </em>
        </p>
      </app-server-element>
    </div>
  </div>

  <hr>
  <div class="col-xs-12">
    <label for="title">ngOnChanges TEST </label> <br>
    <input id="title" type="text" [(ngModel)]="title">
  </div>
</div>

In the AppComponent, we have an app-cockpit component with event bindings (serverCreated) and (bpCreated). This implies that when these events are triggered within the app-cockpit, the changed values will be emitted to the parent component (AppComponent). Then, AppComponent will update the serverData property values using the onServerAdded($event) and onServerBlueprintAdded($event) methods.

AppComponent also contains an app-server-element component with property bindings [svrElement] and [name]. The element is provided by the serverElements array used in the ngFor loop statement.


cockpit.component.ts

import { Component, EventEmitter, Output } from '@angular/core';
import { Server } from '../server.model';

@Component({
  selector: 'app-cockpit',
  templateUrl: './cockpit.component.html',
  styleUrls: ['./cockpit.component.css']
})
export class CockpitComponent {
  @Output() serverCreated = new EventEmitter<Server>();
  @Output('bpCreated') blueprintCreated = new EventEmitter<Server>();
  newServerName = '';
  newServerContent = '';

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

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

cockpit.component.html

<div class="row">
  <div class="col-xs-12">
    <label for="name">Server Name</label>
    <input type="text" class="form-control" id="name" [(ngModel)]="newServerName">
    <label for="content">Server Content</label>
    <input type="text" class="form-control" id="content" [(ngModel)]="newServerContent">
    <br>
    <button class="btn btn-primary" (click)="onAddServer()">Add Server</button>
    <button class="btn btn-primary" (click)="onAddBlueprint()">Add Server Blueprint</button>
  </div>
</div>

In the CockpitComponent, two properties are defined: newServerName and newServerContent, which are initialized to empty strings. In the template, they are used with two-way binding [(ngModel)]. Typing something in the input fields updates these property values. When the Add Server button is clicked, the onAddServer() method is triggered, emitting the serverName and serverContent data that was entered into the input fields.

server-element.component.ts

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

@Component({
  selector: 'app-server-element',
  templateUrl: './server-element.component.html',
  styleUrls: ['./server-element.component.css'],
  encapsulation: ViewEncapsulation.Emulated
})
export class ServerElementComponent {
  @Input('svrElement') element: {
    type: string,
    name: string,
    content: string
  };
  @Input() name: string;
  // Rest of your code
}

server-element.component.html

<div class="panel panel-default">
  <div class="panel-heading" #heading> {{ name }} </div>
  <div class="panel-body">
    <ng-content></ng-content>
  </div>
</div>

server-element.component.css

p {
  color: blue;
}

label {
  color: red;
}

In the ServerElementComponent, the element property is bound using the @Input decorator. This allows the component to receive data from the parent component. The name property is also bound using @Input. The template displays the name using interpolation ({{ name }}), and the content inside the ng-content tag is projected from the parent component.

View Encapsulation


https://medium.com/@su_bak/angular-viewencapsulation-d33fbaf8bf68

Decorators


@ViewChild

View queries are set before the ngAfterViewInit callback is called.
When you want to access a child component's properties, you can use this decorator @ViewChild, but sometimes it can make the code more complicated. You should use this decorator carefully.

Components that are included within parent components are referred to as child components. You can use property binding if you want to send data to a child component. However, if you use the @ViewChild decorator, you can directly access the property of the child component without using property binding.

app-component.ts

import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core';
import { ChildComponent } from "./child/child.component";

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements AfterViewInit {
  // Direct Access to Child Data
  @ViewChild(ChildComponent) childComp!: ChildComponent;

  // Direct Access to Template Element
  @ViewChild('top') topRef!: ElementRef;

  ngAfterViewInit(): void {
    console.log(this.childComp.childData)
    console.log(this.topRef.nativeElement.textContent)
  }
}

app-component.html

<div class="container">
  <div class="row">
    <div class="col-md-12">
      <app-child>
        <p>What's up</p>
      </app-child>
      <p #top>TOP</p>
    </div>
  </div>
</div>

child.component.ts

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

@Component({
  selector: 'app-child',
  templateUrl: './child.component.html',
  styleUrls: ['./child.component.css']
})
export class ChildComponent {
  childData = 'child data'
}

child.component.html

<ng-content></ng-content>

If you need to control multiple child components, you can use ViewChildren.

@ContentChild

Used to retrieve the first element or directive matching the selector from the content DOM. If the content DOM changes and a new child matches the selector, the property will be updated. Content queries are set before ngAfterContentInit.

app-component.html

<div class="container">
  <div class="row">
    <div class="col-md-12">
      <app-child>
        <p #greeting>What's up</p>
      </app-child>
      <p #top>TOP</p>
    </div>
  </div>
</div>

child.component.ts

import { AfterContentInit, Component, ContentChild, ElementRef } from '@angular/core';

@Component({
  selector: 'app-child',
  templateUrl: './child.component.html',
  styleUrls: ['./child.component.css']
})
export class ChildComponent implements AfterContentInit {
  childData = 'child data'
  @ContentChild('greeting') greeting!: ElementRef;

  ngAfterContentInit(): void {
    console.log('CONTENT CHILD SAID: ' + this.greeting.nativeElement.textContent)
  }
}

child.component.html

<ng-content></ng-content>

If you need to control multiple child components, you can use ContentChildren.

Architecture


Really important articles of Angular

https://angular.io/guide/architecture
https://angular.io/guide/architecture-next-steps
https://angular.io/guide/observables
https://angular.io/guide/lifecycle-hooks
https://angular.io/guide/forms-overview

Angular makes signle-page client application using HTML and TypeScript. As a set of TypeScript library, you can implements core and optional functionality.

The architecture of Angular relies on certain fundamental concepts. Angular components which are building block of the Angular framework are organized into ngModules. the ngModules collect related code into functional sets and an Angular application is defined by the ngModules. An Angular application has at least a root module.

  • Components define views which are sets of screen eletments.
  • Components use services which provide specific funtionality not directly related to views.

Modules, Components, Services are clases that use decorators.

  • The metadata for a component class associate it with a template that defines a view. A template combines ordinary HTML with Angular directives and binding markup (various binding methods are there) that allow Angular to modify the HTML before rendering it for display.
  • The metadata for a service class provides the information Angular needs to make it available to components through dependency injection (DI).

And an Angular framework provides router service which provides sophisticated in-browser navigatinal capabilities.

Lifecycle hooks

A component instance has a lifecycle that starts when Angular instantiates the component class and the component view along with it's child views. The lifecycle continues with change detection, when the data-bound properties change, Angular updates both the view and component instance and the lifecycle ends when Angular destoys the component instance and removes it's rendered template from the DOM. directives have a similar lifecyle, as Angular creates, updates, and destroys instaces in the course of excution.

ngOnChanges

Respond when Angular sets or resets data-bound input properties. The method receives a SimpleChanges object of current and previous property values. Called before ngOnInit() (if the component has bound inputs) and whenever one or more data-bound input properties change.

ngOnInit

Initialize the directive or component after Angular first displays the data-bound properties and sets the directive or component's input properties. Called once, after the first ngOnChanges(). ngOnInit() is still called even when ngOnChanges() is not (which is the case when there are no template-bound inputs).

ngOnDoCheck

Detect and act upon changes that Angular can't or won't detect on its own. Called immediately after ngOnChanges() on every change detection run, and immediately after ngOnInit() on the first run.
Defining custom change detection

ngOnAfterContentInit

Respond after Angular projects external content into the component's view, or into the view that a directive is in. Called once after the first ngDoCheck().

When the <ng-content> is projected by a parent template to add contents into a child template ngOnAfterContentInit is executed.

ngOnAfterContentChecked

Respond after Angular checks the content projected into the directive or component. Called after ngAfterContentInit() and every subsequent ngDoCheck().

ngOnAfterViewInit

Respond after Angular initializes the component's views and child views, or the view that contains the directive. Called once after the first ngAfterContentChecked(). When views are rendered ngOnAfterViewInit is executed.

ngOnAfterViewChecked

Respond after Angular checks the component's views and child views, or the view that contains the directive. Called after the ngAfterViewInit() and every subsequent ngAfterContentChecked().

ngOnDestroy

Cleanup just before Angular destroys the directive or component. Unsubscribe Observables and detach event handlers to avoid memory leaks. Called immediately before Angular destroys the directive or component.

Router Module

We want to make users find routes which they want and Router module helps users find the routes with route module like below. appRoute is like a routing table of Angular application and we should import the appRoute to Angular module to make Angular aware this.

import {RouterModule, Routes} from "@angular/router";  
import {NgModule} from "@angular/core";  
import {HomeComponent} from "./home/home.component";  
  
const appRoute: Routes = [  
  {path: '', component: HomeComponent}  
]  
  
@NgModule({  
  imports: [  
    RouterModule.forRoot(appRoute)  
  ],  
  exports: [RouterModule]  
})  
export class AppRoutingModule {  
  
}

After creating the AppRoutingModule, we should add the module in the imports list of AppModule to make Angular aware the AppRoutingModule.

import { NgModule } from '@angular/core';  
import { BrowserModule } from '@angular/platform-browser';  
  
import { AppComponent } from './app.component';  
import { HeaderComponent } from './header/header.component';  
import {RouterOutlet} from "@angular/router";  
import { ServersComponent } from './servers/servers.component';  
import { UsersComponent } from './users/users.component';  
import { HomeComponent } from './home/home.component';  
import {AppRoutingModule} from "./app-routing.module";  
  
@NgModule({  
  declarations: [  
    AppComponent,  
    HeaderComponent,  
    ServersComponent,  
    UsersComponent,  
    HomeComponent  
  ],  
    imports: [  
        BrowserModule,  
        RouterOutlet,  
        AppRoutingModule,  
    ],  
  providers: [],  
  bootstrap: [AppComponent]  
})  
export class AppModule { }

Finally we add RouterOutlet in AppComponent to make Angular navigate routes with our AppRoutingModule after adding the codes we can check below.

HeaderComponent

<div class="container">  
  <div class="row">  
    <div class="col-md-12">  
      <h4>HOME</h4>  
    </div>  
  </div>
</div>

AppComponent

<app-header></app-header>  
<router-outlet></router-outlet>

Router Links


In our Angular application, navigating between different pages like Servers and Users is made effortless using RouterLink. These links, placed in the header, seamlessly guide users to their desired destinations. It's important to note that this mechanism is tailored for Single Page Applications (SPAs). Unlike traditional websites, where clicking a link would trigger a server request and reload the entire page, RouterLink ensures a smooth transition within the application.

AppRoutingModule

import { RouterModule, Routes } from "@angular/router";
import { NgModule } from "@angular/core";
import { HomeComponent } from "./home/home.component";
import { ServersComponent } from "./servers/servers.component";
import { UsersComponent } from "./users/users.component";

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

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

HeaderComponent

<!-- Your header component HTML remains unchanged. -->

In our HomeComponent, we've implemented a button that, when clicked, navigates users to the /servers route.

import { Component } from '@angular/core';
import { Router } from "@angular/router";

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css']
})
export class HomeComponent {
  constructor(private router: Router) { }

  OnLoadServers() {
    this.router.navigate(['servers']);
  }
}

Relative Paths

When reloading the current page within the ServersComponent, we've used the relativeTo option to ensure a relative path is followed, maintaining the context of the current route.

import { Component } from '@angular/core';
import { Router, ActivatedRoute } from "@angular/router";

@Component({
  selector: 'app-servers',
  templateUrl: './servers.component.html',
  styleUrls: ['./servers.component.css']
})
export class ServersComponent {
  constructor(private router: Router, private route: ActivatedRoute) { }

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

Parameter Parsing

For parameter parsing, the ServerComponent dynamically fetches server details based on the ID provided in the route parameters. We've employed observables to ensure accurate handling of URL parameter changes.

import { Component, OnInit } from '@angular/core';
import { ServersService } from "../servers-service";
import { Server } from "../server.model";
import { ActivatedRoute, Params } from "@angular/router";

@Component({
  selector: 'app-server',
  templateUrl: './server.component.html',
  styleUrls: ['./server.component.css']
})
export class ServerComponent implements OnInit {
  server: Server | undefined;

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

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

Child Routes

In our application, child routes have been implemented to display detailed server information. The AppRoutingModule has been configured to handle these nested routes, allowing for a structured and intuitive user experience.

import { RouterModule, Routes } from "@angular/router";
import { NgModule } from "@angular/core";
import { HomeComponent } from "./home/home.component";
import { ServersComponent } from "./servers/servers.component";
import { UsersComponent } from "./users/users.component";
import { ServerComponent } from "./servers/server/server.component";

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

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

Redirecting


In your Angular application, you can seamlessly redirect users to specific pages for non-existent routes or routes that require special permissions using AppRouterApp.

AppRoutingModule

import { RouterModule, Routes } from "@angular/router";
import { NgModule } from "@angular/core";
import { HomeComponent } from "./home/home.component";
import { ServersComponent } from "./servers/servers.component";
import { UsersComponent } from "./users/users.component";
import { ServerComponent } from "./servers/server/server.component";
import { PageNotFoundComponent } from "./page-not-found/page-not-found.component";
import { AuthGuardService } from "./auth-guard.service";
import { EditServerComponent } from "./servers/edit-server/edit-server.component";
import { CanDeactivateGuard } from "./servers/edit-server/can-deactivate-guard.service";

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

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

Protecting Routes (Guard)


canActivate

Certain pages require permission, such as logging in. To redirect users to a specific page when they are not logged in, utilize the canActivate feature. This evaluates the user's login status and redirects them accordingly.

First, create the AuthService with methods like login, logout, and isAuthenticated to manage user login state.

@Injectable()
export class AuthService {

  private isLogin: boolean = false;

  isAuthenticated(): Promise<boolean> {
    return new Promise(resolve => setTimeout(() => resolve(this.isLogin), 500));
  }

  login() {
    this.isLogin = true;
  }

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

Next, implement the AuthGuardService using the CanActivate interface, checking the user's authentication status and redirecting if necessary.

@Injectable()
export class AuthGuardService 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 => {
      if (authenticated) {
        return true;
      } else {
        this.router.navigate(['/']);
        return false;
      }
    });
  }
}

To apply the canActivate feature, add the canActivate option to the route where you want to enforce authentication.

const appRoutes: Routes = [
  // ... other routes
  {
    path: 'servers',
    component: ServersComponent,
    canActivate: [AuthGuardService],  // <-- Apply canActivate guard here
    children: [
      { path: ':id', component: ServerComponent },
      { path: ':id/edit', component: EditServerComponent },
    ]
  },
  // ... other routes
];

Finally, create login and logout buttons to access the servers page, which requires login. Clicking the login button allows access to the servers page, and clicking the logout button denies access.

HomeComponent

import { Component } from '@angular/core';
import { Router } from "@angular/router";
import { AuthService } from "../auth.service";

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css']
})
export class HomeComponent {

  constructor(private router: Router, private authService: AuthService) {}

  onLoadServers() {
    this.router.navigate(['servers']);
  }

  onLogin() {
    this.authService.login();
  }

  onLogout() {
    this.authService.logout();
  }
}

HomeComponent Template

<div class="container">
  <div class="row">
    <div class="col-md-12">
      <h4>HOME</h4>
      <button class="btn btn-primary" (click)="onLoadServers()">Load Servers</button>
      <button class="btn btn-info" (click)="onLogin()">Login</button>
      <button class="btn btn-warning" (click)="onLogout()">Logout</button>
    </div>
  </div>
</div>

If you wish to implement this feature in child routes, utilize the canActivateChild method in your AuthGuardService.

CanDeactivate

In Angular, the canDeactivate feature allows you to perform actions when a route is deactivated, giving you the opportunity to handle scenarios where a user attempts to leave a page without saving their data. For instance, you can alert the user that unsaved changes will be lost using the canDeactivate method.

EditServerComponent
Firstly, create the EditServerComponent which enables users to edit server details.

import { Component, OnInit } from '@angular/core';
import { ServersService } from "../servers-service";
import { ActivatedRoute, Router } from "@angular/router";
import { Server } from "../server.model";
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 {

  serverName: string = '';
  serverStatus: string = '';
  serverId: number;
  server: Server;
  changesSaved: boolean = false;

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

  ngOnInit(): void {
    this.serverId = +this.route.snapshot.params['id'];
    this.server = this.serversService.getServer(this.serverId);
    if (this.server) {
      this.serverName = this.server.name;
      this.serverStatus = this.server.status;
    }
  }

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

  canDeactivate(): Observable<boolean> | Promise<boolean> | boolean {
    if ((this.serverName !== this.server.name || this.serverStatus !== this.server.status) && !this.changesSaved) {
      return confirm('You have unsaved changes! Do you really want to leave?');
    }
    return true;
  }
}

EditServerComponent Template

<h4>Server Edit Page</h4>
<div class="form-group">
  <label for="name">Server Name</label>
  <input type="text" id="name" class="form-control" [(ngModel)]="serverName">
</div>
<div class="form-group">
  <label for="status">Server Status</label>
  <select type="text" id="status" class="form-control" [(ngModel)]="serverStatus">
    <option value="online">Online</option>
    <option value="offline">Offline</option>
  </select>
  <button class="btn btn-primary" (click)="onUpdateServer()">Update Server</button>
</div>

CanDeactivateGuard

Next, create the CanDeactivateGuard and CanComponentDeactivate interface. The guard will be used in your AppRoutingModule with the CanDeactivate option.

import { Observable } from "rxjs";
import { ActivatedRouteSnapshot, CanDeactivate, RouterStateSnapshot, UrlTree } 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 | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
        return component.canDeactivate();
    }
}

AppRoutingModule Implementation

Finally configure the canDeactivate guard in your AppRoutingModule to enable the alert functionality when a user attempts to leave the editing server page without saving changes.

import { RouterModule, Routes } from "@angular/router";
import { NgModule } from "@angular/core";
import { HomeComponent } from "./home/home.component";
import { ServersComponent } from "./servers/servers.component";
import { UsersComponent } from "./users/users.component";
import { ServerComponent } from "./servers/server/server.component";
import { PageNotFoundComponent } from "./page-not-found/page-not-found.component";
import { AuthGuardService } from "./auth-guard.service";
import { EditServerComponent } from "./servers/edit-server/edit-server.component";
import { CanDeactivateGuard } from "./servers/edit-server/can-deactivate-guard.service";

const appRoutes: Routes = [
  { path: '', component: HomeComponent },
  {
    path: 'servers',
    component: ServersComponent,
    canActivateChild: [AuthGuardService],
    children: [
      { path: ':id', component: ServerComponent },
      { path: ':id/edit', component: EditServerComponent, canDeactivate: [CanDeactivateGuard] },
    ]
  },
  { path: 'users', component: UsersComponent },
  { path: '**', component: PageNotFoundComponent },
];

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

With these implementations, users will be prompted with an alert when trying to leave the editing server page without saving changes, ensuring a smooth and intuitive user experience.

Resolver Guard

The reolve gurad is used to retrieve data from the API and once we get the data we will navigate to the route. We create resolve service which retrieve the server data with resolve method.

General routing flow is that
1. User clicks the link
2. Angular loads the respective component

Routing flow with resolver
1. User click the link
2. Angular executes certain code and returns a value or observable
3. You can collect the returned value or observable in constructor or in ngOnInit, in class of your component which is about to load
4. Use the collected the data for your purpose
5. Now you can load your component

Steps 2, 3, 4 are done with a code called Resolver.

Basically resolver is that intermediate code, which can be executed when a link has been clicked and before a component is loaded.

ServerResorverService

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


@Injectable()
export class ServerResolverService implements Resolve<Server>{

  constructor(private serversService:ServersService) { }

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

Next add the resolve option into the servers/:id route

AppRoutingModule

import {RouterModule, Routes} from "@angular/router";
import {NgModule} from "@angular/core";
import {HomeComponent} from "./home/home.component";
import {ServersComponent} from "./servers/servers.component";
import {UsersComponent} from "./users/users.component";
import {ServerComponent} from "./servers/server/server.component";
import {PageNotFoundComponent} from "./page-not-found/page-not-found.component";
import {AuthGuardService} from "./auth-guard.service";
import {EditServerComponent} from "./servers/edit-server/edit-server.component";
import {CanDeactivateGuard} from "./servers/edit-server/can-deactivate-guard.service";
import {ServerResolverService} from "./servers/server/server-resolver.service";

const appRoute: Routes = [
  {path: '', component: HomeComponent},
  {path: 'servers',
    component: ServersComponent,
    // canActivate: [AuthGuardService],
    canActivateChild: [AuthGuardService],
    children: [
      {path: ':id', component: ServerComponent, resolve: {server: ServerResolverService}},
      {path: ':id/edit', component: EditServerComponent, canDeactivate: [CanDeactivateGuard]},
    ]},
  {path: 'users', component: UsersComponent},
  {path: '**', component: PageNotFoundComponent},
]

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

}

Finally in the ServerComponent, we can subscribe the server data.

Observable


What's the observable

Observable has various of a veriety of data like (user input) Events, Http Request, Triggerd in Code and We can trigger the Observable with Observer like below.

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit, OnDestroy {
  private firstObsSubscription: Subscription;

  constructor() {
  }

  ngOnInit() {

    this.firstObsSubscription = interval(1000).subscribe(count => {
      console.log(count);
    })

... skip ...

in the example, the interval of rxjs is observable which can be subcribed and if you come to the AppCopoment view, It woulb be executed in the console of user's browser.

You should destroy the observable when you finish your job with this. Otherwise it will lead memory leak.

... skip ...

  ngOnDestroy(): void {
    this.activatedSub.unsubscribe();
  }

... skip ...

Building Custom Observable

in this section we are going to make our custom observable.

... Skip ...    

  ngOnInit() {
    
    // Let's make our custom observable
    const customIntervalObservable = Observable.create(observer => {
      let count = 0;
      setInterval(() => {
        observer.next(count);
        if (count == 2) {
          observer.complete();
        }
        if (count > 3) {
          observer.error(new Error('Count is greater 3!'));
        }
        count++;
      }, 1000)
    });

    this.firstObsSubscription = customIntervalObservable.subscribe(data =>{
      console.log(data);
    }, error => {
      console.log(error.message);
    }, () => {
      console.log('Completed!')
    })
    
... Skip ...    

We made Observable with create function which will be triggerd from Observer. this observable has setInterval function executed next, error, complete functions of the Observable. firstObsSubscription subscribes the customIntervalObservable to use the data emitted by the Observable which changes the count located in observable.next(count); per one second. if count become 2, the Observable is finished and executes console.log('Completed!') at the customIntervalObservable.subscribe. It will be executed when the logic of Observable is ended without any erorrs. if the count is bigger than 3, error occures and Observable is finished. when it comes to error, the observer.complete(); is not working.

Operators

We can manage the data we received by the Observable with Operator of rxjs.


this.firstObsSubscription = customIntervalObservable
      .pipe(filter(data => {
          return data > 0;
        })
        , map((data: number) => {
          return 'Round: ' + (data + 1);
        })).subscribe(
        data => {
          console.log(data);
        }, error => {
          console.log(error.message);
          alert(error.message)
        }, () => {
          console.log('Completed!');
        });

We should add the pipe before subscribe to create, read, update, delete or filter the data Observable sent to the Observer. We can do anything you want in the operator pipe. In the pipe operator, We can filter and edit the data and then the data will go through subscribe. Therefore the data processed in subscribe will be below because the data is filtered and edited with pipe operators.

Subjects

Subject is type of Observable but It's little different with Observable We learned.

Observable works as Unicast and Subject works as multicast. Observable need to be subscribed to trigger some of works located in the Observable with Observer.

But Subject doesn't need Observer to trigger the Observable because Subject is Observable and Observer at the same time. so Subeject can call the next, error, complete functions out of the code without Observer because it had the Observer already.

We are going to change @Output value to Subject in the below example.

user.service.ts

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

@Injectable({
  providedIn: 'root'
})
export class UserService {
  activateEmitter = new EventEmitter<boolean>();
  
  constructor() { }
}

user.component.ts


import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import {UserService} from "../user.service";

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

  constructor(private route: ActivatedRoute,
              private userService: UserService) {
  }

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

  onActivate() {
    this.userService.activateEmitter.emit(true);

  }
}

user.component.html

<p>User with <strong>ID {{ id }}</strong> was loaded</p>
<button class="btn btn-primary" (click)="onActivate()">Activate</button>

app.component.ts

import {Component, OnDestroy, OnInit} from '@angular/core';
import {UserService} from "./user.service";
import {Subscription} from "rxjs";

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, OnDestroy {
  userActivated = false;
  private activatedSub: Subscription;
  constructor(private userService: UserService) {}

  ngOnInit() {
    this. activatedSub = this.userService.activateEmitter.subscribe(didActivate => {
      this.userActivated = didActivate;
      })
  }

  ngOnDestroy(): void {
    this.activatedSub.unsubscribe();
  }
}

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">
      <a routerLink="/">Home</a> |
      <a [routerLink]="['user', 1]">
        User 1
      </a>
      |
      <a [routerLink]="['user', 2]">
        User 2
      </a>
    </div>
  </div>
  <hr />
  <p *ngIf="userActivated">Activated!</p>
  <hr />
  <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>

Above example indicates the usage of @Output value to send the emmited data activateEmitter and AppComponent subscribes the activateEmitter with userService. We can change this logic with Subject.

user.service.ts

import {EventEmitter, Injectable} from '@angular/core';
import {Subject} from "rxjs";

@Injectable({
  providedIn: 'root'
})
export class UserService {
  // activateEmitter = new EventEmitter<boolean>();
  activateEmitter= new Subject<boolean>();
  
  constructor() { }
}

user.component.ts

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import {UserService} from "../user.service";

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

  constructor(private route: ActivatedRoute,
              private userService: UserService) {
  }

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

  onActivate() {
    this.userService.activateEmitter.next(true);
  }
}

As I said We can use next with activateEmitter.next(true) afterwards it triggered to transfer the boolean data to activateEmitter located in UserService.

Form

Template Driven Approach

Angular infers the From Object from the DOM.

In order to use forms' input data as javascript object in our Angular application, we should use @ViewChild annotation. we added #f="ngForm" for this and used it at the component of the template. after that we can use the forms' data as javascript objects.

to make Agnaulr application know the forms' data, we added ngModel in the input or select tag and various options are used in the tags. required option indicates we should type the form essentially.

to control a specific tag data, we should add template variable like #email=ngModel. we can access the email data directrly in the template level with the template variable. we can validate the data with the template variable also.

for advanced user experience, we can use css functions which can explain or express the state of the forms' data user is typing or user has typed.

app.component.ts

import {Component, ViewChild} from '@angular/core';
import {NgForm} from "@angular/forms";

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  @ViewChild('f') signupForm: NgForm;
  defaultQuestion = 'pet';
  answer: string = '';
  genders = ['male', 'female'];

  user = {
    username: '',
    email: '',
    secretQuestion: '',
    answer: '',
    gender: '',
  };

  submitted = false;

  suggestUserName() {
    const suggestedName = 'Superuser';

    // It's not a best practice because the code is long
    // And we should change other things not related to the username

    // this.signupForm.setValue({
    //   userData: {
    //     username: suggestedName,
    //     email: ''
    //   },
    //   secret: 'pet',
    //   questionAnswer: '',
    //   gender: 'mail',
    // })

    // this way is better than before, because we don't have to override others
    this.signupForm.form.patchValue({
      userData: {
        username: suggestedName,
      }
    })

  }

  // Below indicates Template-driven approach

  // The way to access javascript DOM Object of the form we wrote at the view page
  // onSubmit(f: NgForm) {
  //   console.log(f)
  // }

  // We can access the form data with @ViewChild like below
  // We can validate the user's inputs with the NgForm object
  // We should validate it at front-end and back-end always
  // There are many types of validators provided by Angular : https://angular.io/api/forms/Validators

  onSubmit() {
    console.log(this.signupForm);
    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;
    this.submitted = true;

    this.signupForm.reset();
  }
}

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">
      <form (ngSubmit)="onSubmit()" #f="ngForm">
        <!--        You can group the user-data inputs with `ngModelGroup`-->
        <!--        After that, You can check the `userData` in the From object-->
        <div id="user-data"
             ngModelGroup="userData"
             #userData="ngModelGroup"
        >
          <div class="form-group">
            <label
              for="username">Username</label>
            <input type="text"
                   id="username"
                   class="form-control"
                   ngModel
                   name="username"
                   required>
          </div>
          <button class="btn btn-default"
                  type="button"
                  (click)="suggestUserName()">
            Suggest an Username
          </button>
          <div class="form-group">
            <label for="email">Mail</label>
            <input type="email"
                   id="email"
                   ngModel
                   name="email"
                   class="form-control"
                   required
                   email
                   #email="ngModel">
            <span class="help-block"
                  *ngIf="!email.valid && email.touched"
            >
              Please enter a valid email!</span>
            <!--            the local value "email" exposes the form object's control information about Email-->
          </div>
        </div>
        <p
          *ngIf="!userData.valid && userData.touched"
          class="help-block"
        >User Data is invalid!</p>

        <div class="form-group">
          <label for="secret">Secret Questions</label>
          <!--          If you use [(ngModel)] in there, you can choose the default value we wrote-->
          <select id="secret"
                  [(ngModel)]="defaultQuestion"
                  name="secret"
                  class="form-control">
            <option value="pet">Your first Pet?</option>
            <option value="teacher">Your first teacher?</option>
          </select>
        </div>
        <div class="form-group">
          <textarea name="questionAnswer"
                    rows="3"
                    [(ngModel)]="answer"
                    class="form-control"></textarea>
        </div>

        <div class="radio"
             *ngFor="let gender of genders">
          <label>
            <input
              type="radio"
              name="gender"
              ngModel
              required
              [value]="gender">
            {{ gender }}
          </label>
        </div>

        <p>Your reply : {{ answer }}</p>
        <button class="btn btn-primary"
                [disabled]="!f.valid"
                type="submit">Submit
        </button>
      </form>
    </div>
  </div>
  <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 : {{ user.secretQuestion }} </p>
      <p>Answer : {{ user.answer }}</p>
      <p>Gender : {{ user.gender }}</p>
    </div>
  </div>
  <hr>
  <app-form-pratice></app-form-pratice>
</div>

app.component.css


.container {
  margin-top: 30px;
}

input.ng-invalid.ng-touched {
  border: 1px solid red;
}

Reactive Arpproach

Form is created programmatically and synchronized with the DOM.

In this section, we can make forms with the reactive approach unlike the template driven approch, the reactive approach makes forms with typescript code. we need to add ReactiveFormModule in our app.module.ts before you use ReactiveForm.

We should declare FormGroup and FormControl at a ngOnInit method. those are the forms' data we want to control. we made a value signUpForm and initialized this at ngOnInit method. in order to apply the FormGroup and FormControl in the template of the component, we added the signUpForm as FormGroup name and the forms' data like username, email as FormControl name. after that we can check that the angular control the forms' data.

From now on, we can control the forms' data with various validators which include custrom validators and we can show up various messages for a user interface using the forms' data which made by javascript object.

You can also make various custom validators which work as an async and sync processes.

app.component.ts

import {Component, OnInit} from '@angular/core';
import {FormArray, FormControl, FormGroup, Validators} from "@angular/forms";
import {Observable} from "rxjs";

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  genders = ['male', 'female'];
  signupForm: FormGroup;
  forbiddenUsernames = ['superuser', 'root'];

  ngOnInit(): void {
    // the validators' return type should be Observable or Promise
    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)
    );
  }

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

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

  getControls() {
    return (<FormArray>this.signupForm.get('hobbies')).controls;
  }

  forbiddenNames(control: FormControl): {[s: string]: boolean} {
    if (this.forbiddenUsernames.indexOf(control.value) !== -1) {
      return {'nameIsForbidden': true};
    } return null;
    // return {'nameIsForbidden': false};
  //   you can tell angular the form is valid or not with above method
  //   you can use both return types 'null' or javascript object 'statement'
  }

  // async validator method
  forbiddenEmails(control: FormControl): Promise<any> | Observable<any> {
    return new Promise<any>((resolve, reject) => {
      setTimeout(() => {
        if (control.value === 'test@test.com') {
          resolve({'emailIsForbidden': true});
        } else {
          resolve(null);
        }
      }, 1500);
    });
  }
}

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">
      <!--   We should connect signupForm with formGroup of the template   -->
      <form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
        <!--   We should make angular know fromGroupName is 'userData' we declared at 'signupForm'     -->
        <div formGroupName="userData">
          <div class="form-group">
            <label for="username">Username</label>
            <!--  We should make angular know formControlName is 'username' -->
            <input
              type="text"
              id="username"
              class="form-control"
              formControlName="username"
            >

            <span *ngIf="!signupForm.get('userData.username').valid && signupForm.get('userData.username').touched">
              <span class="help-block"
                    *ngIf="signupForm.get('userData.username').errors['nameIsForbidden']">
                this name is invalid
              </span>
              <span class="help-block"
                  *ngIf="signupForm.get('userData.username').errors['required']">
                this username field is required.
              </span>
            </span>

          </div>
          <div class="form-group">
            <label for="email">email</label>
            <!--  We should make angular know formControlName is 'email' -->
            <input
              type="text"
              id="email"
              class="form-control"
              formControlName="email"
            >
            <span class="help-block"
                  *ngIf="!signupForm.get('userData.email').valid && signupForm.get('userData.email').touched"
            >Please enter a valid email!</span>
          </div>
        </div>

        <div class="radio" *ngFor="let gender of genders">
          <label>
            <!--  We should make angular know formControlName is 'gender' -->
            <input
              type="radio"
              [value]="gender"
              formControlName="gender"
            >{{ gender }}
          </label>
        </div>

        <!--   we should make angular know the formArrayName is 'hobbies' we declared at signupForm-->
        <div formArrayName="hobbies">
          <h4>Your Hobbies</h4>
          <button
            class="btn btn-info"
            type="button"
            (click)="onAddHobby()"
          >
            Add Hobby
          </button>
          <div
            class="form-group"
            *ngFor="let hobbyControl of getControls(); let i = index">
            <input type="text" class="form-control" [formControlName]="i">
          </div>
        </div>
        <span class="help-block"
              *ngIf="!signupForm.valid && signupForm.touched"
        >Please enter a valid data!</span> <br>
        <button class="btn btn-primary" type="submit">Submit</button>
      </form>

      <br><hr>
      <app-reactive-form-practice></app-reactive-form-practice>
    </div>
  </div>
</div>

app.component.css

.container {
  margin-top: 30px;
}

input.ng-invalid.ng-touched {
  border: 1px solid red;
}

Pipe


We can transform strings, currency amounts, dates and other data for display with pipes. If we want to express date type data to user, we can simply use this with DatePipe. the DatePipe provides various date types we want to customize. like DatePipe, we can change strings to uppercase with uppercase.

We can make custom pipes like shorten and filter pipes. If you want to execute the fitler pipe as async mode, you should add pure: false option in the pipe decoration of filter pipe.

We used Promise obejct and added this as mustache at template but Angular doesn't know this is Promise type. We should make Angular know that type is Promise to make Angular subscribe the Promise object to express appStatus with Async mode. We should add the pipes in the declaration section of AppModule.

app.component.ts

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

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  appStatus= new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('stable');
    }, 2000)
  })
  servers = [
    {
      instanceType: 'medium',
      name: 'Production',
      status: 'stable',
      started: new Date(15, 1, 2017)
    },
    {
      instanceType: 'large',
      name: 'User Database',
      status: 'stable',
      started: new Date(15, 1, 2017)
    },
    {
      instanceType: 'small',
      name: 'Development Server',
      status: 'offline',
      started: new Date(15, 1, 2017)
    },
    {
      instanceType: 'small',
      name: 'Testing Environment Server',
      status: 'critical',
      started: new Date(15, 1, 2017)
    }
  ];

  filterStatus = '';

  // Pipe API
  // https://angular.io/api?query=pipe
  getStatusClasses(server: { instanceType: string, name: string, status: string, started: Date }) {
    return {
      'list-group-item-success': server.status === 'stable',
      'list-group-item-warning': server.status === 'offline',
      'list-group-item-danger': server.status === 'critical'
    };
  }

  onAddServer() {
    this.servers.push({
      instanceType: 'small',
      name: 'New Server',
      status: 'stable',
      started: new Date(15, 1, 2017)
    })
  }
}

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">
      <input type="text" [(ngModel)]="filterStatus">
      <br>
      <button class="btn btn-primary" (click)="onAddServer()">
        Add Server
      </button>
      <br>
<!--      Angular doesn't know the 'appStatus' Promise,
so we should make it know that using 'async' pipe option then Angular
would subscribes the Promise object and response as async mode-->
      <h2>Status : {{ appStatus | async }}</h2>
      <hr>
      <ul class="list-group">
        <li
          class="list-group-item"
          *ngFor="let server of servers | filter: filterStatus:'status'"
          [ngClass]="getStatusClasses(server)">
          <span
            class="badge">
            {{ server.status }}
          </span>
          <!-- "shorten" is a custom pipe -->
          <strong>{{ server.name | shorten: 15 }}</strong> |
          {{ server.instanceType | uppercase }} |
          {{ server.started | date: 'fullDate' | uppercase }}
        </li>
      </ul>
    </div>
  </div>
</div>
<hr>
<app-assignment></app-assignment>

shorten.pipe.ts

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

@Pipe({
  name: 'shorten',
})
export class ShortenPipe implements PipeTransform{

  // Rest parameters
  // https://yamoo9.gitbook.io/typescript/ts-vs-es6/spread-default-reset
  // transform(value: any, ...args: any[]): any {
  // }

  transform(value: any, limit: number) {
    if (value.length > limit) {
      return value.substr(0, limit) + ' ...';
    }
    return value;
  }
}

filter.pipe.ts

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

// If you want to use this pipe as async mode, you should add pure: false option in the pipe decoration.
// But this option would lead the user to performance issue.
@Pipe({
  name: 'filter',
  pure: false,
})
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;
  }

}

Http Request

We can make http requests with Angular. it's really useful when you need to request and response with RestAPI. For example you can request a backend server to gather some data we need with RestAPI (like Create, Read, Update, Delete, ..). Let's make the http request.

At first we made a firebase database called Realtime database. we can send our http requests to the database because the database provides RestAPI function in default.

We made PostService to make http requests contain createAndStorePost, fetchPosts, onClearPosts methodes.

in the createAndStorePost we can store our input data with post method. and the HttpClident is made in observable so we can subscribe that and show responseData on console log. if we don't subscribe the HttpClient, it would be not working because it would not control the response and then Angular regards the HttpClient not be used. we can specify the response type with < {name: string} >. it's totally optional but recommended. after that, I used my firebase url to store my data with post request and made custom headers and params. We can make multiple params also and specify responseType what we want. we wrote responseData with console.log in the subscribe context and change the error subject to the error.message if the error occurs.

In the fetchPosts, We used get method to require some data we stored before. If you use pipe you can change the reponse data to what you want. in the fetchPosts we used map to change the responseData to what we want. we made postsArray made by Post[] type. the responseData has key so we check that with responseData.hasOwnProperty(key) if it's true, we push the data({responseData[key], id: key}) into postsArray. we added custom field id in the example. catchError context is useful when you need to control error handling (like sending a request to handle the error..). if you subscribe the fetchPosts in a component wihch uses the method, you can return the HttpClient directly unlike createAndStorePost which subscribe the HttpClient in the service method. but you have to subscribe fetchPosts in a component certainly.

In the onClearPosts, we used observe it gives us events or response.. and we can check the response data with tap. tap executes some codes we want without change the response data. so we can write the log on console.log with tap function.

app.component.html

<div class="container">
  <div class="row">
    <div class="col-xs-12 col-md-6 col-md-offset-3">
      <form #postForm="ngForm" (ngSubmit)="onCreatePost(postForm.value)">
        <div class="form-group">
          <label for="title">Title</label>
          <input
            type="text"
            class="form-control"
            id="title"
            required
            ngModel
            name="title"
          />
        </div>
        <div class="form-group">
          <label for="content">Content</label>
          <textarea
            class="form-control"
            id="content"
            required
            ngModel
            name="content"
          ></textarea>
        </div>
        <button
          class="btn btn-primary"
          type="submit"
          [disabled]="!postForm.valid"
        >
          Send Post
        </button>
      </form>
    </div>
  </div>
  <hr />
  <div class="row">
    <div class="col-xs-12 col-md-6 col-md-offset-3">
      <button class="btn btn-primary" (click)="onFetchPosts()">
        Fetch Posts
      </button>
      |
      <button
        class="btn btn-danger"
        [disabled]="loadedPosts.length < 1"
        (click)="onClearPosts()"
      >
        Clear Posts
      </button>
    </div>
  </div>
  <div class="row">
    <div class="col-xs-12 col-md-6 col-md-offset-3">
      <p *ngIf="loadedPosts.length < 1 && !isFetching">No posts available!</p>
      <ul class="list-group" *ngIf="loadedPosts.length >= 1 && !isFetching">
        <li class="list-group-item" *ngFor="let post of loadedPosts">
          <h3>{{ post.title }}</h3>
          <p>{{ post.content }}</p>
        </li>
      </ul>
      <p *ngIf="isFetching && !error">Loading...</p>
      <div class="alert alert-danger" *ngIf="error">
        <h1>An Error Occurred!</h1>
        <p>{{ error }}</p>
      </div>
    </div>
  </div>
</div>

app.component.ts

import {Component, OnDestroy, OnInit} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {map} from "rxjs/operators";
import {Post} from "./post.model";
import {PostsService} from "./posts.service";
import {Subscription} from "rxjs";

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, OnDestroy {
    loadedPosts: Post[] = [];
    isFetching = false;
    error = null;
    errorSub: Subscription;

    constructor(private http: HttpClient, private postsService: PostsService) {
    }

    ngOnInit() {
        this.errorSub = this.postsService.error.subscribe(
            error => {
                this.error = error;
            }
        )
        // this.fetchPosts();
        this.isFetching = true;
        this.postsService.fetchPosts().subscribe(posts => {
            this.isFetching = false;
            this.loadedPosts = posts;
        }, err => {
            this.isFetching = false;
            this.error = err.message;
        })

    }

    onCreatePost(postData: { title: string; content: string }) {
        // Send Http request
        // console.log(postData);
        // this.http.post< {name: string } >('https://ng-complete-guide-b0c9e-default-rtdb.asia-southeast1.firebasedatabase.app/posts.json',
        //   postData).subscribe(
        //     responseData => {
        //       console.log(responseData);
        //     }
        // )
        // Change the http request to service.
        this.postsService.createAndStorePost(postData['title'], postData['content']);
    }

    onFetchPosts() {
        // Send Http request
        // this.fetchPosts();
        this.isFetching = true;
        this.postsService.fetchPosts().subscribe(posts => {
            this.isFetching = false;
            this.loadedPosts = posts;
        }, err => {
            this.isFetching = false;
            this.error = err.message;
        })


    }

    onClearPosts() {
        // Send Http request
        this.postsService.onClearPosts().subscribe(
            () => {
                this.loadedPosts = [];
            }
        );
    }

    ngOnDestroy(): void {
        this.errorSub.unsubscribe();
    }

    // private fetchPosts() {
    //   this.isFetching = true;
    //   this.http
    //     .get<{[key: string]: Post}>('https://ng-complete-guide-b0c9e-default-rtdb.asia-southeast1.firebasedatabase.app/posts.json')
    //     .pipe(
    //       map(responseData => {
    //         const postsArray: Post[] = [];
    //         for (const key in responseData) {
    //           if (responseData.hasOwnProperty(key)) {
    //             // 펼침 연산자 : https://cotak.tistory.com/72
    //             postsArray.push({...responseData[key], id: key});
    //           }
    //         }
    //         return postsArray;
    //       })
    //     )
    //     .subscribe(posts => {
    //       console.log(posts);
    //       this.isFetching = false;
    //       this.loadedPosts = posts;
    //     }
    //   )
    // }
    onHandleError() {
      this.error = null;
    }
}

posts.service.ts

import {Injectable} from '@angular/core';
import {HttpClient, HttpEventType, HttpHeaders, HttpParams} from "@angular/common/http";
import {Post} from "./post.model";
import {catchError, map, tap} from "rxjs/operators";
import {Observable, Subject, throwError} from "rxjs";

@Injectable({
  providedIn: 'root'
})
export class PostsService {

  constructor(private http: HttpClient) { }

  // loadedPosts = new Subject<Post[]>();
  error = new Subject<string>();
  createAndStorePost( title: string, content: string ) {
    const postData: Post = {title: title, content: content};
    // < {name : string} > means we will receive the response type {name: string}. it's totally optional but recommended
    this.http.post< {name: string } >('https://ng-complete-guide-b0c9e-default-rtdb.asia-southeast1.firebasedatabase.app/posts.json',
      postData,
      {
        // You can check the full response of our request like headers, status code, body..
        // observe: "response",
      }
    ).subscribe(
        responseData => {
          console.log(responseData);
        }, error => {
          this.error.next(error.message);
      }
    )
  }

  // at the below, we split the subscribe method to control the data we return.
  // in the past, we used subject to control changed data but this time we use another way to solve it.
  fetchPosts(): Observable<Post[]> {
    let searchParams = new HttpParams();
    searchParams = searchParams.append('print', 'pretty');
    searchParams = searchParams.append('custom', 'key');
    return this.http
      .get<{[key: string]: Post}>(
        'https://ng-complete-guide-b0c9e-default-rtdb.asia-southeast1.firebasedatabase.app/posts.json',
        {
          headers: new HttpHeaders({'Custom-Header': 'Hello'}),
          // params: new HttpParams().set('print', 'pretty'),
          // If you want to apply multiple params, you can use below method.
          params: searchParams,
          responseType: "json",
        }
      )
      .pipe(
        map(responseData => {
          const postsArray: Post[] = [];
          console.log(responseData)
          for (const key in responseData) {
            if (responseData.hasOwnProperty(key)) {
              // 펼침 연산자 : https://cotak.tistory.com/72
              postsArray.push({...responseData[key], id: key});
            }
          }
          return postsArray;
        }), catchError(errorRes => {
            // You can make logics when user faces the error
            // sending error message to server or redirect user to another page..
            console.log(errorRes);
            return throwError(errorRes);
          })
      )
    //   .subscribe(posts => {
    //     // console.log(posts);
    //   }
    // )
  }

  onClearPosts() {
    return this.http
      .delete('https://ng-complete-guide-b0c9e-default-rtdb.asia-southeast1.firebasedatabase.app/posts.json',
        {
          observe: 'events',
          responseType: 'text',
        }).pipe(tap(
          events => {
            console.log(events);
            if (events.type === HttpEventType.Response) {
              console.log(events.body);
            }
            if (events.type === HttpEventType.Sent) {
              console.log('type: 0')
            }
          }
      ));
  }
}

interceptors

Interceptos sends outgoing request before passing it to the next interceptor in the chain. we can add or update the request data in the interceptor. if we need add an authentication header, we can add it into all requests with the interceptors.

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import {HTTP_INTERCEPTORS, HttpClient, HttpClientModule} from '@angular/common/http';

import { AppComponent } from './app.component';
import {AuthInterceptorService} from "./auth-interceptor.service";

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, FormsModule, HttpClientModule],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthInterceptorService,
      multi: true,
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

auth-interceptor.service.ts


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


export class AuthInterceptorService implements HttpInterceptor{
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    console.log('Request is on its way');
    return next.handle(req);
  }
}

Afterwards, we can check the message Request is on its way in all of the requests like below.

If you want to add auth header into all of the reuqests, you can make it like below.

auth-interceptor.service.ts

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


export class AuthInterceptorService implements HttpInterceptor{
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    console.log('Request is on its way');
    console.log(req.url);
    const modifiedRequest = req.clone({
      headers: req.headers.append('Auth', 'xyz')
    });
    return next.handle(modifiedRequest).pipe(
      tap(event => {
        console.log(event);
        if (event.type === HttpEventType.Response) {
          console.log('Response arrived, body data: ');
          console.log(event.body);
        }
      })
    )
  }
}

also you can handle the response data with tap because it's made by observable.
you can use multiple interceptors like below.

logging-interceptor.service.ts

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

export class LoggingInterceptorService implements HttpInterceptor{
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    console.log('Outgoing request');
    console.log(req.url);

    return next.handle(req).pipe(
      tap(event => {
        if (event.type === HttpEventType.Response) {
          console.log('Incoming response');
          console.log(event.body);
        }
      })
    );
  }
}

auth=interceptor.service.ts

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


export class AuthInterceptorService implements HttpInterceptor{
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // console.log('Request is on its way');
    // console.log(req.url);
    const modifiedRequest = req.clone({
      headers: req.headers.append('Auth', 'xyz')
    });
    return next.handle(modifiedRequest).pipe(
      tap(event => {
        // console.log(event);
        if (event.type === HttpEventType.Response) {
          // console.log('Response arrived, body data: ');
          // console.log(event.body);
        }
      })
    )
  }
}

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import {HTTP_INTERCEPTORS, HttpClient, HttpClientModule} from '@angular/common/http';

import { AppComponent } from './app.component';
import {AuthInterceptorService} from "./auth-interceptor.service";
import {LoggingInterceptorService} from "./logging-interceptor.service";

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, FormsModule, HttpClientModule],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthInterceptorService,
      multi: true,
    },
    {
      provide: HTTP_INTERCEPTORS,
      useClass: LoggingInterceptorService,
      multi: true,
    },
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

the recipe application uses data storage service like below :
data-storage.service.ts

import {Injectable} from '@angular/core';
import {HttpClient, HttpParams} from '@angular/common/http';
import {exhaustMap, map, take, tap} from 'rxjs/operators';

import {Recipe} from '../recipes/recipe.model';
import {RecipeService} from '../recipes/recipe.service';
import {AuthService} from "../auth/auth.service";

@Injectable({providedIn: 'root'})
export class DataStorageService {
  constructor(private http: HttpClient,
              private recipeService: RecipeService,
              private authService: AuthService) {
  }

  storeRecipes() {
    const recipes = this.recipeService.getRecipes();
    this.http
      .put(
        'https://ng-course-recipe-book-b669a-default-rtdb.asia-southeast1.firebasedatabase.app/recipes.json',
        recipes
      )
      .subscribe(response => {
        console.log(response);
      });
  }

  fetchRecipes() {
    // return this.authService.user.pipe(
    //   // take means we get values which we specified and unsubscribe after it.
    //   // like below, we gather 1 user and unsubscribe this.
    //   take(1),
    //
    //   exhaustMap(user => {

    //     return this.http
    //       .get<Recipe[]>(
    //         'https://ng-course-recipe-book-b669a-default-rtdb.asia-southeast1.firebasedatabase.app/recipes.json'
    //         , {
    //           params: new HttpParams().set('auth', user.token)
    //         }
    //       );
    //   }), map(recipes => {
    //     return recipes.map(recipe => {
    //       return {
    //         ...recipe,
    //         ingredients: recipe.ingredients ? recipe.ingredients : []
    //       };
    //     });
    //   }),
    //   tap(recipes => {
    //     this.recipeService.setRecipes(recipes);
    //   })
    // )

    return this.http
      .get<Recipe[]>(
        'https://ng-course-recipe-book-b669a-default-rtdb.asia-southeast1.firebasedatabase.app/recipes.json'
      ).pipe(map(recipes => {
          return recipes.map(recipe => {
            return {
              ...recipe,
              ingredients: recipe.ingredients ? recipe.ingredients : []
            };
          });
        }),
        tap(recipes => {
          this.recipeService.setRecipes(recipes);
        })
      )

    // return this.http
    //   .get<Recipe[]>(
    //     'https://ng-course-recipe-book-b669a-default-rtdb.asia-southeast1.firebasedatabase.app/recipes.json'
    //   )
    //   .pipe(
    //     map(recipes => {
    //       return recipes.map(recipe => {
    //         return {
    //           ...recipe,
    //           ingredients: recipe.ingredients ? recipe.ingredients : []
    //         };
    //       });
    //     }),
    //     tap(recipes => {
    //       this.recipeService.setRecipes(recipes);
    //     })
    //   )
  }
}

Authentication

You can make an authentication process with Angular. with this authentication function, you can make users login Angular web application. when users login to the Angular, the Angular will send login request with id and password to a backend system which checks the reuqest is correct or not. you should add the endpoint of the auth component in routing module with auth.

You can make form auth component which has signup and login forms users can login or sginup to. Auth component will send the request data to an Auth service which has various function related to authentication. the Auth service has signup, login, autoLogin, autoLogout methods which use HttpClient library to communicate with a backend application connected with the Angular application. in the signup, login methods, you can use observable and pipe function to request and response the login or signup data and alter the response data. pipe function is very efficient to alter the response data as it is the observable response data. if you want to control error or alter, update the response data, you can do that with pipe function. there are so many techniques to use the pipe function.

After login to the Angular application, you should add a login token which responsed from the backend application to all of reuqests which user make with an interceptor function. the interceptor can add headers or parameters. the intercept method use pipe with take(1) which takes 1 data and exhaustMap which make the modifiedReq that has auth token in the header of the request. you should add provider of the interceptor in the app module of the Angular.

What's the exhaustMap?

auth.component.html

<div class="row">
  <div class="col-xs-12 col-md-6 col-md-offset-3">
    <div class="alert alert-danger" *ngIf="error">
      <p>
        {{ error }}
      </p>
    </div>
    <div *ngIf="isLoading" style="text-align: center">
      <app-loading-spinner></app-loading-spinner>
    </div>
    <form #authForm="ngForm" (ngSubmit)="onSubmit(authForm)" *ngIf="!isLoading">
      <div class="form-group">
        <label for="email">Email</label>
        <input type="email"
               id="email"
               class="form-control"
               ngModel
               required
               email
               name="email">
      </div>
      <div class="form-group">
        <label for="password">Password</label>
        <input type="password"
               id="password"
               class="form-control"
               ngModel
               required
               minlength="6"
               name="password">
      </div>
      <div>
        <button class="btn btn-primary"
                [disabled]="!authForm.valid"
                type="submit">{{ isLoginMode ? 'Login' : 'Sign Up' }}
        </button> |
        <button class="btn btn-primary"
                (click)="onSwitchMode()"
                type="button">{{ isLoginMode ? 'Switch to Sign Up' : 'Switch to Login' }}</button>
      </div>
    </form>
  </div>
</div>

auth.component.ts

import {Component} from "@angular/core";
import {NgForm} from "@angular/forms";
import {AuthResponseData, AuthService} from "./auth.service";
import {Observable} from "rxjs";
import {Router} from "@angular/router";

@Component({
  selector: 'app-auth',
  templateUrl: './auth.component.html'
})
export class AuthComponent {
  isLoginMode = true;
  isLoading = false;
  error: string = null;

  constructor(private authService: AuthService,
              private router: Router) {
  }

  onSwitchMode() {
    this.isLoginMode = !this.isLoginMode;
  }
  onSubmit(form: NgForm) {
    if (!form.valid) {
      return;
    }

    const email = form.value.email;
    const password = form.value.password;

    // repeat code is improved with authObs
    let authObs: Observable<AuthResponseData>;

    this.isLoading = true;
    if (this.isLoginMode) {
      authObs = this.authService.login(email, password)
    } else {
      authObs = this.authService.signup(email, password)
    }

    // authObs works like below! (we don't use repeated code anymore)
    authObs.subscribe(
      resData => {
        console.log(resData);
        this.isLoading = false;
        this.error = null;
        this.router.navigate(['/recipes']);
      }, errorMessage => {
        console.log(errorMessage);
        this.error = errorMessage;
        this.isLoading = false;
      }
    )

    form.reset();

    this.authService.user.subscribe(
      user => {
        console.log(user);
      }
    );
  }
}

app-routing.module.ts

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 {RecipeStartComponent} from './recipes/recipe-start/recipe-start.component';
import {RecipeDetailComponent} from './recipes/recipe-detail/recipe-detail.component';
import {RecipeEditComponent} from './recipes/recipe-edit/recipe-edit.component';
import {RecipesResolverService} from "./recipes/recipes-resolver.service";
import {AuthComponent} from "./auth/auth.component";
import {AuthGuard} from "./auth/auth.guard";


const appRoutes: Routes = [
  {path: '', redirectTo: '/recipes', pathMatch: 'full'},
  {
    path: 'recipes',
    component: RecipesComponent,
    canActivate: [AuthGuard],
    children: [
      {path: '', component: RecipeStartComponent},
      {path: 'new', component: RecipeEditComponent},
      {path: ':id', component: RecipeDetailComponent, resolve: [RecipesResolverService]},
      {path: ':id/edit', component: RecipeEditComponent, resolve: [RecipesResolverService]},
    ]
  },
  {path: 'shopping-list', component: ShoppingListComponent},
  {path: 'auth', component: AuthComponent}
];

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

}

auth-interceptor.service.ts

import {Injectable} from "@angular/core";
import {HttpEvent, HttpHandler, HttpInterceptor, HttpParams, HttpRequest} from "@angular/common/http";
import {Observable} from "rxjs";
import {AuthService} from "./auth.service";
import {exhaustMap, take} from "rxjs/operators";

@Injectable()
export class AuthInterceptorService implements  HttpInterceptor{
  constructor(private authService: AuthService) {
  }
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return this.authService.user.pipe(
      take(1),
      exhaustMap(user=> {
        if (!user) {
          return next.handle(req);
        }

        const modifiedReq = req.clone({
          params: new HttpParams().set('auth', user.token)
        });
        return next.handle(modifiedReq);
      })
    );
  }
}

app.module.ts

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


import { AppComponent } from './app.component';
import { HeaderComponent } from './header/header.component';
import { RecipesComponent } from './recipes/recipes.component';
import { RecipeListComponent } from './recipes/recipe-list/recipe-list.component';
import { RecipeDetailComponent } from './recipes/recipe-detail/recipe-detail.component';
import { RecipeItemComponent } from './recipes/recipe-list/recipe-item/recipe-item.component';
import { ShoppingListComponent } from './shopping-list/shopping-list.component';
import { ShoppingEditComponent } from './shopping-list/shopping-edit/shopping-edit.component';
import { DropdownDirective } from './shared/dropdown.directive';
import { ShoppingListService } from './shopping-list/shopping-list.service';
import { AppRoutingModule } from './app-routing.module';
import { RecipeStartComponent } from './recipes/recipe-start/recipe-start.component';
import { RecipeEditComponent } from './recipes/recipe-edit/recipe-edit.component';
import { RecipeService } from './recipes/recipe.service';
import {DataStorageService} from "./shared/data-storage.service";
import {HTTP_INTERCEPTORS, HttpClientModule} from "@angular/common/http";
import {AuthComponent} from "./auth/auth.component";
import {LoadingSpinnerComponent} from "./shared/loading-spinner/loading-spinner.component";
import {AuthInterceptorService} from "./auth/auth-interceptor.service";


@NgModule({
  declarations: [
    AppComponent,
    HeaderComponent,
    RecipesComponent,
    RecipeListComponent,
    RecipeDetailComponent,
    RecipeItemComponent,
    ShoppingListComponent,
    ShoppingEditComponent,
    DropdownDirective,
    RecipeStartComponent,
    RecipeEditComponent,
    AuthComponent,
    LoadingSpinnerComponent,
  ],
  imports: [
    BrowserModule,
    FormsModule,
    ReactiveFormsModule,
    AppRoutingModule,
    // It's really important to use HttpClient module!
    HttpClientModule,
  ],
  providers: [ShoppingListService, RecipeService,
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthInterceptorService,
      multi: true,
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Finally we should control the routing table users can visit or not with guard function. the guard check users are authenticated or not and if users are not login status the guard returns /auth url. if the users are authenticated the guard returns correct url users want to visit with login privilege. the guard should be added into the endpoints which need the login privilege of routing module.

auth.guard.ts


import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree} from "@angular/router";
import {Observable} from "rxjs";
import {Injectable} from "@angular/core";
import {AuthService} from "./auth.service";
import {map, take, tap} from "rxjs/operators";

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

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):
    Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    // return this.authService.user.pipe(map(user => {
    //   // if user is valid return true, not valid return false.
    //   return !!user;
    // }), tap(isAuth => {
    //   if (!isAuth) {
    //     this.router.navigate(['/auth']);
    //   }
    // }))

    return this.authService.user.pipe(
      take(1),
      map(user => {
        // if user is valid return true, not valid return false.
        const isAuth = !!user;
        if (isAuth) {
          return true;
        }
        // it's more advanced version than above code.
        return this.router.createUrlTree(['/auth']);
      })
    )
  }

}

app-routing.module.ts


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 {RecipeStartComponent} from './recipes/recipe-start/recipe-start.component';
import {RecipeDetailComponent} from './recipes/recipe-detail/recipe-detail.component';
import {RecipeEditComponent} from './recipes/recipe-edit/recipe-edit.component';
import {RecipesResolverService} from "./recipes/recipes-resolver.service";
import {AuthComponent} from "./auth/auth.component";
import {AuthGuard} from "./auth/auth.guard";


const appRoutes: Routes = [
  {path: '', redirectTo: '/recipes', pathMatch: 'full'},
  {
    path: 'recipes',
    component: RecipesComponent,
    canActivate: [AuthGuard],
    children: [
      {path: '', component: RecipeStartComponent},
      {path: 'new', component: RecipeEditComponent},
      {path: ':id', component: RecipeDetailComponent, resolve: [RecipesResolverService]},
      {path: ':id/edit', component: RecipeEditComponent, resolve: [RecipesResolverService]},
    ]
  },
  {path: 'shopping-list', component: ShoppingListComponent},
  {path: 'auth', component: AuthComponent}
];

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

}

0개의 댓글

관련 채용 정보