Understanding MVC Services for Front End: Angular

A step-by-step Angular tutorial

Carlos Caballero
Nov 12, 2019 · 8 min read
Photo by Caspar Camille Rubin on Unsplash

Introduction

This post is the third in a series of three posts to understand how the MVC architecture works to create frontend applications. The objective is to understand how to structure a frontend application. This is done by evolving from a web page in which JavaScript is used as a scripting language, towards an application in which JavaScript/TypeScript is used as an object-oriented language.

In this third post, the application will be built using Angular, from the second version which used TypeScript. Therefore, this piece covers the migration of the application from TypeScript to Angular. However, it’s important to understand how all the parts of the application are related and how it is structured. Angular allows us to forget about DOM, so the user.view.ts file disappear from our app.

Finally, in the last piece, we will transform our code to integrate it with the Angular framework.


Project Architecture

There’s nothing more valuable than an image to understand what we are going to build. This is the application we are building:

This can be built using a single JavaScript or TypeScript file, which modifies the DOM of the document and performs all operations, but this is strongly coupled code and not what we intend to apply in this piece.

What is the MVC architecture? MVC is an architecture with three layers/parts:

  • Models — Manage the data of an application. The models will be anemic (they will lack functionalities) since they will be referred to the services.

This is the file structure that we will have in our problem domain:

The index.html file will act as a canvas on which the entire application will be dynamically built, using the root element.

Finally, our file architecture is composed of the following TypeScript files:

  • user.model.ts — The attributes (the model) of a user.

The App Module is the one shown below:

import { FormsModule, ReactiveFormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { UserService } from './shared/services/user.service';
import { UsersComponent } from './views/users/users.component';

@NgModule({
declarations: [AppComponent, UsersComponent],
imports: [BrowserModule, FormsModule, ReactiveFormsModule],
providers: [UserService],
bootstrap: [AppComponent]
})
export class AppModule {}

You can see that our app use three modules: BrowserModule, FormsModule , and reactiveFormsModule. The first module is used to get the basic structural and attributes directives from Angular, while the second and third modules are used to create our forms. In this example, the Users Module is not defined because this example is tiny. You can see that UserService and UsersComponent are included in this module. In fact, you could split the UsersComponent into two components (List and Form) but in this example, our goal is to show you the evolution from JavaScript to Angular.


Models (anemic)

The first built class in this example is the application model, user.model.ts, which consists of the class attributes, and a private method that’s generating random IDs (these IDs could come from a database in the server).

The models will have the following fields:

  • id. Unique value.

The User class has been typed using TypeScript. However, the User constructor receives a plain object that will be provided from LocalStorage or from the user data input through the form. This plain object must comply with the UserDto interface in such a way that any plain object cannot be instantiated but those that satisfy the defined interface.

The user.model.ts is shown below:

export interface UserDto {
name: string;
age: string;
complete: boolean;
}

export class User {
public id: string;
public name: string;
public age: string;
public complete: boolean;

constructor(
{ name, age, complete }: UserDto = {
name: null,
age: null,
complete: false
}
) {
this.id = this.uuidv4();
this.name = name;
this.age = age;
this.complete = complete;
}

uuidv4(): string {
return (([1e7] as any) + -1e3 + -4e3 + -8e3 + -1e11).replace(
/[018]/g,
(c: number) =>
(
c ^
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
).toString(16)
);
}
}

Services

The operations performed on users are carried out in the service. The service is what allows the models to be anemic since all the logic load is in them. In this specific case, we will use an array to store all users and build the four methods associated with reading, modifying, creating and deleting (CRUD) users.

You should note that the service makes use of the model, instantiating the objects that are extracted from LocalStorage to the User class. This is because LocalStorage only stores data and not prototypes of stored data. The same happens with the data that travels from the backend to the frontend: they do not have their classes instantiated.

The constructor of our class is as follows:

constructor() {
const users: UserDto[] = JSON.parse(localStorage.getItem('users')) || [];
this.users = users.map(user => new User(user));
}

Note that we have defined a class variable called users that stores all users once they have been transformed from a plain object (UserDto) to a prototyped object of the User class.

The next thing we must define in the service will be each of the operations we want to develop. These operations are shown below using TypeScript:

add(user: User) {
this.users.push(new User(user));
this._commit(this.users);
}

edit(userID: string, userToEdit: User) {
this.users = this.users.map(user =>
user.id === userID
? new User({
...user,
...userToEdit
})
: user
);

this._commit(this.users);
}

delete(userID: string) {
this.users = this.users.filter(({ id }) => id !== userID);
this._commit(this.users);
}

toggle(userID: string) {
this.users = this.users.map(user =>
user.id === userID
? new User({ ...user, complete: !user.complete })
: user
);

this._commit(this.users);
}

The commit method responsible for storing the operation performed in our data store (in our case LocalStorage) is still to be defined:

_commit(users: User[]) {
localStorage.setItem('users', JSON.stringify(users));
}

This method doesn’t invokes a callback function that’s binding when creating the service, as is required when developing in JavaScript or TypeScript, because Angular performs this task, performing the binding between controller and template.

The file user.service.ts is as follows:

import { User, UserDto } from "../models/user.model";

export class UserService {
public users: User[];

constructor() {
const users: UserDto[] = JSON.parse(localStorage.getItem("users")) || [];
this.users = users.map(user => new User(user));
}

_commit(users: User[]) {
localStorage.setItem("users", JSON.stringify(users));
}

add(user: User) {
this.users.push(new User(user));
this._commit(this.users);
}

edit(userID: string, userToEdit: User) {
this.users = this.users.map(user =>
user.id === userID
? new User({
...user,
...userToEdit
})
: user
);

this._commit(this.users);
}

delete(userID: string) {
this.users = this.users.filter(({ id }) => id !== userID);
this._commit(this.users);
}

toggle(userID: string) {
this.users = this.users.map(user =>
user.id === userID
? new User({ ...user, complete: !user.complete })
: user
);

this._commit(this.users);
}
}

Views

This is the part that changes most compared to the previous pieces in this series. In this case, we do not need to work with the DOM since Angular will perform the hard task of dynamically manipulating the DOM. However, we do have to properly define the template.

Here’s the template (an Angular-enriched HTML version) created for this example:

<h1>Users</h1>

<form [formGroup]="userForm" (ngSubmit)="add(userForm.value)">
<input
type="text"
placeholder="Name"
name="name"
formControlName="name"
/><input
type="text"
placeholder="Age"
name="age"
formControlName="age"
/><button>Submit</button>
</form>
<ul class="user-list">
<li *ngFor="let user of users">
<input type="checkbox" (change)="toggle(user)" [checked]="user.complete" />
<span>
<s *ngIf="user.complete; else uncompleteName">{{ user.name }}</s>
<ng-template #uncompleteName>{{ user.name }}</ng-template>
</span>
<span
#age
contenteditable="true"
class="editable"
(focusout)="edit(user, age)"
>
<s *ngIf="user.complete; else uncompleteAge">{{ user.age }}</s>
<ng-template #uncompleteAge>{{ user.age }}</ng-template></span
>
<button class="delete" (click)="delete(user)">Delete</button>
</li>
</ul>

This is not an Angular tutorial but rather a series of ppieces in which you can see the evolution of a Web application from JavaScript to TypeScript to Angular.

However, we note that much DOM manipulation code in previous pieces has been resolved by Angular providing two structural directives such as * ngFor and * ngIf that allow the DOM to be manipulated comfortably from the template itself.

Another interesting point to note is that Angular has helped us in this example with the use of reactive forms. With these, the template is connected to the controller without us having to send a handler to make the connections.


Controllers

The last file of this architecture is the controller ( users.component.ts). The controller receives the two dependencies it has (service and formBuilder) by dependency injection (DI). Those dependencies are stored in the controller in private variables.

The controller only focuses on managing the attributes that are connected to the view (template) and invoking the service. Exactly like our first JavaScript code or second TypeScript version of the previous posts. In this case, we have left the framework all the tasks associated with the DOM.

This is the file users.component.ts:

import { Component, OnInit } from '@angular/core';

import { FormBuilder } from '@angular/forms';
import { UserService } from 'src/app/shared/services/user.service';

@Component({
selector: 'app-users',
templateUrl: './users.component.html',
styleUrls: ['./users.component.css']
})
export class UsersComponent implements OnInit {
public users;
public userForm;

constructor(
private userService: UserService,
private formBuilder: FormBuilder
) {
this.userForm = this.formBuilder.group({
name: '',
age: ''
});
}

ngOnInit() {
this.refreshUsers();
}
refreshUsers() {
this.users = this.userService.users;
}

add(userForm) {
this.userService.add(userForm);
this.refreshUsers();
this.userForm.reset();
}
delete({ id }) {
this.userService.delete(id);
this.refreshUsers();
}
edit(user, { innerText: age }) {
const { id } = user;
this.userService.edit(id, { ...user, age });
this.refreshUsers();
}
toggle({ id }) {
this.userService.toggle(id);
this.refreshUsers();
}
}

Conclusion

In this third piece, we’ve developed a web application in which the project has been structured following the MVC architecture in which anemic models are used and the responsibility for the logic lies in the services.

It is important to highlight that the didactical of this post is that you understand the structuring of the project in different files with different responsibilities and how the view is totally independent of the model/service and the controller.

It’s also important to note that in this post, we’ve migrated the application from TypeScript to Angular, allowing us to forget about repetitive tasks related to the DOM that are the same in all Web applications we develop.

I recommend that you start from the first post related to JavaScript and understand the architecture used. The next step is to reinforce the code by applying TypeScript (in the second post) and finally review this post in which the code has been adapted to a framework.

This is not an Angular tutorial but about the evolution of our code from a technology to a framework.


The GitHub branch of this post is https://github.com/Caballerog/Angular-MVC-Users


Originally published at https://www.carloscaballero.io on November 12, 2019.

Better Programming

Advice for programmers.

Carlos Caballero

Written by

Hi! My name is Carlos Caballero and I’m PhD. in Computer Science from Málaga, Spain. Teaching developers and degree/master computer science how to be experts!

Better Programming

Advice for programmers.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade