Tool tips are common for providing hints on how to use different parts of a web app. It is easy to add and it helps users understand the app more. They’re also useful for display long text that would be too long.
Ngx-Bootstrap has tool tips built in as a component. It also has many other components that we can use. Adding tool tips using Ngx-Bootstrap is easy. See https://valor-software.com/ngx-bootstrap/#/tooltip for a full set of options for the tool tip. It can have text, position can change, also can change position according to screen size changes. The content can also be HTML. Trigger for tool tips can also be customized.
In this article, we will write an employee manager app that lets users enter their employee data in a form. Users can add, edit and delete their data with tool tips to guide them through the forms when they use the form.
To start the project, we install the Angular CLI if not installed already by running npm i -g @angular/cli
. Next we run the Angular CLI to create the project by typing:
ng new employee-manager
In the wizard, we choose to include routing and use SCSS as our CSS preprocessor.
Then we install some packages. We need the Ngx-Bootstrap for styling, modal, and the tool tips, as well as MobX to store the countries in a shared store. To install them, we run:
npm i ngx-bootatrap mobx mobx-angular
Next we create our components, classes and services. To do this, we run:
ng g component employeeForm
ng g component homePage
ng g service employees
ng g class employeeStore
ng g class employee
Now we are ready to write some code. In employee-form.component.html
, we replace the existing code with:
<form (ngSubmit)="save(employeeForm)" #employeeForm="ngForm">
<div class="form-group">
<label tooltip="Enter employee's name here" placement="right">Name</label>
<input
type="text"
class="form-control"
placeholder="Name"
#name="ngModel"
name="name"
[(ngModel)]="form.name"
required
/>
<div *ngIf="name?.invalid && (name.dirty || name.touched)">
<div *ngIf="name.errors.required">
Name is required.
</div>
</div>
</div>
<div class="form-group">
<label tooltip="Enter employee's address here" placement="right"
>Address</label
>
<input
type="text"
class="form-control"
placeholder="Address"
#address="ngModel"
name="address"
[(ngModel)]="form.address"
required
/>
<div *ngIf="address?.invalid && (address.dirty || address.touched)">
<div *ngIf="address.errors.required">
Address is required.
</div>
</div>
</div>
<div class="form-group">
<label tooltip="Enter employee's position here" placement="right"
>Position</label
>
<input
type="text"
class="form-control"
placeholder="Position"
#position="ngModel"
name="position"
[(ngModel)]="form.position"
required
/>
<div *ngIf="position?.invalid && (position.dirty || position.touched)">
<div *ngIf="position.errors.required">
Position is required.
</div>
</div>
</div>
<div class="form-group">
<label tooltip="Select employee's employment status" placement="right"
>Employment Status</label
>
<select
class="form-control"
#status="ngModel"
name="status"
[(ngModel)]="form.status"
required
>
<option value="Employed">Employed</option>
<option value="Terminated">Terminated</option>
</select>
<div *ngIf="status?.invalid && (status.dirty || status.touched)">
<div *ngIf="status.errors.required">
Status is required.
</div>
</div>
</div>
<button class="btn btn-primary">Save</button>
</form>
This is the template for the employee form for letting users add and edit employee data. We have name, address, position, and status fields, and they’re all set as required. Error messages are displayed when any of them aren’t filled in and submitted will be stopped if the user tries to submit without any of these data. The labels will show the tool tips when users hover over them. We set the placement
to right
so that they display on the right. When users click the Save button the save
function in the code is called.
Then in employee-form.component.ts
, we replace the existing code with:
import { Component, OnInit, Output, Input, SimpleChanges, EventEmitter } from '@angular/core';
import { employeeStore } from '../employee-store';
import { EmployeesService } from '../employees.service';
import { NgForm } from '@angular/forms';
import { Employee } from '../employee';
@Component({
selector: 'app-employee-form',
templateUrl: './employee-form.component.html',
styleUrls: ['./employee-form.component.scss']
})
export class EmployeeFormComponent implements OnInit {
form: Employee = <Employee>{};
@Output('saved') saved = new EventEmitter();
@Input() edit: boolean;
@Input() selectedEmployee: Employee;
store = employeeStore;
constructor(private employeeService: EmployeesService) { }
ngOnInit() {
}
ngOnChanges(changes: SimpleChanges) {
this.form = Object.assign({}, this.selectedEmployee);
}
save(employeeForm: NgForm) {
if (employeeForm.invalid) {
return;
}
if (this.edit) {
this.employeeService.editEmployee(this.form)
.subscribe(res => {
this.getEmployees()
this.saved.emit();
})
}
else {
this.employeeService.addEmployee(this.form)
.subscribe(res => {
this.getEmployees()
this.saved.emit();
})
}
}
getEmployees() {
this.employeeService.getEmployees()
.subscribe((res: Employee[]) => {
this.store.setEmployees(res);
})
}
}
This file has the functions that we called in the previous template like the save
function. We also have the Inputs to get the data from the home page, as well as an Output to emit a saved
event to the home page. Since we use the form for editing, we also need to pass in the selected entry with the selectedEmployee
Input, so that it can be edited. To update the form
object with the selectedEmployee
values, we copied the value whenever the selectedEmployee
Input changes.
In the save
function, we validate the form and call different functions for saving depending on whether the form is being used for adding or editing an entry. The latest entries will be populated in our MobX store by calling the getEmployees
function and the saved
event will be emitted once that’s done.
Next in home-page.component.html
, we replace the code with:
<ng-template #addTemplate>
<div class="modal-header">
<h2 class="modal-title pull-left">Add Employee</h2>
</div>
<div class="modal-body">
<app-employee-form (saved)="closeModals()"></app-employee-form>
</div>
</ng-template>
<ng-template #editTemplate>
<div class="modal-header">
<h2 class="modal-title pull-left">Edit Employee</h2>
</div>
<div class="modal-body">
<app-employee-form
[edit]="true"
(saved)="closeModals()"
[selectedEmployee]="selectedEmployee"
></app-employee-form>
</div>
</ng-template>
<h1 class="text-center">Employee Manager</h1>
<div class="btn-group" role="group" id="add-group">
<button
type="button"
class="btn btn-secondary"
(click)="openAddModal(addTemplate)"
>
Add Employee
</button>
</div>
<br />
<div class="card" *ngFor="let e of store.employees">
<div class="card-body">
<h5 class="card-title">{{ e.name }}</h5>
<p class="card-text">Position: {{ e.position }}</p>
<p class="card-text">Address: {{ e.address }}</p>
<p class="card-text">Employment Status: {{ e.status }}</p>
<button (click)="openEditModal(editTemplate, e)" class="btn btn-primary">
Edit
</button>
<button (click)="deleteEmployee(e.id)" class="btn btn-primary">
Delete
</button>
</div>
</div>
to add buttons for adding, editing, and deleting entries. The employee entries are displayed in the cards. The Edit and Delete buttons are displayed at the bottom of the cards. Also, we have the modals for adding and editing entries that we open with the Add and Edit buttons respectively.
In home-page.component.scss
, we add:
$margin: 10px;
#add-group {
margin-bottom: $margin;
}
button {
margin-right: $margin;
}
to add some margins to our buttons.
Next in home-page.component.ts
, we replace the existing code with:
import { Component, OnInit, TemplateRef } from '@angular/core';
import { employeeStore } from '../employee-store';
import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
import { EmployeesService } from '../employees.service';
import { Employee } from '../employee';
@Component({
selector: 'app-home-page',
templateUrl: './home-page.component.html',
styleUrls: ['./home-page.component.scss']
})
export class HomePageComponent implements OnInit {
addModalRef: BsModalRef;
editModalRef: BsModalRef;
selectedEmployee: Employee = <Employee>{};
store = employeeStore;
constructor(
private modalService: BsModalService,
private employeeService: EmployeesService
) { }
ngOnInit() {
this.getEmployees()
}
getEmployees() {
this.employeeService.getEmployees()
.subscribe((res: Employee[]) => {
this.store.setEmployees(res);
})
}
openAddModal(template: TemplateRef<any>) {
this.addModalRef = this.modalService.show(template);
}
openEditModal(template: TemplateRef<any>, employee: Employee) {
this.editModalRef = this.modalService.show(template);
this.selectedEmployee = employee;
}
closeModals() {
this.addModalRef && this.addModalRef.hide();
this.editModalRef && this.editModalRef.hide();
}
deleteEmployee(id) {
this.employeeService.deleteEmployee(id)
.subscribe((res: Employee[]) => {
this.getEmployees();
})
}
}
We have the openAddModal
and openEditModal
functions to open the Add and Edit modals. The closeModals
function is for closing the modals when things are saved in the app-employee-form
component. The deleteEmployee
function is for deleting the employee, and the getEmployees
is used for getting the entries when the page loads when items are deleted and out the items in our store so every component can access it.
In app-routing.module.ts
, we put:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomePageComponent } from './home-page/home-page.component';
const routes: Routes = [
{ path: '', component: HomePageComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
so users can see the pages we just added when they click on the links or enter the URLs.
Next in app.component.html
, we put:
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" routerLink="/">Employee Manager</a>
<button
class="navbar-toggler"
type="button"
data-toggle="collapse"
data-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a class="nav-link" routerLink="/">Home </a>
</li>
</ul>
</div>
</nav>
<div class="page">
<router-outlet></router-outlet>
</div>
to add the links to our pages and expose the router-outlet
so users can see our pages.
Then in app.component.scss
, we add:
.page {
padding: 20px;
}
nav {
background-color: aquamarine !important;
}
to add padding to our pages and change the color of our Bootstrap navigation bar.
In app.module.ts
, we replace the existing code with:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomePageComponent } from './home-page/home-page.component';
import { EmployeeFormComponent } from './employee-form/employee-form.component';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { TooltipModule } from 'ngx-bootstrap/tooltip';
import { ModalModule } from 'ngx-bootstrap/modal';
import { EmployeesService } from './employees.service';
@NgModule({
declarations: [
AppComponent,
HomePageComponent,
EmployeeFormComponent
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule,
HttpClientModule,
ModalModule.forRoot(),
TooltipModule.forRoot()
],
providers: [
EmployeesService
],
bootstrap: [AppComponent]
})
export class AppModule { }
we add our components, services, and libraries that we use in our app. Note that we add each component in Ngx-Bootstrap separately so we only add what we need.
In employee.ts
, we add:
export class Employee {
public id: number;
public name: string;
public address: string;
public position: string;
public status: string;
}
to add types to our employee form model.
Then in employeeStore.ts
, we add:
import { observable, action } from 'mobx-angular';
import { Employee } from './employee';
class EmployeeStore {
@observable employees: Employee[] = [];
@action setEmployees(employees) {
this.employees = employees;
}
}
export const employeeStore = new EmployeeStore();
to create the MobX store to get our components share the data. Whenever we call this.store.setEmployees
our components we set the currencies data in this store since we added the @action
decorator before it. When we call this.store.employees
in our component code we are always getting the latest value from this store since has the @observable
decorator. We add the Employee[]
type to employees
so we don’t have to guess the fields available.
Then in employee.service.ts
, we replace the existing code with:
import { Injectable } from '@angular/core';
import { environment } from 'src/environments/environment';
import { HttpClient } from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class EmployeesService {
constructor(private http: HttpClient) { }
getEmployees() {
return this.http.get(`${environment.apiUrl}/employees`);
}
addEmployee(data) {
return this.http.post(`${environment.apiUrl}/employees`, data);
}
editEmployee(data) {
return this.http.put(`${environment.apiUrl}/employees/${data.id}`, data);
}
deleteEmployee(id) {
return this.http.delete(`${environment.apiUrl}/employees/${id}`);
}
}
so that we can make HTTP requests to our back end to get, save and delete the user’s employee entries.
Next in environment.prod.ts
and environment.ts
, we replace the existing code with:
export const environment = {
production: true,
apiUrl: 'http://localhost:3000'
};
to add our API’s URL.
Finally, in index.html
, we replace the code with:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Employee Manager</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
rel="stylesheet"
/>
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<script
src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
crossorigin="anonymous"
></script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"
integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1"
crossorigin="anonymous"
></script>
<script
src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"
integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM"
crossorigin="anonymous"
></script>
</head>
<body>
<app-root></app-root>
</body>
</html>
to add the Bootstrap CSS and JavaScript dependencies into our app, as well as changing the title.
After all the work, we can run ng serve
to run the app.
To start the back end, we first install the json-server
package by running npm i json-server
. Then, go to our project folder and run:
json-server --watch db.json
In db.json
, change the text to:
{
"employees": [
]
}
So we have the employees
endpoints defined in the requests.js
available.