19 Mar 2018
cat: Angular 2+
0 Comments

Angular 4 – HttpClient Mapping using Decorators

Introduction

In Angular 4 we use HttpClient to perform AJAX request. HttpClient does help with type hints unlike the old Http but it does not really map to a typed object, it only returns a JavaScript object. In this tutorial we will use decorators to help with mapping properties. With this technique we don’t have to map every JSON response from AJAX to typed objects, we just decorate properties of a class.

Setup

In a new Angular project let us create the decorators first.

src/app/decorators/json-property.ts

export function JsonProperty(name?: string) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        if (!target.constructor.prototype.hasOwnProperty('_jsonUnserializeMeta')) {
            target.constructor.prototype._jsonUnserializeMeta = [{ prop: propertyKey, jsonProp: name || propertyKey }];
        } else {
            target.constructor.prototype._jsonUnserializeMeta.push({ prop: propertyKey, jsonProp: name || propertyKey });
        }
    };
}

This is just a simple decorator that pushes some properties to the class of the property it is attached to. Refer to the Typescript documentation to know more about decorators.

HttpClient

Next, we will create a service that uses HttpClient to make AJAX request.

ng generate service services/api/api --spec=false

src/app/services/api/api.service.ts

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
// Import rxjs map so we can manipulate
// the response for the subscriber.
import 'rxjs/add/operator/map';

// Interface so http client methods returns Observable<T> since HttpClient have
// many versions of a method like get(), post(), etc.
interface IApiResponse {
  headers?: HttpHeaders | { [header: string]: string | string[] },
  observe?: 'body',
  params?: HttpParams | { [param: string]: string | string[] },
  reportProgress?: boolean,
  responseType?: 'json',
  withCredentials?: boolean,
}

// Interface that will help us generate an instance from a generic.
interface IEntity<T> {
  new(...args: any[]): T;
}

@Injectable()
export class ApiService {

  constructor(
    private http: HttpClient
  ) {
  }

  // This will be the get method we will use in the application
  // instead of using the methods directly from HttpClient.
  get<T>(type, url) {
    return this.http.get<T>(url)
      .map(response => {
        return this._unserialize<T>(type, response);
      });
  }

  // We unserialize the properties with the help of the hidden
  // properties (_jsonUnserializeMeta) added by the decorators.
  private _unserialize<T>(type: IEntity<T>, response: any) {

    // If the response is an array we loop through it.
    if (Array.isArray(response)) {

      // We will add the items in this array.
      let entities = [];

      response.forEach(item => {

        // Create an instance of the type
        let entity = this._instantiate(type);

        // Assign the properties
        if (entity.constructor.prototype.hasOwnProperty('_jsonUnserializeMeta')) {
          entity.constructor.prototype._jsonUnserializeMeta.forEach(property => {
            entity[property.prop] = item[property.jsonProp];
          });
        }

        // Push the new item created.
        entities.push(entity);
      });

      // Return the array
      return entities;
    }

    // If the response is not an array
    // Create an instance of the type
    let entity = this._instantiate(type);

    // Assign the properties
    if (entity.constructor.prototype.hasOwnProperty('_jsonUnserializeMeta')) {
      entity.constructor.prototype._jsonUnserializeMeta.forEach(property => {
        entity[property.prop] = response[property.jsonProp];
      });
    }

    // Return the object
    return entity;
  }

  // A method to help us create instances 
  private _instantiate<T>(type: IEntity<T>): T {
    return new type();
  }
}

Currently there is only the get() method. We map the response correctly using the hidden properties that the decorator added. So when we subscribe, we will get a typed object as the response. You can of course improve this class further.

Using the Service

I created a src/books.json to use for testing.

[
    {
        "id": 1,
        "title": "One Thing",
        "description": "The ONE Thing: The Surprisingly Simple Truth Behind Extraordinary Results is a non-fiction, self-help book written by authors and real estate entrepreneurs.",
        "author": "Gary W. Keller and Jay Papasan",
        "published_at": "2013-04-01"
    },
    {
        "id": 2,
        "title": "How to Win Friends and Influence People",
        "description": "How to Win Friends and Influence People is a self-help book written by Dale Carnegie, published in 1936. Over 30 million copies have been sold world-wide, making it one of the best-selling books of all time.",
        "author": "Dale Carnegie",
        "published_at": "1936-10-01"
    }
]

We need to create a model class that we can map this JSON to.

src/app/models/book.ts

import { JsonProperty } from "../decorators/json-property";

export class Book {
    private _id: number;
    private _title: string;
    private _description: string;
    private _author: string;
    private _publishedAt: Date;

    public get id(): number {
        return this._id;
    }

    @JsonProperty()
    public set id(value: number) {
        this._id = value;
    }

    public get title(): string {
        return this._title;
    }

    @JsonProperty()
    public set title(value: string) {
        this._title = value;
    }

    public get description(): string {
        return this._description;
    }

    @JsonProperty()
    public set description(value: string) {
        this._description = value;
    }

    public get author(): string {
        return this._author;
    }

    @JsonProperty()
    public set author(value: string) {
        this._author = value;
    }

    public get publishedAt(): Date {
        return this._publishedAt;
    }

    @JsonProperty('published_at')
    public set publishedAt(value: Date) {
        this._publishedAt = value;
    }

}

In this class, we decorate the properties we want to be assigned with value. If the property name is different from the JSON property name, we can specify it on the decorator.

Then we can create a separate service that will use the ApiService.

ng generate service services/books/books --spec=false

src/app/services/books/books.service.ts

import { Injectable } from '@angular/core';
import { ApiService } from '../api/api.service';
import { Book } from '../../models/book';

@Injectable()
export class BooksService {

  constructor(
    private api: ApiService
  ) {
  }

  getBooks() {
    return this.api.get<Array<Book>>(Book, 'books.json');
  }
}

Finally we can use this service to our component to display the books.

src/app/app.component.ts

import { Component } from '@angular/core';
import { Book } from './models/book';
import { BooksService } from './services/books/books.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styles: [`.book {
    border: solid 1px #000;
    margin-bottom: 15px;
  }`]
})
export class AppComponent {

  books: Book[] = [];

  constructor(
    private service: BooksService
  ) {
  }

  ngOnInit() {
    this.service.getBooks()
      .subscribe((response: Book[]) => {
        this.books = response;
      });
  }
}

src/app/app.component.html

<h1>Books</h1>
<div *ngFor="let b of books;"
     class="book">
  <div>
    <strong>{{ b.title }}</strong>
  </div>
  <p>{{ b.description }}</p>
  <div>Author: {{ b.author }}</div>
  <div>Published At: {{ b.publishedAt | date:'mediumDate' }}</div>
</div>

Resources

Be the first to write a comment.

Post A Comment