Part 6. Clock-in/out System: Basic frontend

This post is part of a Series of post which I'm describing a clock-in/out system if you want to read more you can read the following posts:


This is the first post about the frontend for our clock-in/out system, which already has a basic, functional backend working. The frontend will be developed using the JS framework Angular due to it being the best framework, in the sense of software architecture, (my intent isn't to unleash a war about which is the best/worst JavaScript framework). This is a post about my system, I'm sure that there are better solutions for developing this layer of my software :-).

The result is shown below

Well.... Let's go!

The first step is creating a new project using angular-cli:

[carlos@carlos-lenovo clock-in-out]$ ng new frontend
? Would you like to add Angular routing? No
? Which stylesheet format would you like to use? SCSS   [ http://sass-lang.com   ]

In our context, a routing is not required, because our frontend is not an SPA (OMG! So why are you using Angular then? Because this is a simple tutorial to show how to integrate NestJS + Angular).

The next step is to install several libraries that are dependencies in our code (Angular Material):

npm i @angular/cdk  @angular/animations  @angular/material

Our project has three important points:

  1. AppModule: This is the main module, which is responsible for launching the other modules.
  2. UserComponent: This component is used to add new users and their keys (it's only for admin purposes, although there isn't any security).
  3. TicketingComponent: This is the most important component, since this is the one which refreshes the screen with the information about the users who should be working in the building.

Now, I'm going to show and explain each of the modules.

AppModule

This is the module which is used to launch the other modules. In the following code I'm loading the modules:

  1. Angular Material:
    1. MatTableModule: The table that will show the list of users which are in the building.
    2. MatInputModule: Form's input that will be used to add the users-keys pair.
  2. RouterModule: This module will be used to load the clock-in/out and admin pages.
  3. FormsModule: This module is required to use template-driven forms in our project.
  4. BrowserModule and BrowserAnimationsModule: These are the modules required to use Angular in the browser (and the animations).
  5. HttpClientModule: This module will be used to communicate the frontend and the backend using the HTTP protocol.

The Route[] are the routes to load our components. In our case it's very easy, because the default path will load TicketingComponent and the path /user will load our admin page (UserComponent).

Lastly, we must declare our components in the AppModule: AppComponent, UserComponent and TicketingComponent

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RouterModule, Route } from '@angular/router';
import {
  MatTableModule,
  MatInputModule,
} from '@angular/material';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';
import { TicketingComponent } from './ticketing/ticketing.component';
import { UserComponent } from './user/user.component';

const routes: Route[] = [
  {
    path: '',
    component: TicketingComponent,
  },
  {
    path: 'user',
    component: UserComponent,
  },
];

@NgModule({
  declarations: [AppComponent, UserComponent, TicketingComponent],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    FormsModule,
    HttpClientModule,
    MatTableModule,
    MatInputModule,
    RouterModule.forRoot(routes),
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

The AppComponent is the bootstrap of our app. This component only runs the router-outlet.

app.component.ts

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

@Component({
  selector: 'app-root',
  template: '<router-outlet></router-outlet>',
})
export class AppComponent {}

Constants and environment

In any software we develop, we need different constants and environment variables, i.e, http://localhost:4200 is the traditional URI to develop an Angular app, although  you need to change the domain or the port when you deploy your app. For this purpose, Angular provides us with configuration to change between different environments.

So, the file AppSettings can define every our constants. The most important constant is the APIENDPOINT which is provide from the file envionment.

import { environment } from 'src/environments/environment';

export class AppSettings {
  static readonly TYPE_ACTION = {
    INPUT: 'input',
    OUTPUT: 'output',
  };
  static readonly DATE_FORMAT = 'DD/MM/YYYY HH:mm:ss';
  static readonly APIENDPOINT = environment.APIENDPOINT_BACKEND;
  static readonly APIENDPOINT_USER = `${AppSettings.APIENDPOINT}/user`;
  static readonly APIENDPOINT_USERS = `${AppSettings.APIENDPOINT}/users`;
}

The environment file is loaded by default when you're developing an Angular app:

export const environment = {
  production: false,
  APIENDPOINT_BACKEND: "http://localhost:3000"
};

The only difference in the environment.production.ts file is the APIENDPOINT_BACKEND constant, which contains the name of the machine on which our app is deployed (In our case, a docker container).

export const environment = {
  production: true,
  APIENDPOINT_BACKEND: "http://ClockBackend:3000"
};

Ticketing Component

The ticketing component is the most interesting piece of code in this project, due to it having been developing using RxJS to make the system in near-realtime. This example doesn't use redux, so the double data-binding is used to refresh the template, from the logic part. This component's template is the following.

<div>
  <span class="center">Absent</span>
  <span class="right"> {{ timestamp$ | async }}</span>
</div>
<div style="display:flex">
  <mat-table [dataSource]="middle((usersAbsent$ | async), 1)">


    <ng-container matColumnDef="name">
      <mat-header-cell *matHeaderCellDef> Employee </mat-header-cell>
      <mat-cell *matCellDef="let element"> {{element.name}} </mat-cell>
    </ng-container>


    <ng-container matColumnDef="room">
      <mat-header-cell *matHeaderCellDef> Room </mat-header-cell>
      <mat-cell *matCellDef="let element"> {{element.schedule[0].room}} </mat-cell>
    </ng-container>

    <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
    <mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
  </mat-table>

  <mat-table [dataSource]="middle((usersAbsent$ | async), 2)">
    <ng-container matColumnDef="name">
      <mat-header-cell *matHeaderCellDef> Name </mat-header-cell>
      <mat-cell *matCellDef="let element"> {{element.name}} </mat-cell>
    </ng-container>

    <ng-container matColumnDef="room">
      <mat-header-cell *matHeaderCellDef> Room </mat-header-cell>
      <mat-cell *matCellDef="let element"> {{element.schedule[0].room}} </mat-cell>
    </ng-container>

    <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
    <mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
  </mat-table>
</div>


<span class="center">PRESENT</span>
<div style="display:flex">
  <mat-table [dataSource]="middle((usersPresent$ | async), 1)">

    <ng-container matColumnDef="name">
      <mat-header-cell *matHeaderCellDef> Employee </mat-header-cell>
      <mat-cell *matCellDef="let element"> {{element.name}} </mat-cell>
    </ng-container>


    <ng-container matColumnDef="room">
      <mat-header-cell *matHeaderCellDef> Room </mat-header-cell>
      <mat-cell *matCellDef="let element"> {{element.schedule[0].room}} </mat-cell>
    </ng-container>

    <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
    <mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
  </mat-table>

  <mat-table [dataSource]="middle((usersPresent$ | async), 2)">

    <ng-container matColumnDef="name">
      <mat-header-cell *matHeaderCellDef> EMPLOYEE </mat-header-cell>
      <mat-cell *matCellDef="let element"> {{element.name}} </mat-cell>
    </ng-container>

    <ng-container matColumnDef="room">
      <mat-header-cell *matHeaderCellDef> Room </mat-header-cell>
      <mat-cell *matCellDef="let element"> {{element.schedule[0].room}} </mat-cell>
    </ng-container>

    <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
    <mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
  </mat-table>
</div>


You may note that the template has several Observable$, which are rendered using the pipe async. For example, in the following code, the span tag redered the result of the subscription of the observable timestamp$. This pipe is a syntactic sugar for the traditional subscribe method. You can read more about this pipe in the official documentation.

<span class="right"> {{ timestamp$ | async }}</span>

Other interesting point of the template is the use of the component Material Datatable which can received a set of data to be rendering in a table or an observable using the input [source] but in our case the Datatable will receive a set of data (after that the pipe async will do its job). Futhermore, the data are show in two different tables, so the data are separated in two sets using the method middle.

The CSS is quite simple and is shown in the following code:

mat-table {
  width: 49.5%;
  border-right: 1px solid black;
}
mat-row {
  min-height: 24px;
}
.center {
  font-family: 'Raleway', sans-serif;
  font-size: 18px;
  font-weight: 800;
  text-align: center;
  text-transform: uppercase;
}
.right {
  float: right;
}

table {
  width: 100%;
}

.mat-form-field {
  font-size: 14px;
  width: 100%;
}

td,
th {
  width: 25%;
}

Although this post isn't about CSS, you must know NEVER to use id's in styling your web (you can read more about this fact in dev.to, CSSWizard and developingdesigns).

Our CSS file is simple, since it's only styling our table (which must have a width of 49.50% and different typography size adjustments).

I will now reveal the most interesting piece of code in this post, the TicketingComponent, which has the subsequent attributes:

import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, timer, interval } from 'rxjs';
import { switchMap, map, retryWhen } from 'rxjs/operators';
import * as moment from 'moment';
import { AppSettings } from 'src/app/app.settings';

@Component({
  selector: 'ticketing',
  templateUrl: './ticketing.component.html',
  styleUrls: ['./ticketing.component.scss'],
})
export class TicketingComponent {
  public usersAbsent$: Observable<any>;
  public usersPresent$: Observable<any>;
  public timestamp$: Observable<any>;
  displayedColumns: string[] = ['name', 'room'];
  ....

The description of each of our attributes is:

  • usersAbsent$: This is the observable which contains the list of User which are not in the building.
  • usersPresent$: This is the observable which contains the list of User which are in the building.
  • timestamp$: This is the observable which contains the timestamp from the server.
  • displayedColumns: The array of columns which will be shown in the table.

It is very important to remember that we're using observables in our code to provide us with the power of stream's manipulation by using the RxJS operators. These observables are subscribed using the pipe async in the template.

Our next step is the component constructor, where the real magic appears! You must understand the streams in RxJS to be able to understand the following code:

constructor(public httpClient: HttpClient) {
    const interval$ = timer(0, 3000);
    const data$ = interval$.pipe(
      switchMap(() =>
        this.httpClient.get<{ users: any[]; timestamp: number }>(
          AppSettings.APIENDPOINT_USERS,
        ),
      ),
      retryWhen(() => interval(3000)),
    );
    const users$ = data$.pipe(map(({ users }) => users));
    this.timestamp$ = data$.pipe(
      map(({ timestamp }) =>
        moment.unix(timestamp).format(AppSettings.DATE_FORMAT),
      ),
    );
    this.usersPresent$ = users$.pipe(
      map(users => users.filter(this.isPresent)),
    );
    this.usersAbsent$ = users$.pipe(map(users => users.filter(this.isAbsent)));
  }

This code does the following:

The observable interval$ is created using the timer operator, which in turn will trigger a call each 3000 ms. In the subsequent line of the code you can see how the observable data$ is created from the observable interval$ which runs a http request using the httpClient service.

The get request then return an object comprising a list of users and a timestap (from the server). Two sections of this code fragment are particularly relevant:

  1. The operator switchMap is used to cancel an unfinished request when a new request is made (to avoid several request being made at the same time).
  2. The operator retryWhen is used to handle the server errors. For example, if the connection is lost in the client or server you will need to retry again the request. So, when the code has an error, the request will be retried in 3000 ms.

Ok, now the observable data$ has a stream containing information about the list of users and timestamp. The observable users$ is created from the observable data$ which does a destructuration in each stream of data (this is the reason for the mapoperator being there). If you have understood the previous code, you can imagine how the observable timestamp$ is created. This timestamp is in unix format, we need to transform it to the DATE_FORMAT (DD/MM/YYYY).

Perhaps you can now imagine how the usersPresent$ and usersAbsent$ observables are created from the users$ observable. For these observables you must use the RxJS map operator to create a new observable, using the Array.prototype filter method. The last step is creating the private isPresent and isAbsent methods, which are shown subsequently:

 private isPresent(user: any): boolean {
    return (
      user.auths.length > 0 &&
      user.auths[0].reader === AppSettings.TYPE_ACTION.INPUT
    );
  }
  private isAbsent(user: any): boolean {
    return (
      user.auths.length === 0 ||
      (user.auths.length > 0 &&
        user.auths[0].reader === AppSettings.TYPE_ACTION.OUTPUT)
    );
  }

These methods basically check if the user has been authorized by the system, and whether the action is INPUT or OUTPUT.

So, the complete controller code is the following:

import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, timer, interval } from 'rxjs';
import { switchMap, map, retryWhen } from 'rxjs/operators';
import * as moment from 'moment';
import { AppSettings } from 'src/app/app.settings';

@Component({
  selector: 'ticketing',
  templateUrl: './ticketing.component.html',
  styleUrls: ['./ticketing.component.scss'],
})
export class TicketingComponent {
  public usersAbsent$: Observable<any>;
  public usersPresent$: Observable<any>;
  public timestamp$: Observable<any>;
  displayedColumns: string[] = ['name', 'room'];

  constructor(public httpClient: HttpClient) {
    const interval$ = timer(0, 3000);
    const data$ = interval$.pipe(
      switchMap(() =>
        this.httpClient.get<{ users: any[]; timestamp: number }>(
          AppSettings.APIENDPOINT_USERS,
        ),
      ),
      retryWhen(() => interval(3000)),
    );
    const users$ = data$.pipe(map(({ users }) => users));
    this.timestamp$ = data$.pipe(
      map(({ timestamp }) =>
        moment.unix(timestamp).format(AppSettings.DATE_FORMAT),
      ),
    );
    this.usersPresent$ = users$.pipe(
      map(users => users.filter(this.isPresent)),
    );
    this.usersAbsent$ = users$.pipe(map(users => users.filter(this.isAbsent)));
  }

  public middle(users: any[], number: number): any[] {
    if (!users) {
      return [];
    }
    const mid = Math.ceil(users.length / 2);
    return number === 1 ? users.slice(0, mid) : users.slice(mid);
  }
  private isPresent(user: any): boolean {
    return (
      user.auths.length > 0 &&
      user.auths[0].reader === AppSettings.TYPE_ACTION.INPUT
    );
  }
  private isAbsent(user: any): boolean {
    return (
      user.auths.length === 0 ||
      (user.auths.length > 0 &&
        user.auths[0].reader === AppSettings.TYPE_ACTION.OUTPUT)
    );
  }
}

User Component

The last component of our basic frontend is the UserComponent, which is a simple form to add users and keys to our database. The idea to build this component is the same as the one used in the TicketingComponent. Therefore, the template to do the operation subscribes using the async pipe.

<ng-container *ngIf="users$ | async as users; else noUsers">
  <select [(ngModel)]="userID">
    <option *ngFor="let user of users" [value]="user.uid">{{ user.uid }}</option>
  </select>
  Key: <input [(ngModel)]="key" type="text" />
  <button (click)="save()">Save</button>
</ng-container>

<ng-template #noUsers>
  Finished!
</ng-template>

The template uses the if-else of the ng-container to show a message when nobody has a key.

The UserComponent code is the following:

import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, BehaviorSubject } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { AppSettings } from '../app.settings';

@Component({
  selector: 'user',
  templateUrl: 'user.component.html',
})
export class UserComponent {
  users$: Observable<{ uid: string }[]>;
  userID: string;
  key: string;
  update$ = new BehaviorSubject(true);
  constructor(private http: HttpClient) {
    this.users$ = this.update$.pipe(
      switchMap(() =>
        this.http.get<{ uid: string }[]>(AppSettings.APIENDPOINT_USER),
      ),
      map(users => (users.length === 0 ? null : users)),
    );
  }

  save() {
    const user = {
      uid: this.userID,
      key: this.key,
    };
    this.key = '';
    this.userID = '';

    this.http
      .post(AppSettings.APIENDPOINT_USER, user)
      .subscribe(() => this.update$.next(true));
  }
}

In this case, we've defined four relevant attributes:

  1. Observable users$ which contains the list of users' UID.
  2. The string userID which contains the userID selected from the template.
  3. The string key which is the key that will be assigned to the user.
  4. Observable/Subject update$ which allows us to know that the action updated was done successful.

The constructor is very similar to the constructor in the TicketingComponent, due to it recovering the list of users' UID from the backend, by using the switchMap and map operators.

Finally the save method makes a request POST to the backend with the object that the backend requires to save the information.

Conclusion

‌In this post I've explained my basic frontend, developed with Angular and RxJS to ensure a near real-time system (using polling as the technique to connect with the server).

Frontend in Angular

The GitHub project is https://github.com/Caballerog/clock-in-out.
The GitHub branch of this post is https://github.com/Caballerog/clock-in-out/tree/part6-basic-frontend.