Only this pageAll pages
Powered by GitBook
1 of 41

SMU Portal

Loading...

Backend

Loading...

Loading...

Loading...

Loading...

Loading...

Frontend

Loading...

Book Module Tutorial

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Resources

Loading...

Loading...

Loading...

Troubleshooting

Loading...

Configuring the Mailing Service

Introduction

In SMU Portal, there are many features that use the mailing service. For instance, when you create an account you will receive an email containing a link that to confirm your email address, or when making a box reservation you will receive an email containing the reservation's details.

Generating OAuth 2.0 Tokens

OAuth 2.0 is a standard that allows your application to use a set of tokens (strings) instead of using actual login credentials (i.e. email and password). This represents an import security advantage since each set of tokens is only bound or valid for a specific action (or set of actions). In this case, our set will only allow sending emails, so once compromised, the tokens cannot do as much harm as actual account credentials.

Click on Select Project.

This window will pop out. Select New Project and you will be redirected project creation page.

Specify a name and then click Create.

If everything went well, you will be redirected to the dashboard and receive a notification once your project has been created.

Select your project on the top left, click on the burger menu icon, and choose Home > Dashboard.

Click on Go to Project Settings at the bottom of the Project Info card.

Click on the sidebar menu, and choose API & Service > Credentials and you will be redirected to the Credentials page.

Press Create Credentials and select OAuth client ID.

You will be confronted with this page, click on Configure Consent Screen.

Choose External as a User Type and click on Create.

Choose a name for your application and press save.

Once that is finished, click on Credentials from the sidebar, then on Create Credentials and select OAuth client ID as we have done earlier.

Choose Web Application as an Application Type, specify a Name and add https://developers.google.com/oauthplayground to the Authorised redirect URIs list.

Now, your OAuth client has been successfully created, save Client ID and Client Secret, we will need them later on.

Paste https://mail.google.com/ in the scope text box of Step 1 Select & authorize APIs and click on Authorize APIs

You will be redirected to this page, click on Advance and Go to <PROJECT-NAME> (in this case <PROJECT-NAME> is CS321).

Click on Allow

Press Exchange authorization code for tokens, and it will generate a Refresh token and an Access Token.

Finally, go to your .env file and add your Client ID, Client Secret, Refresh, and Access Tokens. SMTP_USER should be your Gmail's email.

# NodeMailer #
# SMTP_USER should contain an email not a username
SMTP_SERVICE = gmail
SMTP_USER = email
SMTP_TYPE = OAuth2

# Google OAuth
G_CLIENT_ID = <CLIENT-ID>
G_CLIENT_SECRET = <CLIENT-SECRET>
G_ACCESS_TOKEN = <ACCESS-TOEKN>
G_REFRESH_TOKEN = <REFRESH-TOEKN>
G_REDIRECT_URI = https://developers.google.com/oauthplayground

The mailing service is implemented with nodemailer coupled with Gmail. In fact, Google offers a free service to send emails through Gmail.

Before starting, you will need a Gmail account, we recommend that you create a new one instead of using your personal email for extra security measure, you can signup .

First, we need to create a new project in the . Make sure that you are connected using your newly created account.

Visit , click on the gear icon, and paste your Client ID and Client Secret, also make sure that you have identical options as shown in the image, then press close.

SMTP
here
Google Cloud Platform
OAuth 2.0 Playground
Google cloud platform dashboard
Selecting a project
Creating a new project
Choosing a name for the project
Project successfully created
Accessing the project's dashboard
Modifying the project's settings
Creating credentials for the project
Creating an OAuth Client ID
Configuring Consent Screen
Selectin User Type
Choosing a name for the application
Creating a OAuth client ID
OAuth 2.0 configuration
Signing in to the application
Allowing the application to use Gmail API
Generating Refresh and Access Tokens

Introduction

The purpose of this document is to provide you with the necessary knowledge to start working on the project, both the backend and the frontend part. First, we will go through a brief example that will make use of the frameworks and libraries involved in setting up and running the backend server​. Then, we will configure the frontend and introduce the technologies used.

Seeding the Database

In this section, we will fill our database with the necessary data for the application to function properly

To seed the database, we will take advantage of a file called seed.js located at models/seed.js. Running this file creates all the roles and applications that are already implemented in SMU Portal. After executing npm run seed we should get the following output:

~/smuportal-backend (master)
$ npm run seed

> smuportal@0.0.0 seed ~/smuportal-backend
> node models/seed.js

Database connection established!
Roles seeded successfully
Apps seeded successfully
Seeding terminated... Database connection closed.
You can now safely run `npm start`

Database seeding is the initial seeding of a database with data. Seeding a database is a process in which an initial set of data is provided to a database when it is being installed. It is especially useful when we want to populate the database with data we want to develop in future. This is often an automated process that is executed upon the initial setup of an application. The data can be or necessary data such as an initial administrator account.

Database seeding. (2020, July 10). Retrieved from

dummy data
https://en.wikipedia.org/wiki/Database_seeding

Starting the Backend

If you have successfully completed the previous sections, your .env file should look like this:

.env
# APP #
SITE_NAME = SMUPortal
APP_SECRET = YgTP8SVHvLYWTn4E3aaM9tXPw8x-lInKCfFO10ZQWBo
REFRESH_TOKEN = 1UKfOmU5rlhan2wnkyjQGkK2sNaJCE-rPS-hOls5Wlk
TOKEN_EXPIRES_IN = 1h

# Email domain names should be seperated by a vertical bar (used in regex)
EMAIL_DOMAINS = medtech.tn|msb.tn|smu.tn

# DB #
DB_URL = <DB-CONNECTION-STRING>

# NodeMailer #
# SMTP_USER should contain an email not a username
SMTP_SERVICE = gmail
SMTP_USER = <EMAIL-ADDRESS>
SMTP_TYPE = OAuth2

# Google OAuth
G_CLIENT_ID = <CLIENT-ID>
G_CLIENT_SECRET = <CLIENT-SECRET>
G_ACCESS_TOKEN = <ACCESS-TOEKN>
G_REFRESH_TOKEN = <REFRESH-TOEKN>
G_REDIRECT_URI = https://developers.google.com/oauthplayground

Do not forget to replace all placeholders (e.g. <CLIENT-ID>) with their correct values.

We are all set to start the backend code.

First, install the dependencies using npm i command.

~/smuportal-backend (master)
npm i

Once that is finished, you can start the server using npm start and you should get the following output

~/smuportal-backend (master)
npm start

> smuportal@0.0.0 start ~/smuportal-backend
> nodemon server.js

[nodemon] 2.0.4
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node server.js`
Server is up
Connection to DB established!

That's it! The backend server is now running. If you are comfortable enough with backend technologies you can start developing, or you can browse the following sections where we go through a small example of a backend service.

Happy coding 🎉!

If for some reason you got a different output, please make sure that you have correctly followed the instructions from previous sections or you can open an .

issue

Creating a MongoDB Database

The first step is to create a MongoDB database. Since this project is about collaborative development, we will be hosting the database on the cloud rather than locally

Creating a MongoDB Account

Click on Start free and you will be redirected to the registration form.

Once that is finished, you will be asked to enter your organization's and project's names (in this case I kept the default proposed names) and the preferred language (though this step is not required).

Once you finished signing up, you will be redirected to the cluster creation page. Choose Shared Clusters as your plan (unless you feel fancy then you can choose a paid plan).

Creating a Cluster

In this step, you will choose a cloud provider (AWS, Google Cloud, or Azure) and the region depending on the provider, then click on the Create Cluster green button at the bottom right.

If everything went well, you will be redirected to your dashboard, and after a few minutes (~1-3min) your cluster should be ready.

Retrieving the Connection String

The last step is to retrieve the connection string (i.e. database URI) that you will use to connect the backend to the database. Click on the Connect button under the cluster's name.

Click on Allow Access from Anywhere, this will allow anyone to connect to your database given the string. The 0.0.0.0/0 IP address will be added to the whitelist. Don't forget to click on Add Ip Address to save.

Next, you will need to create a user that has permission to connect to the database. For security reasons, it is always recommended to use the auto-generated password. Once you are done, click on Create Database User

Then, click on Choose a connection method, and select Connect your application

Select Node.js as the driver and 3.6 or later as the version.

In the grey box, a string under the following format will be located.

mongodb+srv://<username>:<password>@[URL-STRING]/<dbname>?retryWrites=true&w=majority

Where <username> is the database user's username (in this case it's mootez), <password> is the password for the newly created user, and <dbname> is the database name that we will create. You can copy the string and save it elsewhere and click on the Close button.

Creating a Database

For the final part, we will create a database in our cluster.

Click on your cluster's name and navigate to the Collections tab. Click on Add my Own Data and specify a name and a collection for your database (collections are analogous to tables in relational databases, so a database can have multiple collections), in this case, I chose smu as a DB name and users as a first collection.

Don't forget to replace <dbname> in the connection string with the database's name that you have chosen (it is smu in this case).

Conclusion

You have now successfully created a MongoDB database. In the following section, we will show how to connect the backend code to communicate with the database.

First, visit MongoDB's to create a that will be hosted on the cloud.

MongoDB main page
MongoDB registration from
Setting up the MongoDB account
Selecting the free plan
Creating a starter cluster
Waiting for the cluster to be created
Cluster successfully created
Setting up the connection parameters
Successfully whitelisting all IP addresses
Adding a database user
Choosing a connection method
Specifying the connection driver and copying the database link
Creating a new database
Database successfully created
website
Cluster

Create Model

The first step is to identify the actors in our application. In this case, we can consider a user and a book as our models. Great! We can now create our classes in the models folder (We will not create a user class since it is already created).

We will use mongoose as our ORM.

Mongoose provides a straight-forward, schema-based solution to model your application data. It includes built-in type casting, validation, query building, business logic hooks and more, out of the box.

In our case, the book has three attributes, where we define for each attribute its type and other validations.

const mongoose = require("mongoose");

const bookSchema = new mongoose.Schema({
  Title: {
    type: String,
    required: true,
    lowercase: true,
    min: 2,
    max: 255
  },
  Author: {
    type: String,
    required: true,
    lowercase: true,
    min: 2,
    max: 255
  },
  ISBN: {
    type: Number,
    unique: true,
    required: true,
    min: 1000000000,
    max: 9999999999999,
    validate: {
      validator: Number.isInteger,
      message: "{VALUE} is not an integer value"
    }
  },

});

module.exports = mongoose.model("Book", bookSchema);

Note that each book has a unique ISBN and an ISBN is a number that has either 10 or 13 digits.

Mongoose. (n.d.). Retrieved from

https://mongoosejs.com/

Environment Variables

Introduction

Environment variables allow us to define some configuration that is decoupled from the code-base. It makes the application deployment easier in different environments (e.g. Heroku). Information that varies (hence the term variables) depending on the environment should be included in this file. Also, sensitive information like API tokens or database URLs should not be in this source code or known by unauthorized entities.

Environment Variables in Node.js

If you check the repository, you will find a file called, .env.example that contains variables used by the backend application.

Connection to the Database

First, fork the repository and clone it on your machine (if you face some issues during this step, you can refer back to the GitHub Guide).

To allow the backend to connect to your database, create a .env file that is an exact copy of .env.example and modify it as follows:

# DB #
DB_URL = <CONNECTION-STRING-FROM-THE-PREVIOUS-SECTION>

Note that changes in the .env are ignored by git and that is why we provide the .env.example file to serve as a skeleton or a specification for the variables. As a matter of fact the .env file should not be pushed.

Node.js provides a global variable called process.env which is an that contains all environment variables accessible to the developer. These environment variables are stored in a file called .env and, by convention, they are defined in a capitalized snake case, e.g DB_URL.

object

Introduction

In this quick tutorial, we will learn how to create a small application (Book Management System) from backend to frontend. We will go through all the steps required for you to make a new module that respects the preexisting architecture of the project.

Introduction

This part of the guide is a continuation of the previous one where we have created the backend for the Book Management System. In this section, we will provide a step by step guide on how to implement the frontend side of the BMS.

When you are done with this tutorial, your BMS should look like this:

Book Management System

Create Service

In this part we will create a service that contains our business logic. This service will be called book.service.js and should be located in the /services folder.

As we will be manipulated data in our service, we need to import our book model that we have defined earlier.

const Book = require("../models/Book")

We will then create a function bookService() that will contain all the methods that we want to implement.

In this tutorial, we will define three methods: getBooks() which will retrieve all the books in the database, addBook() which will be responsible for creating a new book in the database and deleteBook() which will delete a book from the database.

function bookService() {
  async function getBooks() {
    return Book.find({})
  }

  async function addBook(title, author, isbn) {
    return Book.create({Title: title, Author: author, ISBN: isbn})
  }

  async function deleteBook(isbn) {
    return Book.deleteOne({ISBN: isbn})
  }

  return {
    getBooks,
    addBook,
    deleteBook
  }
}

Finally, we only need to export our service. Exporting our service will allow us to use it elsewhere in the program (just like how we used the book model in the book service module).

module.exports = bookService;

Backend

We will begin by defining our database models, then we will create the appropriate services that contain the business logic of our application. Following that, we will expose API routes to forward the supported requests (and any information encoded in request URLs) to the proper controller functions. Finally, we will test our new API routes using Postman.

Create Route

Creating routes is a fairly straightforward process, we start by adding a new class book.js in the /routes/apifolder.

Then, we need to import the book service, book model and router.

const Router = require("express").Router;
const bookService = require("../../services/book.service")();
const Book = require("../../models/Book");

We should also add the following line:

const router = Router({
  mergeParams: true,
});

This allows us to inherit the parameters defined in req.params at the parent router.

Now, all we have to do is define our API endpoints. Let's create a route that will get us all the books:

router.get("/getBooks", async(req, res) => {
  try {
    const books = await bookService.getBooks();
    res.send(books);
  } catch(err) {
    res.json({ success: false, msg: "Failed to get books"});
  }
});

Since we need to retrieve the data about all the books registered in the database, we create an HTTP request with a GET method. The URL /getBooks is our API path.

In fact, our newly created API path is appended to a previously defined path at the parent router. This router is located in the root directory inapp.js.

Next, we add two more routes, one for adding a book and another one to delete a book:

//Route to create a book
router.post("/addBook", async(req, res, next) => {
    try {
        const {title, author, isbn} = req.body;
        await bookService.addBook(title, author, isbn);
        res.send({ success: true, msg: "Book Added"});
    } catch (err) {
        res.send({ success: false, msg: "Book not Added!", err})
    }
});

Here, our HTTP request method is a POST method since we will be sending data rather than retrieving it. We extract the title, author and ISBN from the request body. Then, we create an instance of a book with those attributes and we call the addBook() function that we defined in our book service. If the request succeeds, we will respond with the message "Book added" otherwise "Book not added!" as well as attach a success indicator (a boolean variable in our case).

// Route to delete a book
router.delete("/deleteBook/:isbn", async(req, res) => {
  try {
    const isbn = req.params.isbn;
    bookService.deleteBook(isbn);
    res.send({ success: true, msg: "Book deleted"})
  } catch (error) {
    res.send({ success: false, msg: "Book not added!"})
  }
});

In this function, we changed the method of request is set to DELETE as we will be removing an entry from the database. This time, we extract the ISBN from the path rather than the body.

As always, we finish by exporting our router.

module.exports = router;

Caution: /getBooksis not the complete API path. In this case the complete API path is

http://localhost:3000/api/user/getBooks

Create Module

The first step is to check our directory path. Once we made sure that we are in the smuportal-frontend, we navigate to applications folder. You can navigate through folders in the console using thecd <directory>command.

/smuportal-frontend/src/app/applications

Once you reached the path, run the command:

ng generate module book-management-system

This will create a new folder that contains a file "book-management-system.module.ts"

We also need to modify our "applications.module.ts" which in the parent folder. We will only modify the routes. We need to define that in our applications module we have added a new application (which is the book management system), and we give the path we want and its module.

import { NgModule } from "@angular/core";
import { Routes, RouterModule } from "@angular/router";
import { CommonModule } from "@angular/common";

const routes: Routes = [
  {
    path: "apps", children: [
      {
        path: "logistics", loadChildren: () =>
          import("./logistics-reservation/logistics-reservation.module").then(
            m => m.LogisticsReservationModule
          )
      },
      {
        path: "bms", loadChildren: () =>
          import("./book-management-system/book-management-system.module").then(
            m => m.BookManagementSystemModule
          )
      },
    ]
  }
];
@NgModule({
  declarations: [],
  imports: [CommonModule, RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class ApplicationsModule { }

Finally, we need to make some small modifications in the box booking since we modified our applications module. We need to modify the file "logistics-reservation.module.ts" by replacing line 14 with:

path: "",

In addition we need to change the file "box-list.component.ts" by replacing line 96 with:

this.router.navigate(["/apps/logistics/book-box", boxId]);

Test using Postman

Postman is a collaboration platform for API development. Postman's features simplify each step of building an API and streamline collaboration so you can create better APIs—faster.

Caution: Before proceeding, make sure your server is running. If not, runnpm start

Let's try to test our get call where we ask for a list of the books:

We select GET as request method, and we enter the URL that we previously specified in our router. As a result of sending the request (by pressing the blue button Send), the server returns an empty array since our database has no books yet.

The next step is therefore to create a new book:

Since adding books is done through a POST method, we select that in Postman. Then, we specify the URL and we also provide the necessary data in the body. For that, we also have to make sure that we click on Body, then raw and finally select JSON as format in the drop-down list whose text is highlighted in orange.

If we try to get the list of books again, the book should be displayed.

Finally, we can remove the book that we added. Note that we should add in the URL the ISBN of the book and select DELETE as the request method.

Starting the Frontend

First, install the dependencies using npm i command.

Once that is finished, you can start the frontend using ng serve and you should get the following output

Happy coding 🎉!

Now that we have created our API endpoints, we need to check whether they work properly or not. To do so, we will use .

The Collaboration Platform for API Development. (n.d.). Retrieved from

If for some reason you got a different output, you can open an .

Postman
https://www.postman.com/
~/CS321_TA/smuportal-frontend (master)
npm i
-/CS321_TA/smuportal-frontend (master)
$ ng serve
10% building 3/3 modules 0 activei 「wds」: Project is running at http://localhost:4200/webpack-dev-server/
i 「wds」: webpack output is served from /   
i 「wds」: 404s will fallback to //index.html

chunk {logistics-reservation-logistics-reservation-module} logistics-reservation-logistics-reservation-module.js, logistics-reservation-logistics-reservation-module.js.map (logistics-reservation-logistics-reservation-module) 41.5 kB  [rendered]
chunk {main} main.js, main.js.map (main) 272 kB [initial] [rendered]
chunk {polyfills} polyfills.js, polyfills.js.map (polyfills) 268 kB [initial] [rendered]
chunk {runtime} runtime.js, runtime.js.map (runtime) 9.07 kB [entry] [rendered]
chunk {styles} styles.js, styles.js.map (styles) 401 kB [initial] [rendered]
chunk {vendor} vendor.js, vendor.js.map (vendor) 6.13 MB [initial] [rendered]
Date: 2020-11-02T17:17:02.730Z - Hash: a3526aeabca7584994f9 - Time: 12504ms
** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **
i 「wdm」: Compiled successfully.

Frontend

After we are done testing the backend, we can move to the frontend part where we will consume our APIs, manipulate the data and display results. In Angular, the principle of separation of concerns is prevalent. We can distinguish three main concepts: modules, components and services. We will go through each one of them as we continue building our Book Management System.

issue

Display App in Dashboard

Adding the App to the Database

In order to display the app on the dashboard's main page, we need first to add it to the database. We have provided you with a script that allows you to perform this process in an easy fashion.

First step is to pull the script from the original repository (since most likely it is not available on your fork).

Open git bash or your preferred terminal, navigate to your backend repository and type the following commands:

git remote add upstream https://github.com/MedTech-CS321/smuportal-backend.git
git fetch upstream
git checkout --track upstream/app-creator

Finally, run npm run createApp and fill in the prompt with information related to your app, in this, we have done the following,

npm run createApp

> smuportal@0.0.0 createApp ~/smuportal-backend
> node models/appCreator.js

Database connection established!
Enter Application's Name: Book Management System
Enter Application's Description: Managing Lists of Books
Enter Feature n1 | (Leave Blank to Finish): Get Books
Enter Feature n2 | (Leave Blank to Finish): Add Books
Enter Feature n3 | (Leave Blank to Finish): Delete Books
Enter Feature n4 | (Leave Blank to Finish):
Choose Roles ( Current Roles: student,professor): professor,student
Book Management System Has Been Successfully Created!
Database connection closed

You can now safely run `npm start`

Caution: Do not forget to revert back to your master branch after creating your app.

Displaying the App on the Frontend Side

For this part you need to navigate to the frontend directory.

To display the app on the dashboard's main page, we need add an entry to the config.json file. You can find the file at src/assets/apps/

{
    "appName": "Book Management System",
    "iconUrl": "book.png",
    "path": "/apps/bms"
}

This is how theconfig.json file should be.

[
  {
    "appName": "Logistics Reservation",
    "iconUrl": "logistics-reservation.png",
    "path": "/apps/logistics"
  },
  {
    "appName": "Job App",
    "iconUrl": "job-applications.png",
    "path": "/apps/jobs"
  },
  {
    "appName": "Document and Services",
    "iconUrl": "docs.png",
    "path": "/apps/docs"
  },
  {
    "appName": "Admin Dashboard",
    "iconUrl": "users.png",
    "path": "/apps/admin"
  },
  {
    "appName": "Book Management System",
    "iconUrl": "book.png",
    "path": "/apps/bms"
  }
]

Finally, our app is displayed in the dashboard and the icon is clickable.

File Structure

Before we start coding, let's break down the project structure of the frontend. At first glance, the number of folders might seem intimidating. However, once we explain the purpose and content of each one of them, you will be able to see the logic behind this particular way of organizing the code.

Our code mainly resides in the src/ folder.

src/ contains three sub-folders namely: app/, assets/, environments/. The following is a table that explains the purpose of each folder.

APP SUPPORT FILES

PURPOSE

app/

assets/

Contains image and other asset files to be copied as-is when you build your application.

environments/

Contains build configuration options for particular target environments. By default there is an unnamed standard development environment and a production ("prod") environment. You can define additional target environment configurations.

As you have already noticed by looking at the frontend code-base, src/ is not the only folder at the root of our project. There are other folders such as dist/ and e2e/. These come much later in the development process and are not in the scope of this tutorial. We do, however, encourage you to read more about Angular's file structure in the link above.

The aforementioned file structure is not a personal architectural decision but rather the default angular skeleton application. On top of that, we have introduced a folder called shared/. This folder represents a module that contains sub-modules.

In our implementation, the shared module contains components (breadcrumb, home-message and interceptors and user-actions), services, models and pipes. For instance, the breadcrumb component might be used in the dashboard component as well as the profile component. In other words, we want to show the breadcrumb inside the profile page and the dashboard. Since the breadcrumb is being requested by more than one component, it should be part of the shared module.

Subfolders of src/

Contains the component files in which your application logic and data are defined. See details .

Source: Workspace and project file structure. (2020). Angular.

SharedModule is a conventional name for an with the components, directives, and pipes that you use everywhere in your app. This module should consist entirely of declarations, most of them exported.

NgModule FAQ. (2020). Angular.

Subfolders of shared/
https://angular.io/guide/file-structure
NgModule
https://angular.io/guide/ngmodule-faq#sharedmodule
below

Create Service

In this step of this tutorial, we will create a service that will consume the API route that we have defined in the backend such that we retrieve the list of the books. We will then modify our list-book component so that the list of books is displayed.

This process is somehow a little bit similar to the backend part, we start first by creating a book model. In the "src/app/shared/models" we create a new file "book.model.ts" and we add the following code:

export interface Book {
  _id: string;
  Title: string;
  Author: string;
  ISBN: string;
}

Also we add the following line in "index.ts" that is in "src/app/shared/models".

export * from "./book.model"

Now, through the console we navigate to the following path:

 src/app/shared/services/

and we type in the console:

ng generate service bms

We now need to consume the API route that we have created in the backend. To do so, we modify the "bms.service.ts" file to the following:

import { Injectable } from '@angular/core';
import { HttpClient } from "@angular/common/http";
import { Observable } from 'rxjs';
import {Book} from '../models/book.model'

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

  constructor(private httpClient: HttpClient) { }

  getBooks(): Observable<Book[]> {
    return this.httpClient.get<Book[]>("http://localhost:3000/api/user/getBooks");
  }
}

We need to check to add in our index.ts that is in "src/app/shared/services".

export * from "./bms.service";

Add a Book

As adding a book consists of both making a backend call to add an entry to the database and updating the view, we will have to modify our BMS service and module. We will also create a component called bms-add-book and update previously defined components for navigability.

Create Book List Component

We need to generate a component for our application. In our console, navigate to the book management folder that we have created earlier. Your path should look similar to:

/smuportal-frontend/src/app/applications/book-management-system

Once reached, we are ready to create our component. Angular allows us to generate a component with a simple command:

ng generate component list-books

We only need to define our component in the module we created. Your book management system module should become the following:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Routes } from "@angular/router";
import { ListBooksComponent } from './list-books/list-books.component';


const routes: Routes = [
  {
    path: "list",
    component: ListBooksComponent
  },
];

@NgModule({
  declarations: [],
  declarations: [ListBooksComponent],
  imports: [
    CommonModule
  ]
})
export class BookManagementSystemModule { }

Finally, we create a new file "index.ts" in the list-books folder and add the following line:

export * from "./list-books.component"

Create Main Component

In this part, we create a new component which will be the main page for our app. We start by checking in our console that we are in the correct directory:

/smuportal-frontend/src/app/applications/book-management-system

We generate our component by typing in the console:

ng generate component bms-main-page

Now we need to make sure that our book management system module is updated:

import { CommonModule } from '@angular/common';
import { RouterModule, Routes } from "@angular/router";
import { ListBooksComponent } from './list-books/list-books.component';
import { BmsMainPageComponent } from './bms-main-page/bms-main-page.component';

const routes: Routes = [
  {
    path: "",
    component: BmsMainPageComponent
  },
  {
    path: "list",
    component: ListBooksComponent
  },
];

@NgModule({
  declarations: [ListBooksComponent, BmsMainPageComponent],
  imports: [
    CommonModule,
    RouterModule.forChild(routes),
  ],
  exports: [RouterModule]
})
export class BookManagementSystemModule { }

Finally, in our book management folder, we create a new file "index.ts" and add the following lines:

export * from "./list-books"
export * from "./bms-main-page"

Defining Concepts

In this part we will define some terms and concepts that will repeatedly mention in this section. It is crucial to know what you are doing (or at least have a general idea) instead of blindly writing code (even if it works!).

Component

A component is the main building block of an Angular application, it is composed of: an HTML template that describes what to render on the page, a TS (TypeScript) class that defines the behavior of the component and a CSS selector that defines how the component is used within a template.

To put things into perspective, here is the main page of the Book Management System:

It is divided into multiple components, where each component has its own responsibility (and it can be divided even more).

We have a navbar component, that sits above another component called BmsMainPageComponent which itself is also containing another component, ListBooksComponent This feature allows us to reuse parts of code systematically all over the project, like the navbar for instance, without the need of rewriting it each time.

Modules

A module is a mechanism that allows to group components, services (which we will define later on in this part), pipes, etc. It is a container for a cohesive block of code that is dedicated to a specific domain. Remember that a good design should be loosely coupled and highly cohesive! In this case, the module is the whole Book Management System.

We can export some elements from the module so that they can be accessed by other modules or keep them hidden so that they are only used internally.

Services

Angular services are singleton object that are instantiated only once during the life cycle of an Angular application. They are used to to share business logic, data and functions across components and modules of the application.

References

BMS Main Page

Now these are broad definitions, as we did not want to get into detailed explanation, otherwise this guide would become fairly long and cumbersome to understand. If you are interested in learning more about these concepts, feel free to check the and the section.

Angular Components Overview : Introduction to modules : What Is a Service in Angular and Why Should You Use it? :

https://angular.io/guide/component-overview
https://angular.io/guide/architecture-modules
https://dzone.com/articles/what-is-a-service-in-angular-js-why-to-use-it
references

Update Service

First we have to add two new functions, namely: addBook and addNewBook.

  • addBook is responsible for making the backend API call to add a new book. In the request body, we pass the book object that has been specified in the parameters.

  • addNewBook is a private method as only the service should be able to update the list of books and no other component. It is responsible for keeping the list of books up to date.

import { Injectable } from '@angular/core';
import { HttpClient } from "@angular/common/http";
import { Observable, BehaviorSubject, of } from 'rxjs';
import {Book} from '../models/book.model'

@Injectable({
  providedIn: 'root'
})
export class BmsService {
  // ...
  addBook(book: Book): void {
     this.httpClient.post<any>("http://localhost:3000/api/user/addBook", book)
       .subscribe({
         next: (data: any) => {
          this.addNewBook(book);
          console.log(data);
         },
         error: (data: any) => console.warn(data)
       })
  }

  private addNewBook(book: Book) {
    const books: Book[] = this.listOfBooks.getValue();
    books.push(book);
    this.listOfBooks.next(books);
  }
}

Caution: you should only add the functions addBook and addNewBook rather than replace the entire content of bms.service.ts with the code above. For the sake of brevity, we omit all unnecessary pieces of code. Whenever a portion of code is omitted, it is replaced by the following indicator:// ...

If the request does not go through for whatever reason, we will get an error and in that case we simply print it to the console.

Caution: Keep in mind that this is done for demonstration purposes. In reality, all errors should be handled properly and the user should be made aware that the system is misbehaving.

On the other hand, if the request succeeds, we can safely update our list of books that we defined in the service by calling addNewBook.

As mentioned earlier, addBook makes a POST HTTP request to the backend server. It sends in the request body the book that it received as an argument. Since HTTP calls are asynchronous, we have to wait for the response of the backend by .

subscribing

Display Book List

Now that our service is ready, we just need to modify our component to display the book list.

Let's start first by importing the service that we have created into our book management system module. Your new module should be similar to the following code:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Routes } from "@angular/router";
import { SharedModule } from "@app/shared/shared.module";
import { ListBooksComponent } from './list-books/list-books.component';
import { BmsMainPageComponent } from './bms-main-page/bms-main-page.component';

const routes: Routes = [
  {
    path: "",
    component: BmsMainPageComponent
  },
  {
    path: "list",
    component: ListBooksComponent
  },
];
@NgModule({
  declarations: [ListBooksComponent, BmsMainPageComponent],
  imports: [
    CommonModule,
    SharedModule,
    RouterModule.forChild(routes),
  ],
  exports: [RouterModule]
})
export class BookManagementSystemModule { }

Our service is imported which means that we are can use it. Let's apply this in our file "list-book.component.ts"

import { Component, OnInit } from '@angular/core';
import {BmsService} from "@app/shared/services"
import { Book } from "@app/shared/models";
import { BehaviorSubject, Observable } from 'rxjs';


@Component({
  selector: 'app-list-books',
  templateUrl: './list-books.component.html',
  styleUrls: ['./list-books.component.css']
})
export class ListBooksComponent implements OnInit {

  listOfBooks: BehaviorSubject<Book[]> = new BehaviorSubject<Book[]>([]);

  constructor(private bmsService: BmsService) { }

  ngOnInit() {
    this.bmsService.getBooks().subscribe({
      next: (data: Book[]) => this.listOfBooks.next(data),
      error: (data: any) => console.log(data)

    })
  }
  
}

Let's now modify our "list-book.component.html" so that our data will be displayed in a table.

<div class="table-responsive">
  <table class="table">
    <thead class="thead-dark">
      <tr>
        <th scope="col">#</th>
        <th scope="col">Title</th>
        <th scope="col">Author</th>
        <th scope="col">ISBN</th>
        <th scope="col">Action</th>
      </tr>
    </thead>
    <tbody>
      <ng-container *ngFor="let book of listOfBooks | async; let i = index;">
        <tr>
          <th scope="row">{{i}}</th>
          <td>{{book.Title}}</td>
          <td>{{book.Author}}</td>
          <td>{{book.ISBN}}</td>
          </td>
        </tr>
      </ng-container>
    </tbody>
  </table>
</div>

One final step is to modify our main page component. We want that our main component contains the list book component. We modify the "bms-main-page.component.html"

<app-navbar></app-navbar>
<div class="container-fluid">
  <div class="row justify-content-center mt-5 h-100">
    <div class="col-6 text-center">
      <div class="main-header">
        <h2>Book Management System</h2>
      </div>
    </div>
  </div>
  <div class="row justify-content-center mt-4">
    <div class="col-6">
      <app-list-books></app-list-books>
    </div>
  </div>
</div>

And we also add some styling by modifying the "bms-main-page.component.css"

.main-header {
  font-family: "Lato", sans-serif;
  font-size: 2.5rem;
  font-style: normal;
  color: #5b5b5b;
  margin: 0 auto;
}

Update Service

We start by defining two functions in the BMS service, deleteBookand deleteBookByID:

  • deleteBook is responsible for communicating with the API endpoint. We pass to it the book's ISBN as a parameter and the latter will be used as the endpoint's parameter to delete the book from the database.

  • deleteBookByID will delete the book from the local list that we have created so that we avoid fetching the list again and provide a better UX.

import { Injectable } from '@angular/core';
import { HttpClient } from "@angular/common/http";
import { Observable, BehaviorSubject, of } from 'rxjs';
import {Book} from '../models/book.model'

@Injectable({
  providedIn: 'root'
})
export class BmsService {
  // ...
  deleteBook(bookISBN: string): void {
     this.httpClient.delete<any>(`http://localhost:3000/api/user/deleteBook/${bookISBN}`).subscribe({
      next: (data: any) => {
        console.log(data);
        this.deleteBookByID(bookISBN);
      },
      error: (data: any) => console.log(data)

    })
  }
  
  private deleteBookByID(bookISBN: string) {
    const books: Book[] = this.listOfBooks.getValue();
    books.forEach((book, index) => {
      if(book.ISBN === bookISBN) {books.splice(index, 1);}
    })
    this.listOfBooks.next(books);
  }
  
}

In deleteBook we are using the HTTP Client to make a DELETE HTTP request to the backend server through the API endpoint. Note that we are not specifying the response's type (unlike what we did when fetching the list of books), that is why the function's call type parameter is set to any.

Once the request has been successfully made, we log the response to the console at line 14 (though this is an optional step, we are just doing so as a sanity check), then we invoke deleteBookByIDto delete the book from the local list.

Caution: Again, in an ideal setting you should properly handle errors by displaying them to your user so they would be aware if the system is not acting properly.

Coming to the deleteBookByIDfunction, we first create an auxiliary variable of type Book[] where we store the subject's current value and we operate on that variable. We iterate over the array to find the book with that specific ID. Once found, we delete it from the array using the method splice, that takes the starting index and the number of elements to be deleted (in the case it is just 1). Finally, we broadcast our new list to the subject.

Final Code Review

After assembling all the different parts together, this is how your final code should look like:

import { Component, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms'
import { Router } from '@angular/router';
import { BmsService, Book } from '@app/shared';
import { BehaviorSubject } from 'rxjs';

@Component({
  selector: 'app-bms-add-book',
  templateUrl: './bms-add-book.component.html',
  styleUrls: ['./bms-add-book.component.css']
})
export class BmsAddBookComponent implements OnInit {
  bookForm;
  bookAdded: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
  constructor(private formBuilder: FormBuilder, private bmsService: BmsService,
   private router: Router) {
    this.bookForm = this.formBuilder.group({
      title: "",
      author: "",
      isbn: 0
    })
  }

  ngOnInit() {
  }

  onSubmit(book: Book) {
    this.bookForm.reset();
    console.warn("Book Data:", book);
    this.bmsService.addBook(book);
    this.bookAdded.next(true);
  }

  goBack() {
    this.router.navigate(["/apps/bms"])
  }
}
<app-navbar></app-navbar>
<div class="container-fluid">
  <div class="row justify-content-center mt-5 h-100">
    <div class="col-6 text-center">
      <div class="main-header">
        <h2>Add New Book</h2>
      </div>
    </div>
  </div>
  <div class="row justify-content-center mt-4">
    <div class="col-3">

      <ng-container *ngIf="this.bookAdded | async">
        <div class="alert alert-success alert-dismissible fade show" role="alert">
          The Book Has Been Successfully Added.
          <button type="button" class="close" data-dismiss="alert" aria-label="Close">
            <span aria-hidden="true">&times;</span>
          </button>
        </div>
      </ng-container>

      <form [formGroup]="bookForm" (ngSubmit)="onSubmit(bookForm.value)">
        <div class="form-group">
          <label for="bookTitle">Title</label>
          <input type="bookTitle" class="form-control" id="bookTitle" placeholder="Enter title" formControlName="title">
        </div>
        <div class="form-group">
          <label for="bookAuthor">Author</label>
          <input type="text" class="form-control" id="bookAuthor" placeholder="Author" formControlName="author">
        </div>
        <div class="form-group">
          <label for="bookISBN">ISBN</label>
          <input type="text" class="form-control" id="bookISBN" placeholder="ISBN" formControlName="isbn">
        </div>
        <button type="submit" class="btn btn-primary">Submit</button>
      </form>
      <div class="btn-group mt-1" role="group" aria-label="Basic example">
        <button type="button" class="btn btn-success" (click)="goBack()">Return</button>
      </div>
    </div>
  </div>
</div>
.main-header {
  font-family: "Lato", sans-serif;
  font-size: 2.5rem;
  font-style: normal;
  color: #5b5b5b;
  margin: 0 auto;
}

Create Component

Generate Component

Just like we did for the creation of the book list component, we will have to navigate to the directory we want our component to be created in.

cd src/app/applications/book-management-system/

Then we generate our component bms-add-book:

ng generate component bms-add-book

We also need to define the path of adding a book to our module.

//... 
const routes: Routes = [
  {
    path: "",
    component: BmsMainPageComponent
  },
  {
    path: "list",
    component: ListBooksComponent
  },
  {
    path: "add",
    component: BmsAddBookComponent
  }
];

Application Logic

Add a book

The code to add a book is rather simple. The user is presented with a form that needs to be filled out. This form contains information about the book (title, author and ISBN) as well as a submit button. Each filed has a corresponding ID. For instance the input field for the title of the book is identifying by bookTitle. We will see later why we need these IDs. We also want to add some styling to improve the looks of our form.

To be able to reference a bookForm, we first have to build a form and specify the fields that we want our form to hold.

//... imports
@Component({
  selector: 'app-bms-add-book',
  templateUrl: './bms-add-book.component.html',
  styleUrls: ['./bms-add-book.component.css']
})
export class BmsAddBookComponent implements OnInit {
  bookForm;
  bookAdded: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
  constructor(private formBuilder: FormBuilder, private bmsService: BmsService,
   private router: Router) {
    this.bookForm = this.formBuilder.group({
      title: "",
      author: "",
      isbn: 0
    })
  }

  ngOnInit() {
  }
}
<form [formGroup]="bookForm" (ngSubmit)="onSubmit(bookForm.value)">
  <div class="form-group">
    <label for="bookTitle">Title</label>
    <input type="bookTitle" class="form-control" id="bookTitle" placeholder="Enter title" formControlName="title">
  </div>
  <div class="form-group">
    <label for="bookAuthor">Author</label>
    <input type="text" class="form-control" id="bookAuthor" placeholder="Author" formControlName="author">
  </div>
  <div class="form-group">
    <label for="bookISBN">ISBN</label>
    <input type="text" class="form-control" id="bookISBN" placeholder="ISBN" formControlName="isbn">
  </div>
  <button type="submit" class="btn btn-primary">Submit</button>
</form>
.main-header {
  font-family: "Lato", sans-serif;
  font-size: 2.5rem;
  font-style: normal;
  color: #5b5b5b;
  margin: 0 auto;
}

When a form is submitted, the function onSubmit() is called and it takes as an argument the value of the form bookForm.value.

As a matter of fact, when a book is submitted, all we need to do is to call the service responsible for adding a book with the information that was just submitted.

//...
onSubmit(book) {
  this.bookForm.reset();
  console.warn("Book Data:", book);
  this.bmsService.addBook(book);
  this.bookAdded.next(true);
}

Navigation

In any good navigation design, the user should be able to intuitively navigate through the website. To achieve that, we have to offer the user a way through which they can access the add book component as well as exit it or go back to a previous component (book listing component).

Let us go back to the book list component that we have previously created and add a button that redirects the user to the route that we defined for our add book component.

//...
goToAdd() {
  this.router.navigate(["/apps/bms/add"])
}
<div class="btn-group" role="group" aria-label="Basic example">
  <button type="button" class="btn btn-success" (click)="goToAdd()">Add Book</button>
</div>

Now, we can reach the add book component from the book listing component by simply clicking on the button "Add Book".

We also want to allow the user to go back to the book listing in case they miss-clicked or simply changed their mind.

//...
goBack() {
  this.router.navigate(["/apps/bms"])
}
<div class="btn-group mt-1" role="group" aria-label="Basic example">
  <button type="button" class="btn btn-success" (click)="goBack()">Return</button>
</div>

Update Component

We will now update the ListBooksComponent , so that we allow the user to actually delete books from the list. What we will do first is to add some changes to the template (i.e. the HTML file) of the component and then we will modify the component's TypeScript class.

Basically, what we want to achieve is to display a red button labeled "Delete" , when clicked, will remove the row from the table and display a message confirming that indeed the book has been removed.

Updating the Template

Go to list-books.component.html and modify it as follow,

<ng-container *ngFor="let book of listOfBooks | async; let i = index;">
  <tr>
      <th scope="row">{{i}}</th>
      <td>{{book.Title}}</td>
      <td>{{book.Author}}</td>
      <td>{{book.ISBN}}</td>
      <td>
      <button type="button" class="btn btn-danger" 
      (click)="deleteBook(book.ISBN)">Delete</button>
      </td>
  </tr>
</ng-container>

What remains now is to add the confirmation message once the user has deleted the book. Go to the top of the file add the following code block,

<ng-container *ngIf="this.bookRemoved | async">
  <div class="alert alert-success alert-dismissible fade show" role="alert">
    The Book Has Been Successfully Removed.
    <button type="button" class="close" data-dismiss="alert" aria-label="Close">
      <span aria-hidden="true">&times;</span>
    </button>
  </div>
</ng-container>

If you look closely, you will notice that the message has an ifdirective, and this makes perfect sens since we do not want the message to be always displayed, rather, it should be visible only after deleting, so its status would depend on the value of bookRemoved (which is a boolean variable).

Updating the Class (the Logic)

The last step is to define the functions and variables (deleteBook() and bookRemoved) in the component's logic.

// ...
export class ListBooksComponent implements OnInit,OnDestroy{

  bookRemoved: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
// ...
  deleteBook(bookISBN: string) {
    this.bmsService.deleteBook(bookISBN);
    this.bookRemoved.next(true);
    
  }
}

What the deleteBook() is actually doing, is calling the previous method that we have implemented in the Book Management System's Service that is responsible for deleting books from the list and database , and then we are changing the state of bookRemoved to true so that the confirmation message is displayed.

What we have done is that we have simply created a button inside the for loop (so that it is created at each row) and we have bound a function deleteBook()that takes the book's ISBN as a parameter to the click event (if you want to read more about in Angular). What will happen, is that if the user clicks on the button, that function will be triggered.

Event Binding
resources

Beginner

Beginner resources for MEAN stack development

Articles

Videos

Software

Download SMU Portal Dashboard Mockup

Advanced

Advanced resources for MEAN stack development mostly for the backend.

Articles

Videos

Recommended Extensions for Visual Studio Code

Angular

HTML/CSS

Extra

Delete a Book

In this section, we will add the last feature of the BMS, which is book deletion. As we have done in earlier parts, we will need to modify the module's service, templates and components.

MEAN Stack: Build an App with Angular and the Angular CLI - SitePoint
Mongoose v6.3.1: Getting Started
Angular
Angular Official Tutorial: Tour of Heroes app and tutorial | Video below
git-cheat-sheet/README.md at master · arslanbilal/git-cheat-sheetGitHub
Git and Git Flow Cheat Sheet

Final Code Review

By now, ListBooksComponent files and BMS service should be look like this,

import { Component, OnInit, OnDestroy} from '@angular/core';
import {BmsService} from "@app/shared/services"
import { Book } from "@app/shared/models";
import { BehaviorSubject, Observable } from 'rxjs';
import { Router } from '@angular/router';


@Component({
  selector: 'app-list-books',
  templateUrl: './list-books.component.html',
  styleUrls: ['./list-books.component.css']
})
export class ListBooksComponent implements OnInit,OnDestroy{

  listOfBooks: BehaviorSubject<Book[]> = new BehaviorSubject<Book[]>([]);
  // Second
  bookRemoved: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);

  constructor(public bmsService: BmsService, private router: Router) { }

  ngOnInit() {
   this.bmsService.getList().subscribe(books => this.listOfBooks.next(books));
  }

  ngOnDestroy() {
  }

  deleteBook(bookISBN: string) {
    this.bmsService.deleteBook(bookISBN);
    this.bookRemoved.next(true);
    
  }

  goToAdd() {
    this.router.navigate(["/apps/bms/add"])
  }

}
<ng-container *ngIf="this.bookRemoved | async">
  <div class="alert alert-success alert-dismissible fade show" role="alert">
    The Book Has Been Successfully Removed.
    <button type="button" class="close" data-dismiss="alert" aria-label="Close">
      <span aria-hidden="true">&times;</span>
    </button>
  </div>
</ng-container>

<div class="table-responsive">
  <table class="table">
    <thead class="thead-dark">
      <tr>
        <th scope="col">#</th>
        <th scope="col">Title</th>
        <th scope="col">Author</th>
        <th scope="col">ISBN</th>
        <th scope="col">Action</th>
      </tr>
    </thead>
    <tbody>
      <ng-container *ngFor="let book of listOfBooks | async; let i = index;">
        <tr>
          <th scope="row">{{i}}</th>
          <td>{{book.Title}}</td>
          <td>{{book.Author}}</td>
          <td>{{book.ISBN}}</td>
          <td><button type="button" class="btn btn-danger" 
          (click)="deleteBook(book.ISBN)">Delete</button>
          </td>
        </tr>
      </ng-container>
    </tbody>
  </table>
  <div class="btn-group" role="group" aria-label="Basic example">
    <button type="button" class="btn btn-success" 
    (click)="goToAdd()">Add Book</button>
  </div>

</div>
import { Injectable } from '@angular/core';
import { HttpClient } from "@angular/common/http";
import { Observable, BehaviorSubject, of } from 'rxjs';
import {Book} from '../models/book.model'



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

  constructor(private httpClient: HttpClient) { }

  listOfBooks: BehaviorSubject<Book[]> = new BehaviorSubject<Book[]>([]);

  getBooks(): void {
   this.httpClient.get<Book[]>("http://localhost:3000/api/user/getBooks").subscribe({
    next: (data: Book[]) => this.listOfBooks.next(data),
    error: (data: any) => console.log(data)

  })
  }

  getList(): Observable<Book[]> {
    if(of(this.listOfBooks)) {
      this.getBooks();
    }
    return this.listOfBooks.asObservable();
  }

  deleteBook(bookISBN: string): void {
     this.httpClient.delete<any>(`http://localhost:3000/api/user/deleteBook/${bookISBN}`).subscribe({
      next: (data: any) => {
        console.log(data);
        this.deleteBookByID(bookISBN);
      },
      error: (data: any) => console.log(data)

    })
  }

  addBook(book: Book): void {
     this.httpClient.post<any>("http://localhost:3000/api/user/addBook", book).subscribe({
       next: (data: any) => {
        this.addNewBook(book);
        console.log(data);
       },
       error: (data: any) => console.warn(data)

     })
  }

  private deleteBookByID(bookISBN: string) {
    const books: Book[] = this.listOfBooks.getValue();
    books.forEach((book, index) => {
      if(book.ISBN === bookISBN) {books.splice(index, 1);}
    })
    this.listOfBooks.next(books);
  }

  private addNewBook(book: Book) {
    const books: Book[] = this.listOfBooks.getValue();
    books.push(book);
    this.listOfBooks.next(books);
  }
}


Troubleshooting

In this section, we will display errors that students have reported followed by their fixes. (Last updated on November, 10th)

Error: self signed certificate in certificate chain Source: backend, nodemailer

Solution: Something is blocking the mailing server from establishing a TLS connection with Google's SMTP server. Disabling the antivirus can help prevent such error.

Error: An unhandled exception occurred: Job name "..getProjectMetadata" does not exist. Error: An unhandled exception occurred: Cannot read property 'Minus' of undefined.

Solution: Your global angular installation and your local one are conflicting. Make sure that you have the same angular version installed (globally and locally) or remove the global angular installation by running:

npm uninstall -g @angular/cli
npm cache clean --force

Video related to Introduction to Angular: Tour of Heroes
https://code.visualstudio.com/code.visualstudio.com
IDE
https://marketplace.visualstudio.com/items?itemName=johnpapa.angular-essentialsmarketplace.visualstudio.com
https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-typescript-tslint-pluginmarketplace.visualstudio.com
https://marketplace.visualstudio.com/items?itemName=ecmel.vscode-html-cssmarketplace.visualstudio.com
https://marketplace.visualstudio.com/items?itemName=pranaygp.vscode-css-peekmarketplace.visualstudio.com
https://marketplace.visualstudio.com/items?itemName=michelemelluso.code-beautifiermarketplace.visualstudio.com
https://marketplace.visualstudio.com/items?itemName=mikestead.dotenvmarketplace.visualstudio.com
https://marketplace.visualstudio.com/items?itemName=VisualStudioExptTeam.vscodeintellicodemarketplace.visualstudio.com

Conclusion

In this part, we have illustrated how to implement the frontend side of the Book Management System module.

Other than that, good luck and happy coding 🎉!

Download Postman | Get Started for FreePostman
API client
Adobe XD | Fast & Powerful UI/UX Design & Collaboration ToolAdobe
UI/UX Designing Tool
StarUML
A sophisticated software modeler for agile and concise modeling
Bulletproof node.js project architecture 🛡️
Better Software Design with Clean Architecture
Microservices Architecture with Node.js | CodeForGeekCodeForGeek

Though not in detailed way, but we have covered multiple concepts such as Component Navigation, Forms Handling, Event Binding, API Communication via HTTP Client and so on. These concepts should be enough to help you implement the frontend part of your module. If you have encountered any problems, you can refer the Book Management System repositories (and ) where you can find the full code base and please make sure that you have followed the steps thoroughly.

backend
frontend
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo