/ angular

Working with models in Angular

One thing I've got pretty used to is using models in Angular; using objects which hold your data may be pretty useful. It makes the developer live significantly easier - so let me show you what I'm talking about and how I handle models in Angular.

The API

Let's assume we've got an API which returns our users:

GET /api/user

{
    "status": "success",
    "response": [
        {
            "id": 1,
            "name": "John",
            "car": {
                "brand": "BMW",
                "year": 2015
            }
        },
        {
            "id": 2,
            "name": Bob",
            "car": {
                "brand": "Koenigsegg",
                "year": 2014
            }
        }
    ]
}

Basic models

We're going to create two very simple models; one for User and one for Car.

Our user model:

// src/app/shared/models/user.model.ts

import {Car} from "./car.model";

export class User {
  id: number;
  name: string;
  car: Car;
}

And the car model:

// src/app/shared/models/car.model.ts

export class Car {
  brand: string;
  year: number;
}

These two objects will hold our data from the API. We're going to extend these models later, first let's create a service for getting our users:

// src/app/core/service/user.service.ts

import {Injectable} from "@angular/core";
import {Http, Response} from "@angular/http";
import 'rxjs/add/operator/map';
import {User} from "../../shared/models/user.model";

@Injectable()
export class UserService {
  constructor(private http: Http) {}

  getUser() {
    return this.http.get('/api/user')
      .map((res: Response) => res.json().response);
  }
}

Calling getUser() now results in:

(2) [Object, Object]
[
  { id: 1, name: "John", car: Object },
  { id: 2, name: "Bob", car: Object }
]

But that's not exactly what we wanted. We want to get an array of User objects from our service. Let's do this.

Deserialization

We want to deserialize our JSON to our objects. Let's create an interface which provides an API for deserialization:

// src/app/shared/models/deserializable.model.ts

export interface Deserializable<T> {
  deserialize(input: any): T;
}

Hint: The reason for input not being type of T is that this would be logically wrong (what we get is JSON, not T). Additionally, it mustn't be the case that your JSON looks the same as your model. Hence we're using any here to avoid compile errors (theoretically you can implement an interface which holds the shape of your JSON, but that's up to you).

Now we can extend our models and implement our interface. Let's start with the User model:

// src/app/shared/models/user.model.ts

import {Car} from "./car.model";
import {Deserializable} from "./deserializable.model";

export class User implements Deserializable<User> {
  id: number;
  name: string;
  car: Car;

  deserialize(input: any): User {
    Object.assign(this, input);
    return this;
  }
}

The interesting part here is the deserialize method. Basically we're just assigning the input object to this - or, in other words, we're merging the input object with the User object.

But there's still one minor issue here: the car member won't be an instance of Car but still be an Object. We need to tell our deserialize method this manually:

deserialize(input: any): User {
  Object.assign(this, input);
  this.car = new Car().deserialize(input.car);
  return this;
}

And, of course, we now need to implement our Deserializable interface for Car too:

// src/app/shared/models/car.model.ts

import {Deserializable} from "./deserializable.model";

export class Car implements Deserializable<Car> {
  brand: string;
  year: number;

  deserialize(input: any): Car {
    Object.assign(this, input);
    return this;
  }
}

Now we can go back to our service and tell it what we want to get: we want to get an array of User, not just objects:

// src/app/core/service/user.service.ts

getUser(): Observable<User[]> {
  return this.http.get('/api/user')
    .map((res: Response) => res.json().response.map((user: User) => new User().deserialize(user)));
}

Calling getUser now results in:

(2) [User, User]
[
  User { id: 1, name: "John", car: Car { brand: "BMW", year: 2015 } },
  User { id: 2, name: "Bob", car: Car { brand: "Koenigsegg", year: 2014 } }
]

Which is exactly what we wanted. Yay!

But why do we want that?

Handling raw JSON objects is really painful and hard to maintain. Having "real" entity objects which store data has obvious advantages (maintaining and extensibility) - an example:

Users have a firstName and a lastName. If you're handling the raw JSON you'll have to print out the full name of your user within your templates like this:

<ul>
  <li *ngFor="let user of users">{{ user.firstName }} {{ user.lastName }}</li>
</ul>

One day your customer calls and tells that the order of first- and lastname should be switched. An endless joy which can be done by a (skilled) potato, since all you need to do is to go through every template and switch the expressions.

But if your user is a User object, you can simply implement a function to print the fullname:

getFullName() {
  return this.firstName + ' ' + this.lastName;
}

// Just another example, assuming our Car class does implement a `isSportsCar` method
hasSportsCar() {
  return this.car.isSportsCar();
}

You can now simply call this function in your template:

<ul>
  <li *ngFor="let user of users">{{ user.getFullName() }}</li>
</ul>

And whenever a change is required you need to change one single line. Simple, but effective.

Another good reason for using models like this is that we're working with Typescript. We want to know the type of things when we use them and not just define everything as any. In combination with a good IDE (and you definitely should use a good IDE) this makes life a lot easier.

Of course this can be used for handling forms too:

form: FormGroup;

createForm() {
  this.form = this.fb.group({
    id: null,
    name: ['', Validators.required],
    car: null
  });
}

private prepareSave(): User {
  return new User().deserialize(this.form.value);
}

onSubmit() {
  const user = this.prepareSave(); // `user` is now an instance of "User"
  // this.http.post('/api/user', user)...
}
Kevin

Kevin

I make stuff. Mostly functional, occasionally shiny, stuff.

Read More