-
Notifications
You must be signed in to change notification settings - Fork 45
devon4ng 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.
ℹ️
|
You have already learned about creating Components in devon4ng here. |
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.
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. |
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. |
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 |
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 |
ℹ️
|
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 have already learned about Toolbars in devon4ng here. |
Once this is done, our app should have a header and "app works!" should appear in the body of the page:
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. |
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:
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.
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:
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:
This is going to be the container for the login.
Now we will continue with the second component: Login.
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 |
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. |
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:
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.
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. |
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. |
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. |
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:
-
What to do when the data is received.
-
What to do when there’s an error.
-
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">
...
Using the method and taking a look at the browser console, we should see the visitor model being returned.
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 |
We are going to create the 3 services via ng generate service <path>
:
-
LoginService
via:
ng generate service form-login/components/login/services/login
-
Auth
service via:
ng generate service core/authentication/auth
-
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 |
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. |
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. |
Once this is done, the last step is to fill the login.service.ts
. In this case, there’s going to be three methods:
-
getVisitorByUsername(username: string)
:
A method that recovers a single user corresponding to the email. -
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 theAuthService
. -
logout()
:
This is going to be used to reset theAuthService
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 |
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);
},
...
);
}
...
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.
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.
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:
-
Add the
Queue
andAccessCode
interface in ourangular/src/app/shared/backendModels/interfaces
and their corresponding filters. -
Generate the
QueueService
andAccessCodeService
and add the necessary methods. -
Modify the
view-queue.component.html
. -
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
}];
}
...
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 |
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.
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
This documentation is licensed under the Creative Commons License (Attribution-NoDerivatives 4.0 International).