Skip to content

devon4ng adding custom functionality

devonfw-core edited this page Dec 16, 2021 · 10 revisions

devon4j adding Custom Functionality

Now we have a fully functional blank project. All we have to do now is to create the components and services which will compose our application.

First, we are going to develop the views of the app through its components, then we will create the services with the logic, security and back-end connection.

Creating Components

ℹ️

You have already learned about creating Components in devon4ng here.
You can go back and read that section again to refresh your memory.

Our app is going to consist of 3 main views:

  • Login

  • Register

  • ViewQueue

To navigate between these views/components, we are going to implement routes using the Angular Router.

To see our progress, move to the root folder of the angular project and run ng serve -o again. This will recompile and publish our client app to http://localhost:4200. Angular will keep watching for changes, so whenever we modify the code, the app will automatically reload.

Root Component

app.component.ts inside angular/src/app will be our root component, so we don’t have to create a new file yet. We are going to add elements to the root component that will be common no matter what view will be displayed.

ℹ️

You have already learned about the Root Component in devon4ng here.
You can go back and read that section again to refresh your memory.

This applies to the header element which will be on top of the window and on top of all other components. If you want, you can read more about Covalent layouts, which we are going to use a lot from now on, for every view component.

ℹ️

You have already learned about Covalent Layouts in devon4ng here.
You can go back and read that section again to refresh your memory.

We don’t really need anything more than a header, so we are going to use the simplest layout for this purpose; the nav view.

In order to be able to use Covalent and Angular Material we are going to create a core module, which we will import into every other module where we want to use Covalent and Angular Material. First, we create a folder called shared in the angular/src/app directory. Inside there we are going to create a file called core.module.ts and will fill it with the following content:

import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { CommonModule } from '@angular/common';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import {
  MatAutocompleteModule,
  MatButtonModule,
  MatButtonToggleModule,
  MatCardModule,
  MatCheckboxModule,
  MatChipsModule,
  MatDatepickerModule,
  MatDialogModule,
  MatExpansionModule,
  MatGridListModule,
  MatIconModule,
  MatInputModule,
  MatListModule,
  MatMenuModule,
  MatNativeDateModule,
  MatPaginatorModule,
  MatProgressBarModule,
  MatProgressSpinnerModule,
  MatRadioModule,
  MatRippleModule,
  MatSelectModule,
  MatSidenavModule,
  MatSliderModule,
  MatSlideToggleModule,
  MatSnackBarModule,
  MatSortModule,
  MatTableModule,
  MatTabsModule,
  MatToolbarModule,
  MatTooltipModule,
} from '@angular/material';
import { CdkTableModule } from '@angular/cdk/table';
import {
  CovalentChipsModule,
  CovalentLayoutModule,
  CovalentExpansionPanelModule,
  CovalentDataTableModule,
  CovalentPagingModule,
  CovalentDialogsModule,
  CovalentLoadingModule,
  CovalentMediaModule,
  CovalentNotificationsModule,
  CovalentCommonModule,
} from '@covalent/core';

@NgModule({
  imports: [
    RouterModule,
    BrowserAnimationsModule,
    MatCardModule,
    MatButtonModule,
    MatIconModule,
    CovalentMediaModule,
    CovalentLayoutModule,
    CdkTableModule,
  ],
  exports: [
    CommonModule,
    CovalentChipsModule,
    CovalentLayoutModule,
    CovalentExpansionPanelModule,
    CovalentDataTableModule,
    CovalentPagingModule,
    CovalentDialogsModule,
    CovalentLoadingModule,
    CovalentMediaModule,
    CovalentNotificationsModule,
    CovalentCommonModule,
    CdkTableModule,
    MatAutocompleteModule,
    MatButtonModule,
    MatButtonToggleModule,
    MatCardModule,
    MatCheckboxModule,
    MatChipsModule,
    MatDatepickerModule,
    MatDialogModule,
    MatExpansionModule,
    MatGridListModule,
    MatIconModule,
    MatInputModule,
    MatListModule,
    MatMenuModule,
    MatNativeDateModule,
    MatPaginatorModule,
    MatProgressBarModule,
    MatProgressSpinnerModule,
    MatRadioModule,
    MatRippleModule,
    MatSelectModule,
    MatSidenavModule,
    MatSliderModule,
    MatSlideToggleModule,
    MatSnackBarModule,
    MatSortModule,
    MatTableModule,
    MatTabsModule,
    MatToolbarModule,
    MatTooltipModule,
    HttpClientModule,
  ],
  declarations: [],
  providers: [
    HttpClientModule
  ],
})
export class CoreModule {}
ℹ️

This CoreModule has almost every module of the different components for Angular Material and Covalent Teradata. If you decide to use a component that is not included yet, you need to add the corresponding module here.

Remember that we need to import this CoreModule module into the AppModule and inside every module of the different components that use Angular Material and Covalent Teradata. If a component does not have a module, it will be imported in the AppModule and hence automatically have the CoreModule. Our app.module.ts should have the following content:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';

// Application components and services
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { CoreModule } from './shared/core.module';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    CoreModule,
  ],
  providers: [
  ],
  bootstrap: [AppComponent],
  schemas: [ CUSTOM_ELEMENTS_SCHEMA ]
})
export class AppModule { }
ℹ️

Even if we setup module correctly the HTML file can give us this red flag: "If td-layout is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the @NgModule.schemas of this component to suppress this message." To solve this we add "schemas: [ CUSTOM_ELEMENTS_SCHEMA ]" inside the @NgModule of all the affected modules.

ℹ️

Remember this step because you will have to repeat it for every other component from Teradata you use in your app.

Now we can use this layout, so let’s implement it in app.component.html. Use the following code:

<td-layout-nav>             <!-- Layout tag-->
  <div td-toolbar-content>
    Jump The Queue          <!-- Header container-->
  </div>
  <h1>
    app works!              <!-- Main content-->
  </h1>
</td-layout-nav>
ℹ️

You have already learned about Toolbars in devon4ng here.
You can go back and read that section again to refresh your memory.

ℹ️

You have already learned about Toolbars in devon4ng here.
You can go back and read that section again to refresh your memory.

Once this is done, our app should have a header and "app works!" should appear in the body of the page:

Root Header

To go a step further, we have to modify the body of the root component because it should be the output of the router. Now it’s time to prepare the routing system.

First, we need to create a component to show as default which will be our access view. We will modify it later. Stop ng serve and run:

ng generate component form-login

It will add a folder to our project with all the files needed for a component. Now we can move on to the router task again. Run ng serve again to continue the development.

Let’s create a module that navigates between components when the Router checks for routes. The file app-routing.module.ts was created automatically when we chose to include Angular Routing during project creation and we only need to modify it now:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { FormLoginComponent } from './form-login/form-login.component';

const appRoutes: Routes = [
  { path: 'FormLogin', component: FormLoginComponent},        // Redirect if url path is /FormLogin.
  { path: '**', redirectTo: '/FormLogin', pathMatch: 'full' } // Redirect if url path do not match any other route.
];

@NgModule({
  imports: [
    RouterModule.forRoot(
      appRoutes,
      { enableTracing: true }, // <-- debugging purposes only
    ),
  ],
  exports: [RouterModule],
})
export class AppRoutingModule {}
ℹ️

You have already learned about Routing in devon4ng here.
You can go back and read that section again to refresh your memory.

Finally, we remove the <h1>app works!</h1> from app.component.html and replace it with a <router-outlet></router-outlet> tag. The final result of our root component will look like this:

Root Router

As you can see, now the body content is the HTML of FormLoginComponent. This is because we told the Router to redirect to login form when the path is /FormLogin, but also, redirect to it by default if any of the other routes match the given path.

For now we are going to leave the header like this. In the future we will separate it into another component inside a layout folder.

LoginForm Component

As we have already created this component from the section before, let’s move on to building the template of the login view.

First, we need to add the Covalent Layout and the card to the file form-login.component.html:

<td-layout>
  <mat-card>
    <mat-card-title>Login</mat-card-title>
  </mat-card>
</td-layout>

This will add a gray background to the view and a card on top of it with the title "Login" now that we have the basic structure of the view.

Now we are going to add this image:

JumpTheQueue Logo Image

In order to have it available, save it in the following path of the project: angular/src/assets/images/ and name it jumptheq.png.

The final code with the form added will look like this:

<td-layout>
  <mat-card>
    <img mat-card-image src="assets/images/jumptheq.png">
  </mat-card>
</td-layout>

This code will give us as a result similar to this:

Form Login

This is going to be the container for the login.
Now we will continue with the second component: Login.

Login Component

Our first step will be to create the component in the exact same way we created the FormLogin component but this time we are going to generate it in a new folder called components inside formlogin. Putting every child component inside that folder will allow us to keep a good and clear structure. In order to do this, we use the command:

ng generate component form-login/components/login

After Angular/CLI has finished generating the component, we have to create two modules, one for the form-login and one for the login:

1.- We create a new file called login-module.ts in the login root:

import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CoreModule } from 'src/app/shared/core.module';
import { LoginComponent } from './login.component';

@NgModule({
  imports: [CommonModule, CoreModule],
  providers: [],
  declarations: [LoginComponent],
  exports: [LoginComponent],
  schemas: [ CUSTOM_ELEMENTS_SCHEMA ]
})
export class LoginModule {}

2.- We create a new file called form-login-module.ts in the form-login root:

import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormLoginComponent } from './form-login.component';
import { CoreModule } from '../shared/core.module';
import { LoginModule } from './components/login/login-module';

@NgModule({
  imports: [CommonModule, CoreModule, LoginModule],
  providers: [],
  declarations: [FormLoginComponent],
  exports: [FormLoginComponent],
  schemas: [ CUSTOM_ELEMENTS_SCHEMA ]
})
export class FormLoginModule {}

As you can see, the LoginModule is already added to the FormLoginModule. Once this is done, we need to remove the FormLoginComponent and the LoginComponent from the declarations since they are already declared in their own modules. Then add the FormLoginModule. This will be done inside AppModule:

...
import { FormLoginModule } from './form-login/form-login-module';
...
  declarations: [
    AppComponent,
  ]

  imports: [
    BrowserModule,
    FormLoginModule,
    CoreModule,
    AppRoutingModule
  ]
...
ℹ️

This is done so the form-login (container/wrapper) and the login stay separated allowing us to reuse the login without having the card around in other views.

After this, we modify the login.component.html and add the form:

<form #loginForm="ngForm" layout-padding>
    <div layout="row" flex>
        <mat-form-field flex>
                <input matInput placeholder="Email" ngModel email name="username" required>
        </mat-form-field>
    </div>
    <div layout="row" flex>
        <mat-form-field flex>
            <input matInput placeholder="Password" ngModel name="password" type="password" required>
        </mat-form-field>
    </div>
    <div layout="row" flex>
    </div>
    <div layout="row" flex layout-margin>
        <div layout="column" flex>
            <button mat-raised-button [disabled]="!loginForm.form.valid">Login</button>
        </div>
        <div layout="column" flex>
            <button mat-raised-button color="primary">Register</button>
        </div>
    </div>
</form>
ℹ️

You have already learned about Forms in devon4ng here.
You can go back and read that section again to refresh your memory.

This form contains two input containers from Material. The containers enclose the input with the properties listed above.

We also need to add a button to send the information and redirect to the QueueViewer or show an error if something went wrong in the process. But for the moment, as we neither have another component nor the auth service yet, we will implement the button visually, as well as the validator to disable it if the form is not correct. We will tackle the on-click-event later.

As a last step we will add this component to the form-login-component.html:

<td-layout>
    <mat-card>
        <img mat-card-image src="assets/images/jumptheq.png">
        <app-login></app-login>
    </mat-card>
</td-layout>

Now you should see something like this:

JumpTheQueue Login Screen

With two components already created, we need to use the router to navigate between them. Following the application flow of events, we are going to add a navigate function to the register button. When we press it, we will be redirected to our future register component.

Register Component

First, we are going to generate the register component via:

ng generate component register`

This will create our component so we can start working on it. Turning back to login.component.html we have to modify these lines of code:

<form (ngSubmit)="submitLogin()" #loginForm="ngForm" layout-padding>
...
<button mat-raised-button type="submit" [disabled]="!loginForm.form.valid">Login</button>
...
<button mat-raised-button (click)="onRegisterClick()" color="primary">Register</button>

Two events were added. First, when we submit the form, the method submitLogin() is going to be called. Second, when the user clicks the button (click) will send an event to the function onRegisterClick(). This function should be inside login.component.ts which is going to be created now:

  ...
  import { Router } from '@angular/router';
  ...
  constructor(private router: Router) { }
  ...
  onRegisterClick(): void {
    this.router.navigate(['Register']);
  }

  submitLogin(): void {
  }

We need to inject an instance of the Router object and declare it with the name router in order to use it in the code, as we did with onRegisterClick(). Doing this will use the navigate function and redirect to the next view. In our case, it will redirect using the route we are going to define in app.routing.module.ts:

...
import { RegisterComponent } from './register/register.component';
...
const appRoutes: Routes = [
  { path: 'FormLogin', component: FormLoginComponent},          // Redirect if url path is /FormLogin.
  { path: 'Register', component: RegisterComponent},            // Redirect if url path is /Register.
  { path: '**', redirectTo: '/FormLogin', pathMatch: 'full' }   // Redirect if url path do not match any other route.
];
...
ℹ️

You have already learned about Dependency Injection in devon4ng here.
You can go back and read that section again to refresh your memory.

Now we are going to imitate the login to shape our register.component.html:

<form layout-padding (ngSubmit)="submitRegister()" #registerForm="ngForm">
  <div layout="row" flex>
      <mat-form-field flex>
        <input matInput placeholder="Email" ngModel email name="username" required>
      </mat-form-field>
  </div>
  <div layout="row" flex>
      <mat-form-field flex>
        <input matInput placeholder="Password" ngModel name="password" type="password" required>
      </mat-form-field>
  </div>
  <div layout="row" flex>
      <mat-form-field flex>
        <input matInput placeholder="Name" ngModel name="name" required>
      </mat-form-field>
  </div>
  <div layout="row" flex>
      <mat-form-field flex>
        <input matInput placeholder="Phone Number" ngModel name="phoneNumber" required>
      </mat-form-field>
  </div>
  <div layout-xs="row" flex>
      <div layout="column" flex>
        <mat-checkbox name="acceptedTerms" ngModel required>Accept Terms And conditions</mat-checkbox>
      </div>
  </div>
  <div layout-xs="row" flex>
      <div layout="column" flex>
        <mat-checkbox name="acceptedCommercial" ngModel required>I want to receive notifications</mat-checkbox>
      </div>
  </div>
  <div layout="row" flex>
  </div>
  <div layout="row" flex>
      <div layout="column" flex="10">
        </div>
      <div layout="column" flex>
          <button mat-raised-button type="submit" [disabled]="!registerForm.form.valid">Register</button>
      </div>
      <div layout="column" flex="10">
      </div>
  </div>
</form>

Now that we have a minimum of navigation flow inside our application, we are going to generate our first service using the command:

ng generate service register/services/register

This will create a folder "services" inside "register" and create the service itself. Services are where we keep the logic that connects to our database and fetches data which is going to be used by our component.ts.

In order to use the service, we are going to create some interface models. Let’s create a folder called backendModels inside "shared" and inside this folder a file called interfaces.ts in which we are going to add the model interfaces that will match our back-end:

export class Visitor {
    id?: number;
    username: string;
    name: string;
    password: string;
    phoneNumber: string;
    acceptedCommercial: boolean;
    acceptedTerms: boolean;
    userType: boolean;
}
export class VisitorArray {
    content: Visitor[];
}
ℹ️

You have already learned about creating new services in devon4ng here.
You can go back and read that section again to refresh your memory.

If we take a closer look, we can see that id has a ? behind it. This indicates that the id is optional.

ℹ️

At this point we are going to assume that you have finished the devon4j part of this tutorial, or have at least downloaded the project and have the back end running locally on http://localhost:8081.

After doing this, we are going to add an environment variable with our base-URL for the REST services. This way we won’t have to change every URL when we switch to production. Inside environments/environment.ts we add:

export const environment: {production: boolean, baseUrlRestServices: string} = {
  production: false,
  baseUrlRestServices: 'http://localhost:8081/jumpthequeue/services/rest'
};

Now in the service, we are going to add a registerVisitor method.

To call the server in this method we are going to inject the Angular HttpClient class from @angular/common/http. This class is the standard used by Angular to perform HTTP calls. The register call demands a Visitor model which we created in the interfaces file. We are going to build a POST call and send the information to the proper URL of the server service. The call will return an observable:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Visitor} from 'src/app/shared/backendModels/interfaces';
import { Observable } from 'rxjs';
import { environment } from 'src/environments/environment';

@Injectable({
  providedIn: 'root'
})
export class RegisterService {

  private baseUrl = environment.baseUrlRestServices;

  constructor(private http: HttpClient) { }

  registerVisitor(visitor: Visitor): Observable<Visitor> {
    return this.http.post<Visitor>(`${this.baseUrl}` + '/visitormanagement/v1/visitor', visitor);
  }
}

This method will send our model to the back-end and return an Observable that we will use on the component.ts.

ℹ️

You have already learned about Observables and RxJs in devon4ng here.
You can go back and read that section again to refresh your memory.

Now we are going to modify register.component.ts to call this service:

import { Component, OnInit } from '@angular/core';
import { RegisterService } from './services/register.service';
import { Visitor } from '../shared/backendModels/interfaces';
import { Router } from '@angular/router';
import { MatSnackBar } from '@angular/material/snack-bar';

@Component({
  selector: 'app-register',
  templateUrl: './register.component.html',
  styleUrls: ['./register.component.scss']
})
export class RegisterComponent implements OnInit {

  constructor(private registerService: RegisterService, private router: Router, public snackBar: MatSnackBar) { }

  submitRegister(formValue): void {
    const visitor: Visitor = new Visitor();
    visitor.username = formValue.username;
    visitor.name = formValue.name;
    visitor.phoneNumber = formValue.phoneNumber;
    visitor.password = formValue.password;
    visitor.acceptedCommercial = formValue.acceptedCommercial;
    visitor.acceptedTerms = formValue.acceptedTerms;
    visitor.userType = false;

    this.registerService.registerVisitor(visitor).subscribe(
      (visitorResult: Visitor) => console.log(JSON.stringify(visitorResult)), // When call is received
      (err) =>  this.snackBar.open(err.error.message, 'OK', {
        duration: 5000,
      }), // When theres an error
    );
  }

  ngOnInit() {
  }
}

In this file we injected RegisterService and Router to use them. Then, inside the method submitRegister, we created a visitor that we are going to pass to the service. We called the service method registerVisitor, we passed the visitor and we subscribed to the Observable<Visitor>, which we returned from the service. This subscription allows us to control three things:

  1. What to do when the data is received.

  2. What to do when there’s an error.

  3. What to do when the call is complete.

Finally, we modify the register.component.html to send the form values to the method:

...
<form layout-padding (ngSubmit)="submitRegister(registerForm.form.value)" #registerForm="ngForm">
...
Register Page

Using the method and taking a look at the browser console, we should see the visitor model being returned.

Creating Services

Now that we registered a Visitor, it’s time to create 3 important services:

  • AuthService

  • AuthGuardService

  • LoginService

The AuthService will be the one that contains the login info, the AuthGuardService will check if a user is authorized to use a component (via the canActivate method), and the LoginService will be used to fill the AuthService.

ℹ️

To keep this tutorial simple, we are going to perform the password check client side. THIS IS NOT CORRECT! Usually, you would send the username and password to the back-end, check that the values are correct, and create a corresponding token which you would pass in the header and use it inside the AuthService — checking with some interceptors that the token is both in the AuthService and in the request.

Login, Auth and AuthGuard Services

We are going to create the 3 services via ng generate service <path>:

  1. LoginService via:
    ng generate service form-login/components/login/services/login

  2. Auth service via:
    ng generate service core/authentication/auth

  3. AuthGuard service via:
    ng generate service core/authentication/auth-guard

After generating the services, we are going to start modifying the interfaces. Inside angular/src/app/shared/backendModels/interfaces we are going to add Role, FilterVisitor, Pageable and a Sort interface:

...
export class FilterVisitor {
    pageable: Pageable;
    username?: string;
    password?: string;
}

export class Pageable {
    pageSize: number;
    pageNumber: number;
    sort: Sort[];
}

export class Sort {
    property: string;
    direction: string;
}

export class Role {
    name: string;
    permission: number;
}
ℹ️

As you can see, we added a Pageable, since a lot of the search methods in the back-end are using SearchCriterias. These need pageables which specify a paseSize and pageNumber. Also, we can see that in this case FilterVisitor uses a pageable and adds parameters as a filter (username and password), which are optional.

Then we are going to create a config.ts file inside the root (angular/app). We are going to use that file to set up default config variables, for example: role names with their permission number, default pagination settings etc. For now we are just adding the roles:

export const config: any = {
    roles: [
        { name: 'VISITOR', permission: 0 },
        { name: 'BOSS', permission: 1 },
    ],
};

After that, we are going to modify the auth.service.ts:

import { Injectable } from '@angular/core';
import { find } from 'lodash';
import { Role } from 'src/app/shared/backendModels/interfaces';
import { config } from 'src/app/config';

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  private logged = false;
  private user = '';
  private userId = 0;
  private currentRole = 'NONE';
  private token: string;

  public isLogged(): boolean {
    return this.logged;
  }

  public setLogged(login: boolean): void {
    this.logged = login;
  }

  public getUser(): string {
    return this.user;
  }

  public setUser(username: string): void {
    this.user = username;
  }

  public getUserId(): number {
    return this.userId;
  }

  public setUserId(userId: number): void {
    this.userId = userId;
  }

  public getToken(): string {
    return this.token;
  }

  public setToken(token: string): void {
    this.token = token;
  }

  public setRole(role: string): void {
    this.currentRole = role;
  }

  public getPermission(roleName: string): number {
    const role: Role = <Role>find(config.roles, { name: roleName });
    return role.permission;
  }

  public isPermited(userRole: string): boolean {
    return (
      this.getPermission(this.currentRole) === this.getPermission(userRole)
    );
  }
}

We will use this service to fill it with information from the logged-in user once the user logs in. This will allow us to check the information of the logged-in user in any way necessary.

ℹ️

You have already learned about Authentication in devon4ng here.
You can go back and read that section again to refresh your memory.

Now we are going to use this class to fill the auth-guard.service.ts:

import { Injectable } from '@angular/core';
import {
  CanActivate,
  Router,
  ActivatedRouteSnapshot,
  RouterStateSnapshot,
} from '@angular/router';
import { AuthService } from './auth.service';

@Injectable({
  providedIn: 'root'
})
export class AuthGuardService implements CanActivate {
  constructor(
    private authService: AuthService,
    private router: Router,
  ) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot,
  ): boolean {
    if (this.authService.isLogged() && this.authService.isPermited('VISITOR')) { // If its logged in and its role is visitor
      return true;
    }

    if (!this.authService.isLogged()) { // if its not logged in
      console.log('Error login');
    }

    if (this.router.url === '/') {  // if the router is the app route
      this.router.navigate(['/login']);
    }
    return false;
  }
}

This service will be slightly different because we have to implement an interface called CanActivate. It has a method called canActivate() returning a boolean. This method will be called when navigating to a specified route, and — depending on the return value of this implemented method — the navigation will proceed or be rejected.

ℹ️

You have already learned about Guards in devon4ng here.
You can go back and read that section again to refresh your memory.

Once this is done, the last step is to fill the login.service.ts. In this case, there’s going to be three methods:

  1. getVisitorByUsername(username: string):
    A method that recovers a single user corresponding to the email.

  2. login(username: string, password: string):
    A method, which is going to use the previous method, to check that the username and password match the form input and then fill the AuthService.

  3. logout():
    This is going to be used to reset the AuthService and log out the user.

Also, we see the first use of pipe and map:
pipe allows us to execute a chain of functions, then map allows us to return the single visitor instead of all the parameters that the server will send us.

import { map, tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Visitor, FilterVisitor, Pageable } from 'src/app/shared/backendModels/interfaces';
import { HttpClient } from '@angular/common/http';
import { environment } from 'src/environments/environment';
import { AuthService } from 'src/app/core/authentication/auth.service';
import { Router } from '@angular/router';
import { MatSnackBar } from '@angular/material/snack-bar';

@Injectable({
  providedIn: 'root'
})
export class LoginService {

    private baseUrl = environment.baseUrlRestServices;
    constructor(private router: Router, private http: HttpClient, private authService: AuthService, public snackBar: MatSnackBar) { }

    getVisitorByUsername(username: string): Observable<Visitor> {
        const filters: FilterVisitor = new FilterVisitor();
        const pageable: Pageable = new Pageable();

        pageable.pageNumber = 0;
        pageable.pageSize = 1;
        pageable.sort= [];
        filters.username = username;
        filters.pageable = pageable;
        return this.http.post<VisitorArray>(`${this.baseUrl}` + '/visitormanagement/v1/visitor/search', filters)
       .pipe(
            map(visitors => visitors.content[0]),
        );
    }

    login(username: string, password: string): void {
      // Checks if given username and password are the ones aved in the database
      this.getVisitorByUsername(username).subscribe(
          (visitorFound) => {
              if (visitorFound.username === username && visitorFound.password === password) {
                  this.authService.setUserId(visitorFound.id);
                  this.authService.setLogged(true);
                  this.authService.setUser(visitorFound.username);
                  if (visitorFound.userType === false) {
                      this.authService.setRole('VISITOR');
                      this.router.navigate(['ViewQueue']);
                  } else {
                      this.authService.setLogged(false);
                      this.snackBar.open('access error', 'OK', {
                          duration: 2000,
                        });
                  }
              } else {
                  this.snackBar.open('access error', 'OK', {
                      duration: 2000,
                    });
              }
          },
          (err: any) => {
            this.snackBar.open('access error', 'OK', {
              duration: 2000,
            });
          },
      );
    }

    logout(): void {
        this.authService.setLogged(false);
        this.authService.setUser('');
        this.authService.setUserId(0);
        this.router.navigate(['FormLogin']);
    }
}

If you remember the devon4j tutorial, we used Criteria in order to filter and to search the DB. The Criteria require a pageable and you can add extra parameters to get specific results. In getVisitorByUsername() you can see the creation of a FilterVisitor corresponding to the Criteria in the back-end. This FilterVisitor gets a Pageable and a username and will return a single result as soon as the POST call is performed. That’s why we return the first page and only a single result.

ℹ️

For the tutorial we are only considering the visitor side of the application. That’s why we setLogged(false) if it’s userType === true (BOSS side).

Then we add to the login-module.ts and LoginService:

...
import { LoginService } from './services/login.service';

@NgModule({
  ...
  providers: [LoginService],
  ...
})
...

After that, we are going to add the AuthGuard and the Auth into the shared/core-module.ts. This will allow us to employ these two services when importing the core module avoiding having to provide these services in every component:

...
  providers: [
    HttpClientModule,
    AuthService,
    AuthGuardService,
  ],
...

You need to import these modules as well, as shown earlier.

Finally, we modify the login.component.html to send the form values to the login.component.ts like we did with the register form. Afterwards, we are going to modify the register.components.ts: When the visitor registers, we can log him in automatically to avoid any nuisances. Let’s start with the login.component.html:

...
<form (ngSubmit)="submitLogin(loginForm.form.value)" #loginForm="ngForm" layout-padding>
...

As you can see, in the form we just added, the values to the ngSubmit allow us to call the method submitLogin() within the logic, sending the loginForm.form.values which are the form’s input values. In the next step we are going to modify the login.components.ts, adding the submitLogin() method. This method calls the LoginService, providing the service with the necessary values received from the form (i.e. the loginFormValues).

...
import { LoginService } from './services/login.service';
...
export class LoginComponent implements OnInit {
  ...
  constructor(private router: Router, private loginService: LoginService) {
  }
  ...
  submitLogin(loginFormValues): void {
    this.loginService.login(loginFormValues.username, loginFormValues.password);
  }
}

Finally, in the register.components.ts we are going to inject the LoginService and use it to login the visitor after registering him. This will also send the user to the ViewQueue, which we will create and secure later in the tutorial.

import { LoginService } from '../form-login/components/login/services/login.service';
...
constructor(private registerService: RegisterService, private router: Router, public snackBar: MatSnackBar,
    private loginService: LoginService) { }
...
  submitRegister(formValue): void {
    ...
    this.registerService.registerVisitor(visitor).subscribe(
      (visitorResult: Visitor) => {
        this.loginService.login(visitorResult.username, visitorResult.password);
      },
      ...
    );
  }
...

Finishing Touches

Now we only need to generate two more components (header and view-queue) and services (AccessCodeService and QueueService) in order to finish the implementation of our JumpTheQueue app.

Separating Header from Layout

By separating the header on top of the page from the layout, we enable the reuse of this component and reach a better separation of concerns across our application. To do this, we are going to generate a new component inside angular/src/app/layout/header via:

ng generate component layout/header

Now we are going to add it to the main view app.component.html:

...
  <div td-toolbar-content flex>
    <app-header layout-align="center center" layout="row" flex></app-header>
  </div> <!-- Header container-->
...

After adding the component to the header view (app-header), we are going to modify the HTML of the component (header.component.html) and the logic of the component (header.component.ts). As a first step, we are going to modify the HTML, adding an icon as a button, which checks whether or not the user is logged in via *ngIf by calling the auth service’s isLogged() method. This will make the icon appear only if the user is logged in:

Jump The Queue
<span flex></span>
<button mat-icon-button mdTooltip="Log out" (click)=onClickLogout() *ngIf="authService.isLogged()">
  <mat-icon>exit_to_app</mat-icon>
</button>

In the header logic (header.component.ts) we are simply going to inject the AuthService and LoginService, then we are going call logout() from LoginService in the OnClickLogout(). Finally, the AuthService is needed because it’s being used by the HTML template to control if the user is logged in with isLogged():

...
  constructor(private authService: AuthService, private loginService: LoginService) { }
...
  onClickLogout(): void {
    this.loginService.logout();
  }
...

Separating components will allow us to keep the code clean and easy to work with.

ViewQueue Component

For the last view, we are going to learn how to use our Observables on the HTML template directly without having to subscribe() to them.

First, we are going to generate the component via:

ng generate component view-queue

After that, we are going to include the component in the app-routing.module.ts, also adding the guard, to only allow users that are VISITOR to see the component. It is important to insert the following code before { path: '**', redirectTo: '/FormLogin', pathMatch: 'full' }:

...
const appRoutes: Routes = [
  ...
  { path: 'ViewQueue',
    component: ViewQueueComponent,
    canActivate: [AuthGuardService]}, // Redirect if url path is /ViewQueue, check if canActivate() with the AuthGuardService.
  ...
];
...

Now in order to make this view work, we are going to do these things:

  1. Add the Queue and AccessCode interface in our angular/src/app/shared/backendModels/interfaces and their corresponding filters.

  2. Generate the QueueService and AccessCodeService and add the necessary methods.

  3. Modify the view-queue.component.html.

  4. Modify the logic of the component view-queue.component.ts.

First, we are going to add the necessary interfaces. We modify angular/src/app/shared/backendModels/interfaces.ts and add the FilterQueue, Queue, FilterAccessCode, AccessCode, QueueArray and AccessCodeArray. These are going to be necessary in order to communicate with the back-end.

...
export class FilterAccessCode {
    pageable: Pageable;
    visitorId?: Number;
    endTime?: string;
}

export class FilterQueue {
    pageable: Pageable;
    active: boolean;
}

export class AccessCode {
    id?: number;
    ticketNumber: string;
    creationTime: string;
    startTime?: string;
    endTime?: string;
    visitorId: number;
    queueId: number;
    content: any;
}

export class Queue {
    id?: number;
    name: string;
    logo: string;
    currentNumber: string;
    attentionTime: string;
    minAttentionTime: string;
    active: boolean;
    customers: number;
    content: any;
}

export class QueueArray {
    content: Queue[];
}

export class AccessCodeArray {
    content: [{
        accessCode: AccessCode
    }];
}
...

AccessCode and Queue Services

After this is done, we are going to generate the AccessCodeService and the QueueService:

ng generate service view-queue/services/Queue

ng generate service view-queue/services/AccessCode

Once this is done, we are going to modify them and add the necessary methods:

  • For the AccessCodeService we are going to need a full CRUD:

import { Injectable } from '@angular/core';
import { AuthService } from 'src/app/core/authentication/auth.service';
import { Router } from '@angular/router';
import { HttpClient } from '@angular/common/http';
import { AccessCode, Pageable, FilterAccessCode } from 'src/app/shared/backendModels/interfaces';
import { Observable } from 'rxjs';
import { environment } from 'src/environments/environment';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class AccessCodeService {

  private baseUrl = environment.baseUrlRestServices;

  constructor(private router: Router, private http: HttpClient, private authService: AuthService) { }

  getCurrentlyAttendedAccessCode(): Observable<AccessCode> {
    const filters: FilterAccessCode = new FilterAccessCode();
    const pageable: Pageable = new Pageable();

    filters.endTime = null;
    pageable.pageNumber = 0;
    pageable.pageSize = 1;
    filters.pageable = pageable;
    return this.http.post<AccessCodeArray>(`${this.baseUrl}` + '/accesscodemanagement/v1/accesscode/cto/search', filters)
    .pipe(
        map(accesscodes => {
          if (!accesscodes.content[0]) {  // if theres no response it means theres noone in the queue
            return null;
          } else {
            if (accesscodes.content[0].accessCode.startTime != null) {
              // if start time is not null it means that hes being attended
              return accesscodes.content[0].accessCode;
            } else {
              // noone being attended
              return null;
            }
          }
        }),
     );
  }

  getVisitorAccessCode(visitorId: number): Observable<AccessCode> {
    const filters: FilterAccessCode = new FilterAccessCode();
    const pageable: Pageable = new Pageable();

    pageable.pageNumber = 0;
    pageable.pageSize = 1;
    filters.visitorId = visitorId;
    filters.pageable = pageable;
    return this.http.post<AccessCodeArray>(`${this.baseUrl}` + '/accesscodemanagement/v1/accesscode/cto/search', filters)
    .pipe(
      map(accesscodes => {
        if (accesscodes.content[0]) {
          return accesscodes.content[0].accessCode;
        } else {
          return null;
        }
      }),
    );
  }

  deleteAccessCode(codeAccessId: number) {
    this.http.delete<AccessCode>(`${this.baseUrl}` + '/accesscodemanagement/v1/accesscode/' + codeAccessId + '/').subscribe();
  }

  saveAccessCode(visitorId: number, queueId: number) {
    const accessCode: AccessCode = new AccessCode();
    accessCode.visitorId = visitorId;
    accessCode.queueId = queueId;
    return this.http.post<AccessCode>(`${this.baseUrl}` + '/accesscodemanagement/v1/accesscode/', accessCode);
  }
}

In the methods getCurrentlyAttendedAccessCode and getVisitorAccessCode we can see the use of Pageable and FilterAccessCode to match the Criteria in the back-end like we explained in previous steps. In this case, the getVisitorAccessCode method will be used to see if the visitor has an AccessCode and the getCurrentlyAttendedAccessCode is going to recover the first AccessCode of the queue.

  • For the QueueService we are only going to need to find the active queue:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
import { Observable } from 'rxjs';
import { Queue, FilterQueue, Pageable } from 'src/app/shared/backendModels/interfaces';
import { environment } from 'src/environments/environment';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class QueueService {

  private baseUrl = environment.baseUrlRestServices;

  constructor(private router: Router, private http: HttpClient) { }

  getActiveQueue(): Observable<Queue> {
    const filters: FilterQueue = new FilterQueue();
    filters.active = true;
    const pageable: Pageable = new Pageable();
    pageable.pageNumber = 0;
    pageable.pageSize = 1;
    pageable.sort = [];
    filters.pageable = pageable;
    return this.http.post<QueueArray>(`${this.baseUrl}` + '/queuemanagement/v1/queue/search', filters)
    .pipe(
         map(queues => queues.content[0]),
     );
  }
}

Now we are going to create the template view-queue.component.html (which will use this data) and we will also introduce a new concept: async pipes in templates.

<td-layout *ngIf="{
  accessCodeAttended: accessCodeAttended$ | async,
  accessCodeVisitor: accessCodeVisitor$  | async,
  queue: queue$ | async
} as data;">
  <div *ngIf="data.queue">
    <mat-card>
    <img mat-card-image src="assets/images/jumptheq.png">

      <div *ngIf="data.accessCodeVisitor">
        <div class="text-center row">
          <h1 style="margin-bottom:10px;" class="text-left text-xl push-md">Your Number:</h1>
        </div>
        <div class="text-center row">
          <h1 style="font-size: 75px; margin:0px;" class="text-center text-xxl push-left-md">{{data.accessCodeVisitor.ticketNumber}}</h1>
        </div>
        <div style="border-bottom: 2px solid black;" class="row">
          <p class="push-left-md">Currently estimate time: 10:00:00</p>
        </div>
      </div>
      <div class="text-center">
        <div class="text-center row">
          <h1 style="margin-bottom:10px;" class="text-left text-xl push-md">Currently Being Attended:</h1>
        </div>
        <div class="row">
          <h1 style="font-size: 100px" class="text-center text-xxl push-lg">{{data.accessCodeAttended?.ticketNumber}}</h1>
        </div>
      </div>
      <div style="border-top: 2px solid black;" class="pad-bottom-lg pad-top-lg text-center row" *ngIf="data.accessCodeVisitor === null">
        <button mat-raised-button (click)="onJoinQueue(data.queue.id)" color="primary" class="text-upper">Join the queue</button>
      </div>
    </mat-card>
    <div *ngIf="data.accessCodeVisitor" style="margin: 8px;" class="row text-right">
        <button mat-raised-button (click)="onLeaveQueue(data.accessCodeVisitor.id)" color="primary" class="text-upper">Leave the queue</button>
    </div>
  </div>
  <div *ngIf="data.queue === null || (data.queue !== null && data.queue.active === false)" class="row">
    <h1 style="font-size: 50px" class="text-center text-xxl push-lg">The queue is not active try again later</h1>
  </div>
</td-layout>

If you watch closely, the starting td-layout has an *ngIf inside it. This *ngIf allows us to asynchronously pipe the observables that we will assign in the next steps. This solution avoids having to use subscribe() (as it subscribes automatically) and — as a result — we don’t have to worry about where to unsubscribe() from the observables.

In this HTML, we give *ngif another use: We use it to hide certain panels. Using accessCodeVisitor, we hide the ticket number panel and the "leave the queue"-button and show the button to join the queue. On the contrary we can hide the ticket number and the "leave the queue"-button and only show the "join the queue"-button.

ℹ️

In this case, since we are using HTTP and the calls are finite, there wouldn’t be any problems if you don’t unsubscribe() from their corresponding observables. However, if — for example — we use an observable to keep track of an input and subscribe() to it but not controlling the unsubscribe() method, the app could end up containing a memory leak. This is because — every time we visit the component with the input — it is going to create another subscription without unsubscribing from the last one.

Finally, to adapt the async pipe, the ngOnInit() method inside view-queue.component.ts now does not subscribe to the observable. In its place, we equal the queue variable directly to the observable, so we can load it using *ngIf.

import { Component, OnInit } from '@angular/core';
import { AccessCode, Queue } from '../shared/backendModels/interfaces';
import { Observable, timer } from 'rxjs';
import { AccessCodeService } from './services/access-code.service';
import { switchMap } from 'rxjs/operators';
import { AuthService } from '../core/authentication/auth.service';
import { QueueService } from './services/queue.service';

@Component({
  selector: 'app-view-queue',
  templateUrl: './view-queue.component.html',
  styleUrls: ['./view-queue.component.scss']
})
export class ViewQueueComponent implements OnInit {

  accessCodeAttended$: Observable<AccessCode>;
  accessCodeVisitor$: Observable<AccessCode>;
  queue$: Observable<Queue>;

  constructor(private accessCodeService: AccessCodeService, private queueService: QueueService, private authService: AuthService) { }

  ngOnInit() {
     // Every minute we are going to update accessCodeAttended$ starting instantly
    this.accessCodeAttended$ = timer(0, 60000).pipe(
      // we switchMap and give it the value necesary from the accessCodeService
      switchMap(() => {
        return this.accessCodeService.getCurrentlyAttendedAccessCode();
      })
    );
    this.accessCodeVisitor$ = this.accessCodeService.getVisitorAccessCode(this.authService.getUserId());
    this.queue$ = this.queueService.getActiveQueue();
  }

  onJoinQueue(queueId: number): void {
    this.accessCodeVisitor$ = this.accessCodeService.saveAccessCode(this.authService.getUserId(), queueId);
  }

  onLeaveQueue(accessCodeId: number): void {
    this.accessCodeService.deleteAccessCode(accessCodeId);
    this.accessCodeVisitor$ = null;
  }
}

In this last component we assign the Observables when the component is initiated. After that, when clicking the "join the queue"-button, we assign a new Observable called AccessCode to the accessCodeVisitor$. Finally, when we leave the queue, we delete the AccessCode and set the accessCodeVisitor to null. Since we are using an async pipe, every time we modify the status of the Observables, they are going to update the template.

Queue Page with Access Code
Queue Page without Access Code

This is all on how to build your own devon4ng application. Now it’s up to you to add features, change styles and do everything you can imagine doing with this app.

As a final step to complete the tutorial, however, we are going to run the app outside of our local machine by deploying it.


Next Chapter: Deploy your devon4ng App

Clone this wiki locally