Pattern for dealing with mapping API objects to UI model objects
Asked Answered
M

3

7

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.

Mariellamarielle answered 4/12, 2017 at 11:33 Comment(3)
I think there is no another way, maybe you can use an automapper: github.com/loedeman/AutoMapper/wiki/Getting-started but it's add a dependancy to your projectKraska
You might first of all do something like this const mapFunction = (response: any[]):Contact[] => { const data = response; return response.map(result=>{ const contact = new contact(); contact.r = result.r; return contact; })Ladder
in Angular 5: return this.httpClient.get<Contact[]>(url, { headers: headers }) .catch(errorFunction); see angular.io/guide/httpBotsford
R
5

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.

Rhapsodic answered 5/12, 2017 at 2:18 Comment(0)
E
2

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.

Executor answered 2/11, 2018 at 3:46 Comment(0)
W
0

I'm completely unfamiliar with angular at the moment, but this should still apply because of typescript. Sorry for my pseudo-angular code. One of the easiest ways is to map your objects to interfaces. Thus if I have JSON response:

{
  Id: 1,
  Name: "Bob",
  Age: 21
}

Because of TypeScript I can just create a matching interface:

export interface IPerson
{
  Id: number,
  Name: string,
  Age: number
}

And I can just pass the value around as the interface without having to map value to classes.

return (this.httpClient.get(url, { headers: headers })) as IPerson;

Granted, if you have to extend the JSON this isn't the correct solution.

Whereon answered 8/12, 2017 at 5:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.