Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pattern for dealing with mapping API objects to UI model objects

I am switching to use the new HttpClient in angular.

The API am calling returns json data in one format. I want to take this data and transform it into a typescript model class suitable for the UI to work with.

The way I did this before was by using the map function e.g.

        return this.httpClient.get(url, { headers: headers })
        .map(mapFunction)
        .catch(errorFunction);

Where the map function does the heavy lifting of transforming the api response intro a model object e.g.

 const mapFunction =
            (response: Response) => {
                const data = response.json();
                const contacts: Contact[] = [];
                let i = 0;
                for (const result of data.resourceResults) {
                    i = i + 1;
                    const contact: Contact = new Contact();
                    contact.role = result.r

To me this seems quite cumbersome and I am basically looking for a way to map objects from the api response type to the ui model type without having to use a custom map function for each request.

like image 957
AJM Avatar asked Dec 04 '17 11:12

AJM


2 Answers

There is no way to do custom mapping without, well, explicitly specifying what needs to be mapped. It's either you tell the server-side to return a UI friendly response, or you need to do the mapping yourself at the client side.

If you want to map the response on the client side, you can leverage on Typescript classes, and use its constructor to quickly generate the items you want:

export class Contact {
    public name:string;
    public roles:any;
    constructor(name:string, roles: any) {
        //specify your own constructor logic
        this.name=name;
        this.roles=roles
    }
}

and now you can write in your mapFunction to explicitly convert the response to a list of Contacts. Also, you can use array's .map() to iterate through the objects, without writing a for loop :

public mapFunction = (response: Response) => {
    const data = response.json();
    const resource = data.resourceResults;
    //map it
    return resource.map(result => new Contact(result.n, result.r))
}

Cumbersome or not, I think its subjective. But definitely you can write your code in a more elegant way.

like image 105
CozyAzure Avatar answered Oct 04 '22 22:10

CozyAzure


To add more examples based on @CozyAzure's response, this is what I do.

Typescript class with interface (for better readability through the app):

interface MessageConfig {
  MessageId?: Guid;
  SentAt?: Date;
  Status?: string;
  Text?: string;
}

export class Message {
  MessageId: Guid;
  SentAt: Date;
  Status: string;
  Text: string;

  constructor(config: MessageConfig) {
    this.MessageId = config.MessageId;
    this.SentAt = config.SentAt || new Date();
    this.Status = config.Status || "Unread";
    this.Text = config.Text;
  }
}

A utility function to map data to Message class:

export function mapMessage(message: any) {
  return new Message({
    MessageId: message.messageId,
    SentAt: new Date(message.sentAt),
    Status: message.status,
    Text: message.text,
  });
}

Service function that maps server response to a single message:

addMessage = (message: Message) => {
    return this.http.post(API, message).pipe(
      map((response: any) => {
        if (response && response.success) {
          const m = mapMessage(response.message);
          return m;
        }
        throw new Error(response.errorMessage);
      }),
      catchError(error => {
        return throwError(error);
      }),
      share()
    );
  };

Service function the maps server response to multiple messages:

getMessages = (): Observable<Array<Message>> => {
    return this.http
      .get(API)
      .pipe(
        map((response: any) => {
          if (response && response.success) {
            let messages: Array<Messages> = [];
            if (response.count > 0) {
              messages = response.messages.map(
                message => mapMessage(message)
              );
            }
            return messages;
          }
          throw new Error(response.errorMessage);
        }),
        catchError(error => {
          return throwError(error);
        }),
        share()
      );
  };

So, yeah, there's a lot going on, but that's what I've come up with after many, many iterations on the idea. It's been the easiest to replicate and maintain, and it's clear and understandable, as well as being fairly DRY. I usually have several API calls mapping to the same class, so 5 minutes of setup saves me that much time for every additional API call.

like image 30
S. Dunn Avatar answered Oct 04 '22 22:10

S. Dunn