Angular

From bibbleWiki
Jump to navigation Jump to search

Introduction

Angular Versions

  • Angular 1.0 is the old version of angular along with AngularJS
  • Angular, starts at 2 and skips 3 and is now at 9 is the version this page is about

Benefits

  • Compiles with ES6
  • Use modules
  • Built in Internationalisation and Accessibility
  • Comes with Router, http, forms, rxjs, etc
  • Supports Progress Web Apps, (Mobile, Web like React)
  • Server-side rendering (render page on server)
  • One-way data flow (like React update from parent down)
  • Dependency injection
  • Uses zone.js to detect change before rendering
  • Like React, multiple rendering targets, Browser/DOM, Server Side, Mobile Apps, Desktops

Angular CLI

When we build JavaScript we have to manage

  • Module Handling
  • Minifying
  • Shims (compatibility to legacy code)
  • Zone.js wrapper
  • Bundling
  • Transpilation, compiling to ESx (babel)

Then Angular CLI can

  • Create new application
  • New components/service/pipe
  • Serve up the Application
  • Linting
  • Testing
  • Building

Sample App

This is the sample app I built
The files for the example can be found at [1]

Architecture

  • Planning
  • Organizing Features
  • Structuring Components
  • Component Communication
  • Concurrent Communication
  • State management
  • Additional Considerations

Planning

Key Things To Think About

  • App Overview, what is for, what are the benefits
  • App Features,
  • Domain Security, are we using roles, tokens etc
  • Domain Rules, client side, server side or both
  • Logging, what will this look like, app insights etc
  • Service/Communication, generally HTTP but could be Web Sockets, Service bus
  • Data Model, what will this look like, do we need view models
  • Feature Components, who will components communicate
  • Shared Functionality, what are we going to be using

Example Data

App Overview

  • Overall Goals
    • Support viewing and editing customers
    • Support viewing orders
    • Secure customer edit form
  • Key Requirements
    • Display customers with card/grid option
    • Support filtering, sorting, and paging customers
    • Map customers (Google maps)
    • Customer editing support (CRUD)
    • Display orders with paging
    • Support login/logout with email/password

App Features

  • Customers Feature
    • Display customers
    • Support card/grid modes
    • Display customers on map
    • Support filtering/paging/sorting
  • Customer Feature
    • Create/Update/Delete customer entity
    • Display customer details/orders/map
    • Form provides validation
  • Orders Feature
    • Display orders
    • Support paging
  • Login/Logout Feature

Domain Security

  • Email/Password for initial release
  • Consider tokens for future release?
    • HttpInterceptor could be used to set auth header
    • What option will be used for token issuer?

Domain Rules

  • Each order must be associated with a customer
  • Order totals should be shown for a given customer
  • Customer edit form should validate data entry
  • User must login to access customer edit form (route guard)
  • Validate login credentials

Logging

  • Create an Angular service for logging
  • Consider using Azure AppInsights
    • Wrap existing AppInsights client SDK
    • Handle logging to different areas based on dev/stage/prod:
    • Console logger
    • AppInsights logger

Service/Communication

  • RESTful Service will be used (Node.js)
  • Angular Services
    • Data Services: Use HttpClient
    • Customers/Orders
    • Login/logout (auth)
    • Sorting service
    • Filtering service
    • Logger service
    • Mediator/EventBus if needed
  • HttpInterceptor will set token for HTTP requests if tokens used (GET/PUT/POST/DELETE/PATCH)

Data Model

  • Application models/interfaces:
    • Customer model/interface
    • Auth model/interface
    • No order editing so include with Customer?
  • Create a class or just use an interface on the client-side?

Feature Components

  • Customers
    • Display/filter/sort/page customers (need data service)
  • Customer
    • Create/read/update/delete customer (need data service and modal needed for deletes)
  • Orders
    • Display/page orders
  • Login
    • Login form and logout functionality (need auth service)

Shared Functionality

  • Toast/Growl (success/information)
  • Modal dialog
  • Google maps
  • Pager
  • Menu
  • Grid/Card (used more than once?)
  • 3rd Party, Ng Bootstrap, Angular Material etc

Angular Style Guide

This can be found here https://angular.io/guide/styleguide This provides,

  • Coding conventions
  • Naming Rules
  • Application Structure
  • Organizing Modules
  • Creating and using components
  • Creating and using services
  • Lifecycle hooks

Other Considerations

  • Accessibility
  • i18n (internationalization)
  • Testing
  • CI/CD, versioning, pipelines
  • APIs

Organizing Features

LIFE

Follow the LIFT appoach

  • Locate code quickly, good naming of folders, modules
  • Identify the code at a glance, good naming of files
  • Keep Flattest structure you can
  • Try to be DRY (Do not repeat yourself)

Organize Code

Reasons to not used Conventions Base

  • Follows strict name conventions
  • Related code may be separated
  • Can result in a lot of files in a folder

Reasons For Feature based

  • Features organized into there own folder
  • Features are self contained
  • Easy to find everything related to the feature

Core and Shared Modules

The examples provide said Core (or Common) module should have

  • Singleton services such as
    • Login, Error, logging

Shared module should have

  • Reusable components, pipes (formatters), directives

Making Core Imported Once

Seemed like a biggy on the course so here is how to do it. It also breaks up an very text driven section :)

@NgModule({
  imports:      [ CommonModule ],
  declarations: [ SomeComponent ],
  exports:      [ SomeComponent ],
})
export class CoreModule {

  constructor (@Optional() @SkipSelf() parentModule: CoreModule) {
    if (parentModule) {
      throw new Error(
        'CoreModule is already loaded. Import it in the AppModule only');
    }
  }

  static forRoot(config: UserServiceConfig): ModuleWithProviders {
    return {
      ngModule: CoreModule,
      providers: [SomeService]
    };
  }
}

And the author was so keen they provided their approach too

export class EnsureModuleLoadedOnceGuard {
   constructor(targetModule :any) {
      if (targetModule) {   
         throw new Error(`${targetModule.constructor.name} has already been loaded.
             Import this module in the AppModule only.`);
      }
   }
}

Create a Library in Angular

This can be done using the angular cli.

# Create library
ng new my-project
cd new my-project
ng g library my-lib
ng build my-lib
# Build and Publish
ng build my-lib
cd dist/my-lib
npm publish

Structuring Components

Container and Presentation Pattern

Like React or probably any web app it is important to separate the container from the presentation.
The one take away was around the routing which could be used to send email to colleague if available. E.g. https://www.acme.co.nz/person/1. Sometimes with containers this is not always available.

Change Detection

With react this is built in almost using state and the propagation of data to child components. With Angular, we use the change detection strategy. From reading the net it seemed to me that OnPush is the way to go for Container/Presentation unless there is a specific reason not to.

  • Default Change Detection Strategy

By default, Angular uses the ChangeDetectionStrategy.Default change detection strategy. This default strategy checks every component in the component tree from top to bottom every time an event triggers change detection (like user event, timer, XHR, promise and so on). This conservative way of checking without making any assumption on the component’s dependencies is called dirty checking. It can negatively influence your application’s performance in large applications which consists of many components

  • OnPush Change Detection Strategy

We can switch to the ChangeDetectionStrategy.OnPush change detection strategy by adding the changeDetection property to the component decorator metadata:

@Component({
    selector: 'hero-card',
    changeDetection: ChangeDetectionStrategy.OnPush,
    template: ...
})
export class HeroCard {
    ...
}

This change detection strategy provides the possibility to skip unnecessary checks for this component and all it’s child components.

The GIF demonstrates skipping parts of the component tree by using the OnPush change detection strategy:
Using this strategy, Angular knows that the component only needs to be updated if:

  • the input reference has changed
  • the component or one of its children triggers an event handler
  • change detection is triggered manually
  • an observable linked to the template via the async pipe emits a new value

Component Inheritance

If we have common inputs and outputs, in Angular, you can use the extend keyword to reuse from a base component. Given the move to functional components I am sure this is not as popular as the author suggested

export class Widget2Compoent extends BaseComponent implements OnInit {
   constructor() {
      super()
   }

   ngOnInit() {
   }
}

Component Communication

Event Bus vs Observable Service

Event Bus

  • Mediator pattern
  • Angular service acts as middleperson between components
  • Components don't know where the data is coming form by default
  • Loosely coupled
  • Relies on subject/observable

The essence of the Mediator Pattern is to "define an object that encapsulates how a set of objects interact". It promotes loose coupling by keeping objects from referring to each other explicitly, and it allows their interaction to be varied independently.Client classes can use the mediator to send messages to other clients, and can receive messages from other clients via an event on the mediator class.
Observable Service

  • Observable pattern
  • Angular service exposes observable directly to components
  • Components know where data coming from
  • Not as loosely coupled as event bus
  • Release on subject/observable

The Observer design pattern is one of the twenty-three well-known "Gang of Four" design patterns that describe how to solve recurring design problems to design flexible and reusable object-oriented software, that is, objects that are easier to implement, change, test, and reuse.

ReactiveX Subjects

There are four types of subjects in ReactiveX

  • PublishSubject (only values at subscription
  • BehaviorSubject (1 previous value)
  • ReplySubject (n Previous value)
  • AsyncSubject (last value)

PublishSubject

PublishSubject emits to an observer only those items that are emitted by the source Observable(s) subsequent to the time of the subscription.

BehaviorSubject (1 previous value)

When an observer subscribes to a BehaviorSubject, it begins by emitting the item most recently emitted by the source Observable (or a seed/default value if none has yet been emitted) and then continues to emit any other items emitted later by the source Observable(s). File:ReactiveX behavior.png

ReplySubject (n Previous value)

ReplaySubject emits to any observer all of the items that were emitted by the source Observable(s), regardless of when the observer subscribes.

AsyncSubject (last value)

An AsyncSubject emits the last value (and only the last value) emitted by the source Observable, and only after that source Observable completes. (If the source Observable does not emit any values, the AsyncSubject also completes without emitting any values.)

Event Bus

Example Event Bus Service

import { Injectable } from '@angular/core';
import { Subject, Subscription, Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';

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

    private subject$ = new Subject();

    on(event: Events, action: any): Subscription {
         return this.subject$
              .pipe(
                    filter((e: EmitEvent) => e.name === event),
                    map((e: EmitEvent) => e.value)
                  )
              .subscribe(action);
    }

    emit(event: EmitEvent) {
        this.subject$.next(event);
    }
}

export class EmitEvent {

  constructor(public name: any, public value?: any) { }

}

export enum Events {
  CustomerSelected
}

Example Usage of Event Bus

We raise an event using

  selectCustomer(cust: Customer) {
    // send to parent via output property
    // note: could use eventbus as well if desired but output property
    // would be the preferred method for passing data to am immediate parent
    this.customerSelected.emit(cust);
    // Send customer to any eventbus listeners listening for the CustomerSelected event
    this.eventbus.emit(new EmitEvent(Events.CustomerSelected, cust));
  }

We act on the event with

  ngOnInit() {
    //Example of using an event bus to provide loosely coupled communication (mediator pattern)
    this.eventbusSub = this.eventbus.on(Events.CustomerSelected, cust => (this.customer = cust));

    //Example of using BehaviorSubject to be notified when a service changes
    this.customersChangedSub = this.dataService.customersChanged$.subscribe(custs => (this.customers = custs));
  }

Concurrent Communication

State management

Additional Considerations

Components

Introduction


Components comprise of

  • Template
  • Class
  • Metadata

How An Application Starts

Here is the index.html which has the pm-root tag inside it to denote a component.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>APM</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <pm-root></pm-root>
</body>
</html>

Here is a sample app.module (possibly main) used.

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

import { AppComponent } from './app.component';

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

And finally the component we defined.

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

@Component({
  selector: 'pm-root',
  template: `
    <div>
      <h1>{{pageTitle}}</h1>
      <div>My First Component</div>
    </div>
    `
})
export class AppComponent {
  pageTitle: string = 'Angular: Getting Started 2';
}

Interfaces

Defining

We can define interfaces in Angular

export interface IProduct {
    productId: number;
    productName: string;
...
    calculateDiscount(percent: number): number;
}

Usage

So we just need to change our any definition in the product component to be IProduct

import { Component } from '@angular/core';
import { IProduct} from './product'

@Component({ 
  selector: 'pm-products',
  templateUrl: './product-list.component.html'
})
export class ProductListComponent {
    pageTitle: string = "Product List"
    imageWidth: number = 50;
    imageMargin: number = 2;
    showImage: boolean = false;
    listFilter: string = "cart"
    products: IProduct[] = [
        {
          "productId": 1,
          "productName": "Leaf Rake",
...

LifeCyle Hooks

Introduction

In Angular, every component has a life-cycle, a number of different stages it goes through. There are 8 different stages in the component lifecycle. Every stage is called as lifecycle hook event.

ngOnChanges

This event executes every time when a value of an input control within the component has been changed. Actually, this event is fired first when a value of a bound property has been changed. It always receives a change data map, containing the current and previous value of the bound property wrapped in a SimpleChange.

ngOnInit

This event initializes after Angular first displays the data-bound properties or when the component has been initialized. This event is basically called only after the ngOnChanges()events. This event is mainly used for the initialize data in a component.

ngDoCheck

This event is triggered every time the input properties of a component are checked. We can use this hook method to implement the check with our own logic check. Basically, this method allows us to implement our own custom change detection logic or algorithm for any component.

ngAfterContentInit

This lifecycle method is executed when Angular performs any content projection within the component views. This method executes when all the bindings of the component need to be checked for the first time. This event executes just after the ngDoCheck() method. This method is basically linked with the child component initializations.

ngAfterContentChecked

This lifecycle hook method executes every time the content of the component has been checked by the change detection mechanism of Angular. This method is called after the ngAfterContentInit() method. This method is also called on every subsequent execution of ngDoCheck(). This method is also mainly linked with the child component initializations.

ngAfterViewInit

This lifecycle hook method executes when the component’s view has been fully initialized. This method is initialized after Angular initializes the component’s view and child views. It is called after ngAfterContentChecked(). This lifecycle hook method only applies to components.

ngAfterViewChecked

This method is called after the ngAterViewInit() method. It is executed every time the view of the given component has been checked by the change detection algorithm of Angular. This method executes after every subsequent execution of the

ngAfterContentChecked()

This method also executes when any binding of the children directives has been changed. So this method is very useful when the component waits for some value which is coming from its child components.

ngOnDestroy

This method will be executed just before Angular destroys the components. This method is very useful for unsubscribing from the observables and detaching the event handlers to avoid memory leaks. Actually, it is called just before the instance of the component is finally destroyed. This method is called just before the component is removed from the DOM.

Nested Components

Introduction

We often want to put component within other component. The nested component receives information from it's input properties and talks to the container (parent) via events. In the example we create a

  • component,
  • style,
  • template

Component

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

@Component({
  selector: 'pm-star',
  templateUrl: './star.component.html',
  styleUrls: ['./star.component.css']
})
export class StarComponent implements OnChanges {
  @Input() rating = 0;
  starWidth = 0;
  @Output() ratingClicked: EventEmitter<string> =
    new EventEmitter<string>();

  ngOnChanges(): void {
    this.starWidth = this.rating * 75 / 5;
  }

  onClick(): void {
    this.ratingClicked.emit(`The rating ${this.rating} was clicked!`);
  }
}

Style

.crop {
  overflow: hidden;
}
div {
  cursor: pointer;
}

Template

<div class="crop"
     [style.width.px]="starWidth"
     [title]="rating"
     (click)="onClick()">
  <div style="width: 75px">
    <span class="fa fa-star"></span>
    <span class="fa fa-star"></span>
    <span class="fa fa-star"></span>
    <span class="fa fa-star"></span>
    <span class="fa fa-star"></span>
  </div>
</div>

Using the Component

To use the component we need to

  • add the component to the app module
  • add the tag to the template and pass the relevant parameters

Add to App Module

import { StarComponent } from './shared/star.component';
import { ConvertToSpacesPipe } from './shared/convert-to-spaces.pipe';
...

@NgModule({
  declarations: [
    AppComponent,
    ProductListComponent,
    ConvertToSpacesPipe,
    StarComponent,
  ],
  imports: [
    BrowserModule,
...

Add to the template

Note the starRating is bound to the @input property of the component. This is similar to props used in react.

   <td><pm-star [rating]='product.starRating'></pm-star></td>

Outputting from Nested Component

We can only use events to talk to the parent container but we can do this by creating events within the component. In the example we created

   @Output() ratingClicked: EventEmitter<string> = new EventEmitter<string>()

We can bind to this in the parent where ratingClicked is the name of the output function and OnRatingClicked is the parents function to execute.
In the parent template

            <td>
              <pm-star [rating]='product.starRating'
                       (ratingClicked)='onRatingClicked($event)'>
              </pm-star>
            </td>


In the parent class

     onRatingClicked(message: string): void {
        this.pageTitle = 'Product List Me: ' + message;
      }

Styles

Styles can be added either by adding an array of styles using the keyword styles or by referencing a style sheet using styleUrls.

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

import { IProduct } from './product';
import { ProductService } from './product.service';

@Component({
  templateUrl: './product-list.component.html',
  styleUrls: ['./product-list.component.css']
})
....

Example ./product-list.component.css

thead {
  color: #337AB7;
}

Templates

Inline Templates

We can define templates using double quotes on one line or by using back ticks.

  template: `
    <div>
      <h1>{{pageTitle}}</h1>
      <div>My First Component</div>
    </div>
    `

Linked Templates

In general it would look like using and linked template would be better. You simply reference the template using the templateUrl keyword instead of template.

Adding Styling Components

We used bootstrap and font-awesome. To make them available in angular we added the imports to the styles.css

@import "~bootstrap/dist/css/bootstrap.min.css";
@import "~font-awesome/css/font-awesome.min.css";

Creating the Template

We call the template the same name as the component. In the same app this was product-list.component.html. This starts with html

<div class='card'>
    <div class='card-header'>
      Page Title
    </div>
    <div class='card-body'>
      <div class='row'>
        <div class='col-md-2'>Filter by:</div>
        <div class='col-md-4'>
          <input type='text'/>
        </div>
      </div>
      <div class='row'>
        <div class='col-md-6'>
          <h4>Filtered by: </h4>
        </div>
      </div>
      <div class='table-responsive'>
        <table class='table'>
          <thead>
            <tr>
              <th>
                <button class='btn btn-primary'>
                  Show Image
                </button>
              </th>
              <th>Product</th>
              <th>Code</th>
              <th>Available</th>
              <th>Price</th>
              <th>5 Star Rating</th>
            </tr>
          </thead>
          <tbody>
          </tbody>
        </table>
    </div>
</div>

Creating the Component

So we create the component

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

@Component({
  selector: 'pm-products',
  templateUrl: './product-list.component.html'
})
export class ProductListComponent {
}

Add Component to App

Seems a popular error but whenever we use a component we must add it to the app. In this case it was app.modules.ts with an import. Note the import AND the add to declarations

import { ProductListComponent } from './products/product-list.component';
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';

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

Interpolation

We can use this to bind the properties to functions to the component in the template. E.g. This gets the property pageTitle from the class component

<div>{{pageTitle}}</div>

This calls foo() in the class

<div>{{getFoo()}}</div>

Directives

These are ways to control template logic. Below are the 3 of the key ones

  • NgIf—conditionally creates or destroys subviews from the template.
  • NgFor—repeat a node for each item in a list.
  • NgSwitch—a set of directives that switch among alternative views.

You can find the docs at [2]

ngIf

Example

   <table class='table' *ngIf='products && products.length'>

ngFor

Within javascript there is a for...of and and a for...in. The for..of iterates over the objects. The for..in loop iterates over the properties of an object. Example

          <tbody>
              <tr *ngFor="let product of products">
                  <td></td>
                  <td>{{product.productName}}</td>
                  <td>{{product.productCode}}</td>
                  <td>{{product.releaseData}}</td>
                  <td>{{product.price}}</td>
                  <td>{{product.starRating}}</td>
              </tr> 
          </tbody>

Data Binding & Pipes

Property Binding

Property binding is binding properties and should not be confused with the interpolation. Property binding is one-way. Note some of the values need product because they are from the variable in the for loop. The values such as imageWidth are a property of the component and do not need the component.

<td><img [src]='product.imageUrl'
         [title]='product.imageUrl' 
         [style.width.px]='imageWidth'
         [style.margin.px]='imageMargin'>
</td>

Event Binding

No surprises,
In the template define an event on the left and a function to call on the right.

<button class="btn btn-primary"
   (click)='toggleImage()'>
   Show Image
</button>

In the class create a function

export class ProductListComponent {
...
   // Generally at the end
   toggleImage(): void {
      this.showImage = !this.showImage
   }
}

Two-way Binding

To implement two-way binding we use ngModel. We will need to import the FormsModule to use this. We do this by specifying the module in the imports array as it is a third-party component not made by us.

import { ProductListComponent } from './products/product-list.component';
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';

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

Add Out Listfilter to the class

export class ProductListComponent {
    pageTitle: string = "Product List"
    imageWidth: number = 50;
    imageMargin: number = 2;
    showImage: boolean = false;
    listFilter: string = "cart"
...

Now we can add our filter to the page using the ngModel.

        <input type='text'
            [(ngModel)]="listFilter"/>
        </div>

Pipes

Introduction

Pipes are used to transform data such as bound properties. There are several built-in data types for dates, numbers,json, slice etc or build our own. As the name suggests, it is achieved by using the "|" character. Parameters can be passed by using the full colon.

{{product.price | currency:'USD':'symbol':'1.2-2' }}

Custom Pipes

We can build our own pipe by

  • implementing the PipeTransform interface on a class using the @Pipe decorator
  • adding the component to our app module
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'convertToSpaces'
})
export class ConvertToSpacesPipe implements PipeTransform {

  transform(value: string, character: string): string {
    return value.replace(character, ' ');
  }
}

Filtering

It is suggested that it is better to filter within the component rather than to have pipes which could do this for us. In the example code we

  • Added a variable to hold the filtered products
  • Added a getter/setter to the filter property to encapsulate access
  • Create a function to do the filtering
  • Add constructor to default values
  constructor() {
     this.filteredProducts = this.products
     this.listFilter = 'cart'
  }

  filteredProducts: IProduct[]

  _listFilter = '';
  get listFilter(): string {
    return this._listFilter;
  }
  set listFilter(value: string) {
    this._listFilter = value;
    this.filteredProducts = this.listFilter ? this.performFilter(this.listFilter) : this.products;
  }

  performFilter(filterBy: string): IProduct[] {
    filterBy = filterBy.toLocaleLowerCase();
    return this.products.filter((product: IProduct) =>
      product.productName.toLocaleLowerCase().indexOf(filterBy) !== -1);
  }

HTTP Management

Introduction

At Angular 7, the default implementation for Http client was rxJs 6.3.3.

Http Client

Set up

Angular provides its own Http client. This can be found in the httpclientmodule. You need to add the to your app in the bootstrap module, usually app.module.ts.

import { HttpClientModule } from '@angular/common/http';
...
  imports: [
    BrowserModule,
    AppRoutingModule,
    FormsModule,
    CoreModule,
    HttpClientModule,
...

Passing Headers

We can do this by creating an object and passing it the http request.

    this.http.get<Book>(`./api/books/${id}`, {
     
      headers: new HttpHeaders({
        'Accept': 'application/json',
        'Authorization': 'my-token',
      })      
      
    });

REST API

Introduction

The http client provides the API for the REST API functions

Add

  addBook(book: Book): Observable<Book> {

    return this.http.post<Book>(
      `./api/books`, 
      book, 
      {
        headers: new HttpHeaders({
        'Accept': 'application/json',
      })      
    });
  }

Update

  updateBook(updateBook: Book): Observable<void> {

    return this.http.put<void>(
      `./api/books/${updateBook.bookID}`, 
      updateBook, 
      {
        headers: new HttpHeaders({
        'Accept': 'application/json',
      })      
    });
  }

Delete

  deleteBook(bookID: number): Observable<void> {
    return this.http.delete<void>(`./api/books/${bookID}`);
  }

Handling Errors

The examples recommended catching the errors and wrapping them in an application specific object. There are many ways to handle errors but here is the approach suggested.

Object For Error

export class BookTrackerError {
  errorNumber: number;
  message: string;
  friendlyMessage: string;
}

Error Handler

  handleHttpError(error: HttpErrorResponse) : Observable<BookTrackerError> {
      let dataError = new BookTrackerError();
      dataError.errorNumber = 100;
      dataError.message = error.statusText;
      dataError.friendlyMessage = "An Error received retrieving data"
      return throwError(dataError);
  }

Implemented on catch on Data Service

We need to use the catchError operator within the pipe and make sure the return argument allows for both return types.

  getAllBooks(): Observable<Book[] | BookTrackerError> {
    return this.http.get<Book[]>('./api/books').pipe(
      catchError(err => this.handleHttpError(err))
    )
  }

Implemented on call to Data Service

So now when we use the data service the error is output to the console. Obviously we might want something else in the app.

   this.dataService.getAllBooks()
      .subscribe(
        (data: Book[]) => this.allBooks = data,
        (err: BookTrackerError) => console.log(err.friendlyMessage),
        ()=> console.log("Completes")
      );

Using Observable Operators

Because we are using the rxjs we can use any of the operators which work with them. In this example we are transforming a new book type to an old book type and reporting it at the end using tap. Note with rxjs it was necessary to import using the notation used below.

import { map,tap } from "rxjs/operators";
...
  getOldBookById(id: number): Observable<OldBook> {

    return this.http.get<Book>(`./api/books/${id}`)
      .pipe(
        map(b => <OldBook>{
          bookID: b.bookID,
          bookTitle: b.title,
          year: b.publicationYear
        }),
        tap(classicBook => console.log(classicBook)),
      );
  }

Resolvers

Resolvers allow you to get the data prior to navigating to the screen. This perhaps, provides a better experience for the user and definitely in the case or error. Resovlers are implemented as a service.

Creating Resolver

Here is the example of the resolver. This wraps data from the data service

import { DataService } from 'app/core/data.service';
import { BookTrackerError } from './../models/bookTrackerError';
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Book } from 'app/models/book';
import { Observable } from 'rxjs';

@Injectable({
    providedIn : 'root'
})
export class BooksResolverService implements Resolve<Book[] | BookTrackerError> {

    constructor(private dataservice : DataService) {}

    resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) : Observable<Book[] | BookTrackerError> {
        return this.dataservice.getAllBooks();
    }
}

Change Router

We need to tell the router we want to use a resolver and we need to provide, the resolver and label the data for retrieval in the page.

import { BooksResolverService } from './core/books-resolver.service';
...
const routes: Routes = [
  { path: 'dashboard', component: DashboardComponent, resolve: {resolvedBooks: BooksResolverService} },
...

Change Page to Get Data from Resolver

We need to, add the activatedRoute to the page, get the data using the name from above and handle if we got data or an error.

...
  constructor(private dataService: DataService,
              private title: Title,
              private route: ActivatedRoute) { }
  
  ngOnInit() {
    let resolvedData : Book [] | BookTrackerError = this.route.snapshot.data["resolvedBooks"]
    if(resolvedData instanceof BookTrackerError)
    {
      console.log(`Dashboard error for ${resolvedData.friendlyMessage}`)
    } else {
      this.allBooks = resolvedData
    }
...

Interceptors

Introduction

Interceptors are useful for

  • Adding headers to all requests
  • Logging
  • Reporting Progress
  • Client-side caching

Defining

Interceptors are defined by implementing a HttpInterceptor interface. There can be more than one interceptor and therefore can be chained together

export class FirstInterceptor implements HttpInterceptor {
   intercept(req: HtttpRequest<any>, next: HttpHandler) : Observable<HttpEvent<any>> {
   
       // Request is immutable so need to copy
       const modifiedRequest = req.clone
       // Make changes
       ...
       // Just Return 
       return next.handle(modifiedRequest)
       // Or We can add pipes to manipulate.
       .pipe(
         tap(event => {
            if(event instanceof HttpResponse) {
               // Modify here
            }
       })
   }
}

The specifying of providers was a little different but here is how to do it within app.module.ts

  providers: [
    {provide: HTTP_INTERCEPTORS, useClass: FirstInterceptor, multi: true },
    {provide: HTTP_INTERCEPTORS, useClass: SecondInterceptor, multi: true },
  ],

Examples

Ordering

Note that the order in which you execute the interceptors is determined by the order in the modules providers.

...
    { provide: HTTP_INTERCEPTORS, useClass: AddHeaderInterceptor, multi: true },
    { provide: HTTP_INTERCEPTORS, useClass: LogResponseInterceptor, multi: true },
...

Example 1 Add Header

Here is an example of adding the content type to the request. Don't for to add it to the providers.

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

@Injectable()
export class AddHeaderInterceptor implements HttpInterceptor {
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

        let myReq: HttpRequest<any> = req.clone({
            setHeaders: {'ContentType' : 'application/json'}
        })
        return next.handle(myReq)
    }
}

Example 2 Log Response

This example logs the response to the console.

import { Injectable } from "@angular/core";
import { HttpInterceptor, HttpHandler, HttpEvent, HttpRequest, HttpResponse, HttpEventType } from "@angular/common/http";
import { Observable } from "rxjs";
import { tap} from "rxjs/operators"
@Injectable()
export class LogResponseInterceptor implements HttpInterceptor {
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        return next.handle(req).pipe(
            tap(event => {
                if(event.type === HttpEventType.Response) {
                    console.log(event.body)
                }
            })
        )
    }
 }

Caching With Interceptors

Role of The Cache Service

The role of the cache will be

  • Provides a data structure for the cached items
  • Add Items to the cache
  • Retrieve items from the cache
  • Remove items from the cache (cache invalidation)

Create the Cache Service

Let use the Angular CLI and not generate unit tests

ng g service services/HttpCache --spec false

And implement the cache

import { HttpResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';

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

  private requests : any = {}

  constructor() { }

  put(url: string , repsonse: HttpResponse<any>) : void {
    this.requests[url] = repsonse;
  }

  get(url: string) : HttpResponse<any> | undefined {
    return this.requests[url];
  }

  invalidate(url: string) : void {
    this.requests[url] = undefined;
  }

  invalidateCache() : void {
    this.requests = {};
  }

}

Create the Interceptor

This creates an interceptor to use the cache.

import { Injectable } from "@angular/core";
import { HttpInterceptor, HttpHandler, HttpEvent, HttpRequest, HttpResponse, HttpEventType } from "@angular/common/http";
import { Observable, of } from "rxjs";
import { tap} from "rxjs/operators"
import { HttpCacheService } from "app/services/http-cache.service";
import { url } from "inspector";
@Injectable()
export class CacheInterceptor implements HttpInterceptor {

    constructor(private cachedService : HttpCacheService) {}

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        
        // Pass along non-cacheable requests and invalidates the cache
        if(req.method !== 'GET') {
            console.log("Clearing the cache")
            this.cachedService.invalidateCache()
            return next.handle(req)
        }

        // Get have a get request
        // attempt to retrieve a cached HttpRequest
        let cachedResponse : HttpResponse<any> = this.cachedService.get(req.url)
        if(cachedResponse) {
            // Wrapping in an observable using of method
            // This is not going to the next interceptor because we have what we want.
            console.log("Got a cached item")
            return of(cachedResponse)
        }
        // send request to server add response to CacheInterceptor
        return next.handle(req).pipe(
            tap(event => {
                if(event.type === HttpEventType.Response) {
                    console.log("Adding to the cache")
                    this.cachedService.put(req.url, event)
                }
            })
        )
    }
 }

Unit Tests

Introduction

The demo showed the basics of using jasmine where the structure of the tests amounted to

describe('DataService Test Suite', () => {
    beforeEach(() => {
    // Do some stuff
    })

    it('test 1', () => {
    // Do some stuff
    })

    it('test 2', () => {
    // Do some stuff
    })

    afterEach(() => {
    // Do some stuff
    })
}

Example

Here is the full example for testing part of the data service

import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController, TestRequest } from '@angular/common/http/testing';

import { DataService } from './data.service';
import { Book } from 'app/models/book';
import { BookTrackerError } from 'app/models/bookTrackerError';

describe('DataService Tests', () => {

  let dataService: DataService;
  let httpTestingController: HttpTestingController;

  let testBooks: Book[] = [
    { bookID: 1, title: 'Goodnight Moon', author: 'Margaret Wise Brown', publicationYear: 1953 },
    { bookID: 2, title: 'Winnie-the-Pooh', author: 'A. A. Milne', publicationYear: 1926 },
    { bookID: 3, title: 'The Hobbit', author: 'J. R. R. Tolkien', publicationYear: 1937 }
  ];

  beforeEach(() => {

    TestBed.configureTestingModule({
      imports: [ HttpClientTestingModule ],
      providers: [ DataService ]
    });

    dataService = TestBed.get(DataService);
    httpTestingController = TestBed.get(HttpTestingController);
  });

  afterEach(() => {
    httpTestingController.verify();
  });

  it('should GET all books', () => {
    dataService.getAllBooks()
      .subscribe((data: Book[]) => {
        expect(data.length).toBe(3);
      });

    let booksRequest: TestRequest = httpTestingController.expectOne('/api/books');
    expect(booksRequest.request.method).toEqual('GET');

    booksRequest.flush(testBooks);
  });

  it('should return a BookTrackerError', () => {

    dataService.getAllBooks()
      .subscribe(
        (data: Book[]) => fail('this should have been an error'),
        (err: BookTrackerError) => {
          expect(err.errorNumber).toEqual(100);
          expect(err.friendlyMessage).toEqual('An error occurred retrieving data.');
        }
      );

    let booksRequest: TestRequest = httpTestingController.expectOne('/api/books');

    booksRequest.flush('error', {
      status: 500,
      statusText: 'Server Error'
    });

  });

});

Services

Introduction

Angular provides the management of services. We can use dependency injections to provide services.

Building a Service

To build a service we need to define a class and add the @Injectable decorator.

import { Injectable } from '@angular/core';
import { IProduct } from './product';

@Injectable()
export class ProductService {

  getProducts(): IProduct[] { return [
    {
      "productId": 1,
      "productName": "Leaf Rake",
      "productCode": "GDN-0011",
      "releaseDate": "March 19, 2019",
      "description": "Leaf rake with 48-inch wooden handle.",
      "price": 19.95,
      "starRating": 3.2,
      "imageUrl": "assets/images/leaf_rake.png"
    },
    {
      "productId": 2,
      "productName": "Garden Cart",
      "productCode": "GDN-0023",
      "releaseDate": "March 18, 2019",
      "description": "15 gallon capacity rolling garden cart",
      "price": 32.99,
      "starRating": 4.2,
      "imageUrl": "assets/images/garden_cart.png"
    },
....
  ]
  }
}

Registering a Service

You can register services to either the root or just to component and its children. To register it with the root we set the providedIn: to root on the @Injectable decorator.

@Injectable({
  providedIn: 'root'
})

Injecting A Service

We do this the same way as we do it in javascript. We create a constructor, pass in the service and assign it to a variable in the class. This is not much different to C#

export class ProductListComponent {
    private _productService : ProductService;
    
    constructor(inProductService: ProductService) {
       this._productService = inProductService
   }

// For short
   constructor(private _productService: ProductService)
}

We also need to initialize our products from this service. To do this once then we should use the OnInit function.

     ngOnInit(): void {
        this.products = this._productService.getProducts();
        this.filteredProducts = this.products
     }

Retrieving Data

Add the HttpClientModule

No surprises

import { HttpClientModule } from '@angular/common/http';
...
  imports: [
    BrowserModule,
    FormsModule,
    HttpClientModule
  ],

Inject The Client the Service

Using the short code we learned in services we add a constructor to add the service to our service along with the imports.

   constructor(private _httpClient: HttpClient) {}

Make the Http Call

This is as simple as passing the Url to the client.

  getProducts(): Observable<IProduct[]> { 
    return this._httpClient.get<IProduct[]>(this.productUrl) 
  }

Subscribing

We need to subscribe to the returning observable and make sure the filtered products are set post deliver of the data (which is async)

    ngOnInit(): void {
        this._productService.getProducts().subscribe({
            next: products => {
              this.products = products;
              this.filteredProducts = this.products;
            },
          });
    }

Routing

Enable the Router

As ever we need to specify our router in the apps module. But with this we also need to specify our list of roots too. See [3] for more information.

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

// And 
....
  imports: [
    BrowserModule,
    FormsModule,
    HttpClientModule,
    RouterModule.forRoot([]),
  ],

Configuring Example Routing

Here is what was used to define the routes for the example app. First set the base root in the index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>APM</title>
  <base href="/">
....
</html>

And now the routes in app.module.ts

    RouterModule.forRoot([
      { path: 'products', component: ProductListComponent },
      { path: 'products/id', component: ProductDetailComponent },
      { path: 'welcome', component: WelcomeComponent },
      { path: '', redirectTo: 'welcome', pathMatch: 'full' },
      { path: '**', redirectTo: 'welcome', pathMatch: 'full' }
    ]),

Binding to Controls

This is done via the routerLink element. For example

<ul class=='nav navabar-nav>
    <li><a [routerLink]="['welcome']">Home</a></li>
    <li><a [routerLink]="['products']">Home</a></li>
</ul>

Displaying the View

To display the view we also need to put the routers selector (router-outlet) into some html. In our case this was app.component.ts

  template: `
    <nav class='navbar navbar-expand navbar-light bg-light'>
        <a class='navbar-brand'>{{pageTitle}}</a>
        <ul class='nav nav-pills'>
          <li><a class='nav-link' routerLinkActive='active' [routerLink]="['/welcome']">Home</a></li>
          <li><a class='nav-link' routerLinkActive='active' [routerLink]="['/products']">Product List</a></li>
        </ul>
    </nav>
    <div class='container'>
      <router-outlet></router-outlet>
    </div>
    `,

Implementing Details Click

To display the detail behind a product read from our list we set up an anchor tag

<a [routerLink]="['/products',product.productId]">
   {{product.productName}}
</a>

Now within the detail component we need to import the ActivatedRoute component to read the id of the product passed. We do this be injection using the shorthand.

import {ActivatedRoute} form '@angular/router'
...
constructor(private route: ActivatedRoute) {};
...
  // Note the plus is a shortcut to convert string to number in javascript
  ngOnInit(): void {
    const param = +this.route.snapshot.paramMap.get("id");
    if (param) {
      const id = +param;
      this.getProduct(id);
    }

  }

Navigating With Code

Import the router service as a dependencies and call navigate['products'];

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

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

  onBack(): void {
    this.router.navigate(['/products']);
  }

Then stick a button on to activate

    <div class='card-footer'>
        <button class='btn btn-outline-secondary'
            (click)='onBack()'
            style='width:80px'>
            <i class='fa fa-chevron-left'></i> Back
        </button>
    </div>

Guarding Routes

Angular provides methods to prevent usage of routes they include

  • CanActivate Guard navigation to route
  • CanDeactivate Guard navigation from a route
  • Resolve Pre-fetch data before activating a route
  • CanLoad Prevent asynchronous routing

To implement derive a service from the relevant guard and attach it to the routing. We can generate the code using the cli and selecting the appropriate guard.

ng g g products/product-detail

This creates the skeleton of a class and all that is required is to add the router as a service and the logic to determine if ok. In the example this is just bogus.

...
export class ProductDetailGuard implements CanActivate {

  constructor(private router: Router) { }

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    const id = +next.url[1].path;
    // const id2 = next.paramMap.get('id');
    // console.log(id2);
    if (isNaN(id) || id < 1) {
      alert('Invalid product Id');
      this.router.navigate(['/products']);
      return false;
    }
    return true;
  }
}

We do, of course, need to add it to the code which does the routing in app.module

    RouterModule.forRoot([
      { path: 'products', component: ProductListComponent },
      { 
        path: 'products/id', 
        canActivate: [ProductDetailGuard],
        component: ProductDetailComponent 
      },

Modules

Creating the Template

We can create a module template using the cli

ng g m products/product --flat -m app

The --flat is because we have our content and this, by default, assumes a new module/folder. The -m app adds the module to our app.module.ts.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

@NgModule({
  declarations: [],
  imports: [
    CommonModule
  ]
})
export class ProductModule { }

Adding Our Modules

Now we can add our code to the modules declarations array along with the imports

...
import { ProductListComponent } from './product-list.component';
import { ConvertToSpacesPipe } from '../shared/convert-to-spaces.pipe';
import { StarComponent } from '../shared/star.component';
import { ProductDetailComponent } from './product-detail.component';
...
@NgModule({
  declarations: [
    ProductListComponent,
    ConvertToSpacesPipe,
    StarComponent,
    ProductDetailComponent,
  ],
  imports: [
    CommonModule

...

Other Third-Party/External Modules

These need to be added to the imports if required. In our case this was FormsModule and RouterModule.

Routing Considerations

The router does not need to be registered in the module as it will need to be registered with the app. Because of this the product routes, which are owned by this module, must be declared using the forChild and not forRoot

  imports: [
    CommonModule,
    FormsModule,  
    RouterModule.forChild([
      { path: 'products', component: ProductListComponent },
      { 
        path: 'products/id', 
        canActivate: [ProductDetailGuard],
        component: ProductDetailComponent 
      },
    ]),
  ]

Shared Module and Export

We want share common code, often this is modules like CommonModule and FormsModule. Specifying these in one shared modules means we do not have to repeat this for each module. Below is an example of a shared module which does this.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { StarComponent } from './star.component';
import { FormsModule } from '@angular/forms';

@NgModule({
  declarations: [
    StarComponent
  ],
  imports: [
    CommonModule
  ],
  exports: [
    StarComponent,
    CommonModule,
    FormsModule
  ]
})
export class SharedModule { }

Deployment

To deploy using apache I

  • Made an Apache conf
  • Added .htaccess
  • Changed the base

Apache Conf

Here is the conf.

# Those aliases do not work properly with several hosts on your apache server
# Uncomment them to use it or adapt them to your configuration
Alias /APM /mnt/nosaying/APM

 <Directory /mnt/nosayingAPM>
    Options +FollowSymlinks
    AllowOverride All
    Require all granted
 </Directory>
     
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined

htaccess

<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /APM/
  RewriteRule ^index\.html$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . index.html [L]
</IfModule>

Changed the base

For index.html I changed the base to point to the subfolder.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>APM</title>
  <base href="/APM/">
...