Angular Adapter pattern

Angular Adapter pattern
Difficulty

Let’s see an Angular version of the Adapter pattern.
Mapping allow us to decrease coupling between what arrives from a remote BE on our FE. We can take advantage of one of the Gang of Four’s design patterns (the Adapter pattern) to transform the external response in what we want.

Adapter pattern in models

Every model will have its personal Adapter. We need the adapt method to have the same keys of the external service, as input.

// app/models/Person.ts
export class Person {
  constructor(
    public id: number,
    public firstname: string,
    public surname: string,
    public birthDate: Date,
  ) { }

  static adapt(item: any): Person {
    return new Person(
      item.uid,
      item.name,
      item.lastname,
      new Date(item.birth),
    );
  }
}

As you can see above, the adapt method allows you to insert even very different object properties in the Person object that we will generate. It is also possible to do a mapping or a filter, in case you have lists of some kind.
It is even possible to nest further calls to adapt methods of other objects, within our own adapt.

Adapter in services

And in our service we can call the model adapt method when mapping on external datas.

// app/services/person.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Person } from '../models/Person';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class PersonService {
  private _apiUrl = 'http://api.myapp.com/persons';

  constructor(private readonly _http: HttpClient) {}

  /** For a list of persons */
  listPersons(): Observable<Person[]> {
    return this.http.get<any[]>(this._apiUrl).pipe(
      map(data => data.map(Person.adapt))
    );
  }

  /** For a unique person */
  getPerson(): Observable<Person> {
    return this.http.get(this._apiUrl).pipe(
      map(data => Person.adapt(data.info))
    );
  }
}

Our Backend service will expose a service like this:

// listPersons
[
  {
    "uid": 1,
    "name": "John",
    "lastname": "Doe",
    "birth": "02/23/1900"
  },
  {
    "uid": 2,
    "name": "Mary",
    "lastname": "Doe",
    "birth": "03/23/2000"
  },
]

Or this:

// onePerson
{
  "info": {
    "uid": 1,
    "name": "John",
    "lastname": "Doe",
    "birth": "02/23/1900"
  }
}

You may notice that the object keys in the service are different from the properties of our internal object.

This is the way to use it in your component:

// person.component.ts
import {Component, OnDestroy, OnInit} from '@angular/core';
import {Subscription} from 'rxjs';
import {PersonService} from '../app/services/person.service';

@Component({
  selector: 'app-person',
  templateUrl: './person.component.html',
})
export class PersonComponent implements OnInit, OnDestroy {
  private sub: Subscription;
  public person: Person;
  public error: boolean;

  constructor(private _personService: PersonService) {}

  ngOnInit(): void {
    this.sub = this._personService.listPersons().subscribe({
      next: person => {
        this.person = person;
      },
      error: err => {}
    });
  }

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

If we have simpler situations can use the basic mapping, with generics:

getPersons(): Observable<Person[]> {
  return this._http.get(this._apiUrl + 'persons').pipe(
    map<any, Person[]>(response => response.result.items)
  );
}

Or maybe we have only one instance of the object in output, to be mapped:

getPerson(): Observable<Person> {
  return this._http.get<Person>(this._apiUrl + 'persons');
}

This is my version of the adapter pattern in Angular.
Try it at home!

2
2 people like this.
Please wait...

Leave a Reply

Thanks for choosing to leave a comment.
Please keep in mind that all comments are moderated according to our comment policy, and your email address will NOT be published.
Please do NOT use keywords in the name field. Let's have a personal and meaningful conversation.