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.
Important: You can find my updated version of this guide which targets Angular 7 here.
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 {
deserialize(input: any): this;
}
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 {
id: number;
name: string;
car: Car;
deserialize(input: any) {
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 {
brand: string;
year: number;
deserialize(input: any): this {
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)...
}
Changelog
12.05.2018
- Changed the
Deserializable
interface fromDeserializable<T>
toDeserializable
. There's no reason for it to be generic.