Skip to content
unknown edited this page May 30, 2019 · 3 revisions

MEAN Stack Full Course for Beginners

A hands-on tutorial to build a working Angular e-commerce application from scratch on MEAN Stack with deploy & run on Cloud using Heroku. Lean step-by-step how to create a full e-commerce store web site using Angular on MEAN.js stack. Also learn how to deploy the site in Cloud using Heroku. Creating angular app from scratch with MEAN STACK

Mean.JS for Beginners

Title MEAN JS- Full Course for Beginners
Author Rupesh Tiwari
Tags mean.js, angular, mongo, express, node.js, angular materaial
Categories mean.js, fullstack
Status Draft

Introduction

I will help you to become a full stack MEAN developer step by step. From the very basic you will learn entire stack. This is a hands-on Tutorial to build an working angular app from the scratch on MEAN Stack. Creating Restful Api Server using node.js. Saving Data in MongoDB and deploying our production code to heroku. You can see each hands-on live in Rupesh Tiwari YouTube Channel

Environment Settings

  • Install Node.js
  • Install Visual Studio Code
  • Install angular cli by running npm i -g @angular/cli

Creating new Angular App

ng new pm-pinch --routing --style scss --prefix pm --skip-install --dry-run

With routing module With sass as default styles With pm as the prefix for all components and directives With Skip installed so it just creats the folder structures With dry run so that it will just create in memory and display it in the console.

Integrating with Angular Material

ng add @angular/material

Building & Running Angular App

  • build: npm run build
  • start: npm start

Adding Registeration Feature

styles.scss

/* You can add global styles to this file, and also import other style files */
@import '~@angular/material/prebuilt-themes/indigo-pink.css';
@import '~@angular/material/theming';


$custom-typography: mat-typography-config(
    $font-family: 'Exo'
);

@include mat-core($custom-typography);

body {
  background-color: #f4f3f3;

  margin: 0;
  app-root * {
    font-family: 'Exo', sans-serif;
  }
}

.left-spacer{
    margin-left: 2%
}

create user interface first.

user.ts

export interface User {
    email:string;
    password:string;
    repeatPassword?:string;

}

lets update the auth service to create register, login and logout methods.

auth.service.ts

import { Injectable } from '@angular/core'
import { of, Subject } from 'rxjs'
import { User } from './user'

@Injectable({
    providedIn: 'root'
})
export class AuthService {
    private user$ = new Subject<User>()
    constructor() {}

    login(email: string, password: string) {
        const loginCredentials = { email, password }
        console.log('login credentials', loginCredentials)
        return of(loginCredentials)
    }

    get user() {
        return this.user$.asObservable()
    }

    register(user: any) {
        this.user$.next(user)
        console.log('registered user successful', user)
        return of(user)
    }

    logout() {
        this.user$.next(null)
    }
}

app.component.scss

header {
    width: 100%;
    .logo {
        background-image: url('../assets/logo.png');
        width: 50px;
        height: 50px;
        background-size: contain;
        background-repeat: no-repeat;
    }

    .mat-icon {
        vertical-align: middle;
        margin: 0 5px;
    }
    a {
        margin: 1%;
        cursor: pointer;
    }
    .active-link {
        background-color: royalblue;
    }
}

app.component.html

<header>
  <mat-toolbar color="primary">
    <mat-toolbar-row>
      <a [routerLink]="['/']" class="logo"></a>
      <span class="left-spacer"></span>
      <a mat-button links side routerLink="/home" routerLinkActive="active-link"
        >Home</a
      >
      <a
        mat-button
<header>
  <mat-toolbar color="primary">
    <mat-toolbar-row>
      <a [routerLink]="['/']" class="logo"></a>
      <span class="left-spacer"></span>
      <a mat-button  routerLink="/home" routerLinkActive="active-link"
        >Home</a
      >
      <a
        mat-button

        routerLink="/products"
        routerLinkActive="active-link"
        >Products</a
      >
      <a
        mat-button

        routerLink="/login"
        routerLinkActive="active-link"
        *ngIf="!user"
        >Login</a
      >
      <div>
        <a
          mat-button

          *ngIf="user"
          [matMenuTriggerFor]="menu"
        >
          <mat-icon>account_circle</mat-icon>{{ user.fullname }}
        </a>
        <mat-menu #menu="matMenu">
          <button mat-menu-item (click)="logout()">logout</button>
        </mat-menu>
      </div>
    </mat-toolbar-row>
  </mat-toolbar>
</header>

<router-outlet></router-outlet>

app.component.ts

import { Component, OnDestroy } from '@angular/core'
import { AuthService } from './auth.service'
import { Subscription } from 'rxjs'
import { Router } from '@angular/router'
import { User } from './user'

@Component({
    selector: 'pm-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnDestroy {
    userSubscription: Subscription
    user: User

    ngOnDestroy(): void {
        if (this.userSubscription) {
            this.userSubscription.unsubscribe()
        }
    }

    constructor(private authService: AuthService, private router: Router) {
        this.userSubscription = this.authService.user.subscribe(
            user => (this.user = user)
        )
    }

    logout() {
        this.authService.logout()
        this.router.navigate(['/'])
    }
}

register.component.html

<mat-card class="register-card">
    <mat-card-header>
        <mat-card-title>Register</mat-card-title>
    </mat-card-header>
    <mat-card-content>
        <form class="register-form">
            <table class="full-width" cellspacing="0" [formGroup]="userForm">
                <tr>
                    <td>
                        <mat-form-field class="full-width">
                            <input
                                matInput
                                placeholder="Fullname"
                                formControlName="fullname"
                                name="fullname"
                                required
                            />
                        </mat-form-field>
                    </td>
                </tr>
                <tr>
                    <td>
                        <mat-form-field class="full-width">
                            <input
                                matInput
                                placeholder="Email"
                                formControlName="email"
                                name="email"
                                required
                            />
                            <mat-error
                                *ngIf="email.invalid && email.errors.email"
                                >Invalid email address</mat-error
                            >
                        </mat-form-field>
                    </td>
                </tr>
                <tr>
                    <td>
                        <mat-form-field class="full-width">
                            <input
                                matInput
                                placeholder="Password"
                                formControlName="password"
                                type="password"
                                name="password"
                                required
                            />
                        </mat-form-field>
                    </td>
                </tr>
                <tr>
                    <td>
                        <mat-form-field class="full-width">
                            <input
                                matInput
                                placeholder="Reapet Password"
                                formControlName="repeatPassword"
                                type="password"
                                name="repeatPassword"
                                required
                            />
                            <mat-error
                                *ngIf="repeatPassword.invalid && repeatPassword.errors.passwordMatch"
                                >Password mismatch</mat-error
                            >
                        </mat-form-field>
                    </td>
                </tr>
            </table>
        </form>
    </mat-card-content>
    <mat-card-actions>
        <button mat-raised-button (click)="register()" color="primary">
            Register
        </button>
        <span class="left-spacer"
            >Allrady have an account ?
            <a [routerLink]="['/login']">login</a> here</span
        >
    </mat-card-actions>
</mat-card>

register.component.scss

.register-card {
    max-width: 800px;
    min-width: 150px;
    margin: 10% auto;
}

.full-width {
    width: 100%;
}

register.component.ts

import { FormControl, Validators } from '@angular/forms'
import { Component, OnInit } from '@angular/core'
import { FormGroup } from '@angular/forms'
import { Router } from '@angular/router'
import { AuthService } from '../auth.service'

@Component({
    selector: 'pm-register',
    templateUrl: './register.component.html',
    styleUrls: ['./register.component.scss']
})
export class RegisterComponent implements OnInit {
    userForm = new FormGroup({
        fullname: new FormControl('', [Validators.required]),
        email: new FormControl('', [Validators.required, Validators.email]),
        password: new FormControl('', [Validators.required]),
        repeatPassword: new FormControl('', [
            Validators.required,
            this.passwordsMatchValidator
        ])
    })

    constructor(private router: Router, private authService: AuthService) {
        console.log('userform', this.userForm)
    }

    passwordsMatchValidator(control: FormControl) {
        let password = control.root.get('password')
        return password && control.value !== password.value
            ? {
                  passwordMatch: true
              }
            : null
    }

    register() {
        if (!this.userForm.valid) return

        const user = this.userForm.getRawValue()
        this.authService
            .register(user)
            .subscribe(s => this.router.navigate(['']))
    }

    get fullname(): any {
        return this.userForm.get('fullname')
    }
    get email(): any {
        return this.userForm.get('email')
    }
    get password(): any {
        return this.userForm.get('password')
    }
    get repeatPassword(): any {
        return this.userForm.get('repeatPassword')
    }

    ngOnInit() {}
}

Creating Restful API server side

install below npm packages:

npm i -S express dotenv body-parser cors helmet morgan express-async-handler npm i -D concurrently nodemon

server/config/config.js

require('dotenv').config()

const envVars = process.env
module.exports = {
    port: envVars.PORT,
    env: envVars.NODE_ENV
}

server/config/express.js

const express = require('express');
const path = require('path');
const config = require('./config');
const logger = require('morgan');
const bodyParser = require('body-parser');
const cors = require('cors');
const helmet = require('helmet');
const routes = require('../routes/index.route');

const app = express();

// logger only in dev env
if (config.env === 'development') {
  app.use(logger('dev'));
}
const distDir = path.join(__dirname, '../../dist');

// dist folder hosting

app.use(express.static(distDir));

// api body parsing
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

// secure apps by setting various HTTP headers
app.use(helmet());

// enable CORS - Cross Origin Resource Sharing
app.use(cors());

// API router
app.use('/api/', routes);

// index html serving
app.get('*', (req, res) => res.sendFile(path.join(distDir, 'index.html')));

module.exports = app;

server/routes/auth.route.js

const express = require('express')
const userController = require('../controllers/user.controller')
const asyncHandler = require('express-async-handler')

const router = express.Router()

router.post('/register', asyncHandler(insert))

async function insert(req, res, next) {
    console.log(`registering user`, req.body)
    let user = await userController.insert(req.body)
    res.json(user)
}

module.exports = router

server/routes/index.js

const express = require('express')
const authRoutes = require('./auth.route')

const router = express.Router()

router.use('/auth', authRoutes)

module.exports = router

server/controllers/user.controller.js

module.exports = {
    insert
}

users = []

async function insert(user) {
    return users.push(user)
}

server/index.js

const app = require('./config/express')
const config = require('./config/config')

// listen to port
app.listen(config.port, () => {
    console.info(`server started on port ${config.port} (${config.env})`)
})

.gitignore

add below filter

# environment
.env

src/.env

NODE_ENV=development
PORT = 4050

.env file will have all of the environment variables which will vary per environment like prod, qa , dev , stage, uat servers. Therefore, do not checkin this file in github. Rather create .env.sample file to checkin in github for just helping others to get the .env file locally by running simple copy command cp .env.sample .env this way locally anyone can get .env file.

package.json

update start:server script and create kill all node processes script.

"scripts": {
    "ng": "ng",
	"start:server": "concurrently -c \"yellow.bold,green.bold\" -n \"SERVER,CLIENT\"  \"npm run server:watch\"  \"npm run build:watch\"",
    "server:watch": "nodemon server",
    "serve": "ng serve -o --port 4030",
    "build": "ng build",
    "build:watch": "ng build --watch",
    "build:prod": "npm run build -- --prod",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e",
    "stop": "taskkill /f /im node.exe"
  }

src/.env.sample

create a sample env file to check in github for reuse. update the read me for how to create .env file from .env.sample

# after cloning repository run below command from root folder
cp .env.sample .env

now you can use postman and register user by posting data.

Integrating Restful API to Angular APP

Github Source Code Branch

auth.service.ts

Integrating Restfull API with Auth service

import { Injectable } from '@angular/core'
import { of, Subject, throwError } from 'rxjs'
import { User } from './user'
import { HttpClient } from '@angular/common/http'
import { switchMap, catchError } from 'rxjs/operators'

@Injectable({
    providedIn: 'root'
})
export class AuthService {
    private user$ = new Subject<User>()
    constructor(private http: HttpClient) {}
    apiUrl = '/api/auth/'

    login(email: string, password: string) {
        const loginCredentials = { email, password }
        console.log('login credentials', loginCredentials)
        return of(loginCredentials)
    }

    logout() {
        // remove user from suject
        this.setUser(null)
        console.log('user did logout successfull')
    }

    get user() {
        return this.user$.asObservable()
    }

    register(user: any) {
        return this.http.post<User>(`${this.apiUrl}register`, user).pipe(
            switchMap(savedUser => {
                this.user$.next(savedUser)
                console.log('registered user successful', savedUser)
                return of(savedUser)
            }),
            catchError(err => {
                return throwError('Registration failed please contact admin')
            })
        )
    }

    private setUser(user) {
        this.user$.next(user)
    }
}

app.module.ts

import httpclient here

import { HttpClientModule } from '@angular/common/http'

imports: [HttpClientModule]

now if you run npm run start:server you can register user successfully. However , if you want to run ng serve and register user then you will get 404 Not Found error In order to solve this you need to add proxy to angular.json file.

angular.json

go to serve.options and add "proxyConfig": "proxy.conf.json"

 "serve": {
   ...
   "options": {
     "browserTarget": "product-mart:build",
     "proxyConfig": "proxy.conf.json"
   },
   ...

next create proxy.conf.json file.

proxy.conf.json

{
  "/api": {
    "target": "http://localhost:4050",
    "secure": false
  }
}

kill all process and then start server in watch mode and serve client.

Configuring Mongoose

Github Source Code Brnach

install npm packages

npm i -S mongoose

Install mongo db in your machine. If you are using mac os then follow this steps

server/config/config.js

add mongo object

require("dotenv").config();

const envVars = process.env;
module.exports = {
 port: envVars.PORT,
 env: envVars.NODE_ENV,
 mongo: {
   uri: envVars.MONGODB_URI,
   port: envVars.MONGO_PORT,
   isDebug: envVars.MONGOOSE_DEBUG
 }
};

server/config/mongoose.js

const mongoose = require('mongoose');
const util = require('util');
const debug = require('debug')('express-mongoose-es6-rest-api:index');
const config = require('./config');
const mongoUri = config.mongo.uri;

mongoose.connect(mongoUri, { keepAlive: 1, useNewUrlParser: true });

const db = mongoose.connection;
db.once('open', ()=>{
    console.log(`connected to database: ${mongoUri}`);
})
db.on('error', () => {
  throw new Error(`unable to connect to database: ${mongoUri}`);
});

// print mongoose logs in dev env
if (config.mongo.isDebug) {
  mongoose.set('debug', (collectionName, method, query, doc) => {
    debug(`${collectionName}.${method}`, util.inspect(query, false, 20), doc);
  });
}


module.exports = db;

.env

NODE_ENV=development
PORT = 4050
MONGODB_URI=mongodb://localhost/productmart
MONGOOSE_DEBUG:true

index.js

initialize mongoose before listening to port

// initialize mongo
require('./config/mongoose')

Now run npm run server:watch and see mongoose connected successfully. see below message :

server started on port 4050 (development)
connected to database: mongodb://localhost/productmart

Saving User in Mongo Db

Now we will create model and save user into mongo db.

install bcrypt for hashing password

npm i -S bcrypt we will create hashed password before saving to db.

remove repeatpassword from user not needed

// user.ts

export interface User {
    email: string
    fullname: string
    password: string
}

server/models/user.model.js

const mongoose = require('mongoose')
const UserSchema = new mongoose.Schema(
    {
        fullname: {
            type: String,
            required: true
        },
        email: {
            type: String,
            required: true,
            unique: true,
            // Regexp to validate emails with more strict rules as added in tests/users.js which also conforms mostly with RFC2822 guide lines
            match: [
                /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
                'Please enter a valid email'
            ]
        },
        hashedPassword: {
            type: String,
            required: true
        },
        createdAt: {
            type: Date,
            default: Date.now
        },
        roles: [
            {
                type: String
            }
        ]
    },
    {
        versionKey: false
    }
)

module.exports = mongoose.model('User', UserSchema)

Update user controller server/controllers/user.controller.js

const bcrypt = require('bcrypt')
const User = require('../models/user.model')

async function insert(user) {
    // make a mogoose db call to save user in db
    console.log(`saving user to db`, user)

    // go to https://www.npmjs.com/package/bcrypt#to-hash-a-password-1
    // and learn about hashsync
    //  the salt to be used to hash the password.
    user.hashedPassword = bcrypt.hashSync(user.password, 10)

    delete user.password

    console.log(`user to save in db`, user)
    return await new User(user).save()
}

module.exports = {
    insert
}

Now you can go to http://localhost:4050/register page and register yourself check mongo db you will have data saved.

Login User by verifying user data in Mongo Db

src\app\auth.service.ts

add login method in auth service

  login(email: string, password: string) {
   const loginCredentials = { email, password };
   console.log('login credentials', loginCredentials);
   // return of(loginCredentials);
   return this.http.post<User>(`${this.apiUrl}login`, loginCredentials).pipe(
     switchMap(savedUser => {
       this.user$.next(savedUser);
       console.log('find user successful', savedUser);
       return of(savedUser);
     }),
     catchError(err => {
       console.log('find user fail', err);

       return throwError(
         'Your login details could not be verified. Please try again.'
       );
     })
   );
 }

server\routes\auth.route.js

add login route

router.post('/login', asyncHandler(getUser))

async function getUser(req, res, next) {
    const user = req.body
    console.log(`searching user`, user)
    const savedUser = await userController.getUserByEmail(
        user.email,
        user.password
    )
    res.json(savedUser)
}

server\controllers\user.controller.js

update user controller to have getuserbyemail method.

async function getUserByEmail(email, password) {
    let user = await User.findOne({ email })
    if (isUserValid(user, password, user.hashedPassword)) {
        user = user.toObject()
        delete user.hashedPassword
        return user
    } else {
        return null
    }
}

function isUserValid(user, password, hashedPassword) {
    return user && bcrypt.compareSync(password, hashedPassword)
}

src\app\login\login.component.ts

Update login.component.ts to show error

import { Component, OnInit } from '@angular/core'
import { Router } from '@angular/router'
import { AuthService } from '../auth.service'

@Component({
    selector: 'pm-login',
    templateUrl: './login.component.html',
    styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {
    email: string
    password: string
    error: string
    constructor(private router: Router, private authService: AuthService) {}

    ngOnInit() {}

    login() {
        this.error = ''
        this.authService.login(this.email, this.password).subscribe(
            s => this.router.navigate(['/']),
            e => {
                console.log('show ui error', e)
                this.error = e
            }
        )
    }
}

src\app\login\login.component.html

Update logincomponent.html to add this extra row to show error.

...
<tr>
    <td colspan="2">
        <mat-error *ngIf="error">{{error}}</mat-error>
    </td>
</tr>
...

Now if you login with correct credential you will be navigated to home page. Or if you did wrong credentials then you will see error.

Debuggin Node.js in Visual Studio Code

Now I will demonstrate how can you debug serverside code in vscode live.

Adding VsCode configuration for debugging server

Go to debug tab and select add configuration for node.js

{
  // Use IntelliSense to learn about possible attributes.
  // Hover to view descriptions of existing attributes.
  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug Server",
      "type": "node",
      "request": "launch",
      "cwd": "${workspaceFolder}",
      "runtimeExecutable": "npm",
      "runtimeArgs": ["run-script", "debug"],
      "port": 9229
    }
  ]
}

Adding package.json script for debug

add "debug": "node --nolazy --inspect-brk=9229 server" in your package.json/scripts

scripts: {
...
"debug": "node --nolazy --inspect-brk=9229 server"
...
}

Now hit F5 button and you should be able to run your server in debug mode.

Debugging Angular in Visual Studio Code

Lets debug client side app now.

launch.json

inside launch.json add below object

  {
      "name": "Launch Angular",
      "type": "chrome",
      "request": "launch",
      "preLaunchTask": "npm: serve",
      "url": "http://localhost:4200/",
      "webRoot": "${workspaceFolder}"
    }

Task.json

add below object in task.json

{
  // See https://go.microsoft.com/fwlink/?LinkId=733558
  // for the documentation about the tasks.json format
  "version": "2.0.0",
  "tasks": [
    {
      "type": "npm",
      "script": "serve",
      "isBackground": true,
      "presentation": {
        "focus": true,
        "panel": "dedicated"
      },
      "group": {
        "kind": "build",
        "isDefault": true
      },
      "problemMatcher": {
        "owner": "typescript",
        "source": "ts",
        "applyTo": "closedDocuments",
        "fileLocation": ["relative", "${cwd}"],
        "pattern": "$tsc",
        "background": {
          "activeOnStart": true,
          "beginsPattern": {
            "regexp": "(.*?)"
          },
          "endsPattern": {
            "regexp": "Compiled |Failed to compile."
          }
        }
      }
    }
  ]
}

package.json

Here is your final scripts in package.json

 
 
 "scripts": {
    "ng": "ng",
    "start:server": "concurrently -c \"yellow.bold,green.bold\" -n \"SERVER,CLIENT\" \"npm run server:watch\" \"npm run build:watch\"",
    "server:watch": "nodemon server",
    "serve": "ng serve",
    "build": "ng build",
    "build:watch": "ng build --watch",
    "build:prod": "npm run build -- --prod",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e",
    "stop": "taskkill /f /im node.exe",
    "debug": "node --nolazy --inspect-brk=9229 server"

Now you can launch your angular app in debug mode.

Debugging NodeJs and Angular Together in Visual Studio Code

Lets debug both client and server together.

launch.json

inside launch.json add below object

  "compounds": [
    {
      "name": "Do Em Both",
      "configurations": ["Debug Server", "Launch Angular"]
    }

Here is the final launch.json

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug Server",
      "type": "node",
      "request": "launch",
      "cwd": "${workspaceFolder}",
      "runtimeExecutable": "npm",
      "runtimeArgs": ["run-script", "debug"],
      "port": 9229
    },
    {
      "name": "Launch Angular",
      "type": "chrome",
      "request": "launch",
      "preLaunchTask": "npm: serve",
      "url": "http://localhost:4200/",
      "webRoot": "${workspaceFolder}"
    }
  ],
  "compounds": [
    {
      "name": "Do Em Both",
      "configurations": ["Debug Server", "Launch Angular"]
    }
  ]
}

Token Based Authentication

The general concept behind a token-based authentication system is simple. Allow users to enter their username and password in order to obtain a token which allows them to fetch a specific resource - without using their username and password. Once their token has been obtained, the user can offer the token - which offers access to a specific resource for a time period - to the remote site. JWT Other advantages are Token-Based Authentication, relies on a signed token that is sent to the server on each request.

What are the benefits of using a token-based approach?

Cross-domain / CORS: cookies + CORS don't play well across different domains. A token-based approach allows you to make AJAX calls to any server, on any domain because you use an HTTP header to transmit the user information.

Stateless (a.k.a. Server side scalability): there is no need to keep a session store, the token is a self-contained entity that conveys all the user information. The rest of the state lives in cookies or local storage on the client side.

CDN: you can serve all the assets of your app from a CDN (e.g. javascript, HTML, images, etc.), and your server side is just the API.

Decoupling: you are not tied to any particular authentication scheme. The token might be generated anywhere, hence your API can be called from anywhere with a single way of authenticating those calls.

Mobile ready: when you start working on a native platform (iOS, Android, Windows 8, etc.) cookies are not ideal when consuming a token-based approach simplifies this a lot.

CSRF: since you are not relying on cookies, you don't need to protect against cross site requests (e.g. it would not be possible to sib your site, generate a POST request and re-use the existing authentication cookie because there will be none).

Performance: we are not presenting any hard perf benchmarks here, but a network roundtrip (e.g. finding a session on database) is likely to take more time than calculating an HMACSHA256 to validate a token and parsing its contents.

JSON Web Token Generation Process & Authentication process with Google/Facebook/Twitter

JWTs allow for the token to be cryptographically signed, meaning that you can guarantee that the token has not been tampered with. There is also provision for them to be encrypted, meaning that without the encryption key the token can not even be decoded.

JWT

Install passport & jsonwebtoken middleware

First install below packages to support JWT Authentication. npm i -S passport passport-jwt passport-local jsonwebtoken

  • passport : Passport's sole purpose is to authenticate requests, which it does through an extensible set of plugins known as strategies like passport-jwt, passport-local
  • jsonwebtoken: Creates JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties.

https://www.npmjs.com/package/passport http://www.passportjs.org/docs/ (read "Verify Callback" header ) JWT

server\controllers\user.controller.js

Find User By ID

add getUserById function and rename existing function getUserByEmail to getUserByEmailPassword also update the server\routes\auth.route.js to use getUserByEmailPassword.

async function getUserById(id) {
    let user = await User.findById(id)
    if (user) {
        user = user.toObject()
        delete user.hashedPassword
        return user
    } else {
        return null
    }
}
module.exports = {
    insert,
    getUserByEmail,
    getUserById
}

Complete user.controller.js file looks like below

const bcrypt = require('bcrypt')
const User = require('../models/user.model')

async function insert(user) {
    // make a mogoose db call to save user in db
    console.log(`saving user to db`, user)

    // go to https://www.npmjs.com/package/bcrypt#to-hash-a-password-1
    // and learn about hashsync
    //  the salt to be used to hash the password.
    user.hashedPassword = bcrypt.hashSync(user.password, 10)

    delete user.password

    console.log(`user to save in db`, user)
    return await new User(user).save()
}

async function getUserByEmailPassword(email, password) {
    let user = await User.findOne({ email })
    if (isUserValid(user, password, user.hashedPassword)) {
        user = user.toObject()
        delete user.hashedPassword
        return user
    } else {
        return null
    }
}

async function getUserById(id) {
    let user = await User.findById(id)
    if (user) {
        user = user.toObject()
        delete user.hashedPassword
        return user
    } else {
        return null
    }
}

function isUserValid(user, password, hashedPassword) {
    return user && bcrypt.compareSync(password, hashedPassword)
}
module.exports = {
    insert,
    getUserByEmailPassword,
    getUserById
}

server\middleware\passport.js

Create Passport Middleware & Register it in App

  • For local login it will search user by email id
  • For JWT login it will parse the Token and get the User Id and search user by user id.
const passport = require('passport')
const LocalStrategy = require('passport-local')
const JwtStrategy = require('passport-jwt').Strategy
const ExtractJwt = require('passport-jwt').ExtractJwt
const bcrypt = require('bcrypt')

const config = require('./config')
const userController = require('../controllers/user.controller')

const localLogin = new LocalStrategy(
    {
        usernameField: 'email',
        passwordField:'password',
    },
    async (email, password, done) => {
        const user = await userController.getUserByEmailPassword(
            email,
            password
        )
        return user
            ? done(null, user)
            : done(null, false, {
                  error:
                      'Your login details could not be verified. Please try again.'
              })
    }
)

const jwtLogin = new JwtStrategy(
    {
        jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
        secretOrKey: config.jwtSecret
    },
    async (payload, done) => {
        const user = await userController.getUserById(payload._id)
        return user ? done(null, user) : done(null, false)
    }
)

module.exports = passport.use(localLogin).use(jwtLogin)

server\config\express.js

Add the passport middleware in our app.

// authentication
app.use(passport.initialize())

The complete express.js file looks like this

const express = require('express')
const path = require('path')
const logger = require('morgan')
const bodyParser = require('body-parser')
const cors = require('cors')
const helmet = require('helmet')

const routes = require('../routes')
const config = require('./config')
const passport = require('../middleware/passport')

// get app
const app = express()

// logger
if (config.env === 'development') {
    app.use(logger('dev'))
}

// get dist folder
const distDir = path.join(__dirname, '../../dist')

// use dist flder as hosting folder by express
app.use(express.static(distDir))

// parsing from api
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))

// secure apps http
app.use(helmet())

// allow cors
app.use(cors())

// authentication
app.use(passport.initialize())

// api router localhost:4050/api
app.use('/api/', routes)

// serve the index.html
app.get('*', (req, res) => res.sendFile(path.join(distDir, 'index.html')))

module.exports = app

server\controllers\auth.controller.js

Generate JSON Web Token

Now we will add generateToken method in auth controller this token we will return to the client.

jwt = require('jsonwebtoken')
const config = require('../config/config')

module.exports = { generateToken }

function generateToken(user) {
    const payload = JSON.stringify(user)
    return jwt.sign(payload, config.jwtSecret, { expiresIn: '1h' })
    // do not put secret in source code
}

server\config\config.js

Add jwtSecret in config

require('dotenv').config()
const envVars = process.env
module.exports = {
    port: envVars.PORT,
    env: envVars.NODE_ENV,
    jwtSecret: envVars.JWT_SECRET,
    mongo: {
        uri: envVars.MONGODB_URI,
        port: envVars.MONGO_PORT,
        isDebug: envVars.MONGOOSE_DEBUG
    }
}

Note: It’s important that your secret is kept safe: only the originating server should know what it is. It’s best practice to set the secret as an environment variable, and not have it in the source code, especially if your code is stored in version control somewhere.

.env

Add tje jwt secret value in .env file

	NODE_ENV = development
	PORT = 4050
	MONGODB_URI=mongodb://localhost/productmart
	MONGOOSE_DEBUG:true
	JWT_SECRET=9f31e57b-4a4d-436f-a844-d9236837b2f6

server\routes\auth.route.js

Update Auth Route

  • Updating auth route to have a findme method to populate the user based on the token by adding passport middlewares.
  • Creating APIsregister, login and findme and for each one we will add a login middleware to return both user & token
const express = require('express')
const asyncHandler = require('express-async-handler')

const userController = require('../controllers/user.controller')
const passport = require('../middleware/passport')
const authController = require('../controllers/auth.controller')

const router = express.Router()

// localhost:4050/api/auth/register
router.post('/register', asyncHandler(register), login)
// localhost:4050/api/auth/login
router.post('/login', passport.authenticate('local', { session: false }), login)
// localhost:4050/api/auth/findme
router.get('/findme', passport.authenticate('jwt', { session: false }), login)

async function register(req, res, next) {
    const user = req.body
    console.log(`registering user`, user)
    req.user = await userController.insert(user)
    next()
}

function login(req, res) {
    const user = req.user
    const token = authController.generateToken(user)
    res.json({
        user,
        token
    })
}
module.exports = router

Now if you register a user from UI then you will see below in the network tab of chrome.

{
    "user": {
        "roles": [],
        "_id": "5cd6cabf0fa512398cd8fbe1",
        "fullname": "asfsd",
        "email": "asdf@gmail.com",
        "hashedPassword": "$2b$10$K3lsayWxuriTDF5/MB5s8ejPMwD/t8M8CZYlZoYpOHddL/y8EZ4DK",
        "createdAt": "2019-05-11T13:14:39.770Z"
    },
    "token": "eyJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6W10sIl9pZCI6IjVjZDZjYWJmMGZhNTEyMzk4Y2Q4ZmJlMSIsImZ1bGxuYW1lIjoiYXNmc2QiLCJlbWFpbCI6ImFzZGZAZ21haWwuY29tIiwiaGFzaGVkUGFzc3dvcmQiOiIkMmIkMTAkSzNsc2F5V3h1cmlUREY1L01CNXM4ZWpQTXdEL3Q4TThDWllsWm9ZcE9IZGRML3k4RVo0REsiLCJjcmVhdGVkQXQiOiIyMDE5LTA1LTExVDEzOjE0OjM5Ljc3MFoifQ.VenrIYnBPMZJ23q8kQIRFLzBk7gouK_oogRssZIKI_8"
}

Client Integration to JWT Authentication

We will integrate JWT serverside apis with angular code base.

Generate TokenStorate Service in Angular App

In client we have to save the web token in local storage therefore we will create a service.

run below script ng g s tokenStorage This will create token.storage.service.ts

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

const TOKEN_KEY = 'ProductMart.AuthToken';

@Injectable({providedIn:'root'})c
export class TokenStorage {
  constructor() {}

  signOut() {
    window.localStorage.removeItem(TOKEN_KEY);
    window.localStorage.clear();
  }

  saveToken(token: string) {
    if (!token) {
      return;
    }
    window.localStorage.removeItem(TOKEN_KEY);
    window.localStorage.setItem(TOKEN_KEY, token);
  }

  public getToken(): string {
    return localStorage.getItem(TOKEN_KEY);
  }
}

src\app\auth.service.ts

Save Json Web Token After Register

 import { TokenStorage } from './token.storage.service';

 constructor(private http: HttpClient, private tokenStorage: TokenStorage) {}

  register(userToSave: any) {
    return this.http.post<any>(`${this.apiUrl}register`, userToSave).pipe(
      switchMap(({ user, token }) => {
        this.user$.next(user);
        this.tokenStorage.saveToken(token);
        console.log('register user successful', user);
        return of(user);
      }),
      catchError(err => {
        return throwError('Registration failed please contact admin');
      })
    );
  }

Now if you register an user from website then you can check the chrome application localstorage and you will find your key and token stored their.

However now if you refresh your page user is no more available in our angular app.

Lets now create findme method that we will call every time when app.component is refreshed.

src\app\auth.service.ts

Fetch User once logged in after page refresh also

adding findme method in authservice to get the user info from token.

  findMe() {
    const token = this.tokenStorage.getToken();

    if (!token) {
      return EMPTY;
    }

    return this.http.get<any>('/api/auth/findme').pipe(
      switchMap(({ user }) => {
        this.setUser(user);
        console.log('Find User Success', user);
        return of(user);
      }),
      catchError(err => {
        return throwError('Find User Failed');
      })
    );
  }

src\app\app.component.ts

Now lets call findme function in app.component

First we will implement onInit interface call find me and then subscribe to the user updated.

  ngOnInit() {
    // find the user if already logged in
    this.allSubscriptions.push(
      this.authService.findMe().subscribe(({ user }) => (this.user = user))
    );

    // update this.user after login/register/logout
    this.allSubscriptions.push(
      this.authService.user.subscribe(user => (this.user = user))
    );
  }

This is the complete file for the app.component

import { Component, OnDestroy, OnInit } from '@angular/core'
import { AuthService } from './auth.service'
import { Router } from '@angular/router'
import { User } from './user'
import { Subscription } from 'rxjs'

@Component({
    selector: 'pm-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnDestroy, OnInit {
    user: User
    allSubscriptions: Subscription[] = []

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

    ngOnInit() {
        // find the user if already logged in
        this.allSubscriptions.push(this.authService.findMe().subscribe())

        // update this.user after login/register/logout
        this.allSubscriptions.push(
            this.authService.user.subscribe(user => (this.user = user))
        )
    }

    logout() {
        this.authService.logout()
        this.router.navigate(['/'])
    }

    ngOnDestroy(): void {
        this.allSubscriptions.forEach(s => s.unsubscribe())
    }
}

Now restart the server and check it out!

Still not working right? Because we are not passing the token to the server when we are trying to find out the logged in user. The general requirement is if the user is logged in then any request goes to server should have Authorization header in the request where we put the Bearer ${token} that goes to server. In order to achieve this in Angular we have httpinterceptors Lets create our own header.interceptor.ts now.

src\app\interceptors\auth-header.interceptor.ts

Crate JWT Header Interceptor

import {
    HttpEvent,
    HttpInterceptor,
    HttpHandler,
    HttpRequest
} from '@angular/common/http'
import { Observable } from 'rxjs'
import { TokenStorage } from '../token.storage.service'

@Injectable()
export class AuthHeaderInterceptor implements HttpInterceptor {
    constructor(private tokenStorage: TokenStorage) {}
    intercept(
        req: HttpRequest<any>,
        next: HttpHandler
    ): Observable<HttpEvent<any>> {
        // Clone the request to add the new header
        const token = this.tokenStorage.getToken()
        const clonedRequest = req.clone({
            headers: req.headers.set(
                'Authorization',
                token ? `Bearer ${token}` : ''
            )
        })

        // Pass the cloned request instead of the original request to the next handle
        return next.handle(clonedRequest)
    }
}

src\app\app.module.ts

Http interceptors are added to the request pipeline in the providers section of the app.module.ts file.

  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthHeaderInterceptor,
      multi: true
    }
  ],

Now lets refresh the client app! Your server is running so now you will see the passport jwtLogin method is called.

server\middleware\passport.js

One correction needed in jwtLogin to put await

const user = await userController.getUserById(payload._id)
const jwtLogin = new JwtStrategy(
    {
        jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
        secretOrKey: config.jwtSecret
    },
    async (payload, done) => {
        const user = await userController.getUserById(payload._id)
        return user ? done(null, user) : done(null, false)
    }
)

Now after login if you refresh the page you have logged in ! it Worked!

Fixing Logout

Once you logout still user is logged in why? When you do logout token is not cleared yet.

got to Authservice and fix logout src\app\auth.service.ts

logout() {
    // remove user from suject
    this.tokenStorage.signOut();
    this.setUser(null);
    console.log('user did logout successfull');
  }

Fixing Login

Once you login & refresh still user is not persisted why? Because, we have not yet set the token in localstorage.

login(email: string, password: string) {
    const loginCredentials = { email, password };
    console.log('login credentials', loginCredentials);
    // return of(loginCredentials);
    return this.http.post<any>(`${this.apiUrl}login`, loginCredentials).pipe(
      switchMap(({ user, token }) => {
        this.tokenStorage.saveToken(token);
        this.user$.next(user);
        console.log('find user successful', user);
        return of(user);
      }),
      catchError(err => {
        console.log('find user fail', err);

        return throwError(
          'Your login details could not be verified. Please try again.'
        );
      })
    );
  }

Improving Network Speed

Install compression npm package this will gzip our http response and hence decrease the payload from kb to bytes only :)

npm i -S compression

server\config\express.js

const compress = require('compression')
app.use(compress())

Now check your network tab again did u see improvement ? I know your answer is yes!

Cloud Setup Deploy And Build Sample App In Cloud

Go Top We will now deploy our app to cloud. I will use heroku for that. Please make sure you have account in Heroku.

Deploying & Running working Angular App to Cloud

Preparing Angular app for cloud

Angular app needs some some changes before we deploy this app to cloud.

Update package.json

  • Create engines
  "engines": {
    "node": "10.x"
  },
  • Create a start script that will spawn your server

Once your app is deployed to cloud run your server.

"scripts": { ...
	"start": "node server"
}
  • Create postinstall script

Once your app will be deployed to cloud you want to run a script that will deploy your angular app to cloud.

"scripts": { ...
	"build": "ng build",
	"postinstall": "npm run build -- --prod --aot"
}
  • Move typescript to dependencies
"dependencies": {
	...
	"typescript": "~3.2.2"
}

Deploying app on Heroku

  • Create App: heroku create
  • Deploy App: git push heroku master
  • Scale App: heroku ps:scale web=1
  • Open App: heroku open
  • Check Logs : heroku logs -tail

Provisioning adons on Heroku

Mongo Lab

Setup EnvVars on Heroku

Add required environment variables values on Heroku

EnvVars

Next restart your app : heroku restart Reopen app: heroku open

Running working angular app over cloud

See app running live on cloud Running Angular App on Cloud

Architecture in Angular projects

Organizing angular project.

  • Create Core Module
  • Create Shared Module
  • Create Feature Module

Creating Shared Module

Shared modules are the module where we put components, pipe, directives and filters that needs to be shared across many features.

components, pipe, directives and filters have to be imported and re-exported in order to use them in different feature.

Run below script to crreate shared module. ng g m shared

Next move material module under shared module. Because, material module has items that are used across all modules like button and card etc.

Complete code of Shared Module

src\app\shared\shared.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { PmMaterialModule } from './material-module';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';

@NgModule({
  declarations: [],
  imports: [CommonModule, PmMaterialModule],
  exports: [PmMaterialModule, FormsModule, RouterModule, ReactiveFormsModule]
})
export class SharedModule {}

Using Shared Module in Feature Module

Import shared module in app.module and products.module

Go Top

Creating Core Module

Core module is a module that can be only imported once in entire app. Therefore, any angular service which you think needs to be singleton throughout your app consider placing them in core module

Run below script for creating core module.

ng g m core

Next lets import core module in app module.

src\app\app.module.ts

imports: [...CoreModule]

What can go in Core Module

  • User Model
  • header component
  • interceptors

Move User Model in Core Module

Since User model is used across the app it is good idea to move this into core module.

Create Header Component inside Core Module

Lets create Header component inside coremodule. Since it is singleton throughout our app.

Run below script for creating component:

ng g c core/header --skip-tests

File: src\app\core\header\header.component.html

 <header>
  <mat-toolbar color="primary">
    <mat-toolbar-row>
      <a class="logo" [routerLink]="['/']"></a>
      <span class="left-spacer"></span>
      <a mat-button routerLink="/home" routerLinkActive="active-link">Home</a>
      <a mat-button routerLink="/products" routerLinkActive="active-link">Products</a>
      <a *ngIf="!user" mat-button routerLink="/login" routerLinkActive="active-link">Login</a>
      <div>
        <a mat-button *ngIf="user" [matMenuTriggerFor]="menu">
          <mat-icon>account_circle</mat-icon>{{ user.fullname }}
        </a>
        <mat-menu #menu="matMenu">
          <button (click)="logoutEvent.emit()" mat-menu-item>logout</button>
        </mat-menu>
      </div>
    </mat-toolbar-row>
  </mat-toolbar>
</header>

src\app\core\header\header.component.scss

header {
  a {
    margin: 2px;
  }
  .active-link {
    background-color: #b9c5eb11;
  }
  .logo {
    background-image: url('../../../assets/logo.png');
    height: 50px;
    width: 50px;
    vertical-align: middle;
    background-size: contain;
    background-repeat: no-repeat;
  }
  .mat-icon {
    vertical-align: middle;
    margin: 0 5px;
  }
}

src\app\core\header\header.component.ts

import {
  Component,
  OnInit,
  OnDestroy,
  Output,
  EventEmitter,
  Input,
  ChangeDetectionStrategy
} from '@angular/core';
import { AuthService } from 'src/app/auth.service';
import { User } from 'src/app/core/user';

@Component({
  selector: 'pm-header',
  templateUrl: './header.component.html',
  styleUrls: ['./header.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class HeaderComponent {
  @Input()
  user: User;

  @Output()
  logoutEvent = new EventEmitter<any>();

  constructor() {}
}

Import the Header component in core module and dont forget to export it.

Complete Code of Core Module

Core Module src\app\core\core.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HeaderComponent } from './header/header.component';
import { SharedModule } from '../shared/shared.module';
import { RouterModule } from '@angular/router';

@NgModule({
  declarations: [HeaderComponent],
  imports: [CommonModule, SharedModule, RouterModule],
  exports: [HeaderComponent]
})
export class CoreModule {}

Update App Route to go by default to Products page

import { HomeComponent } from './home/home.component';
import { LoginComponent } from './login/login.component';
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { RegisterComponent } from './register/register.component';

const routes: Routes = [
  {
    path: '',
    pathMatch: 'full',
    redirectTo: 'products'
  },
  {
    path: 'home',
    pathMatch: 'full',
    component: HomeComponent
  },
  {
    path: 'products',
    loadChildren: './products/products.module#ProductsModule'
  },
  {
    path: 'login',
    component: LoginComponent
  },
  {
    path: 'register',
    component: RegisterComponent
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {}

Go Top

Use Header Component in App Component

AppComponent HTML

src\app\app.component.html

<pm-header (logoutEvent)="logout()" [user]="user$|async"></pm-header>
<router-outlet></router-outlet>

Remove CSS from app component.

Update Appcomponent.ts

import { Component, OnDestroy, OnInit } from '@angular/core';
import { AuthService } from './auth.service';
import { Router } from '@angular/router';
import { User } from './core/user';
import { Subscription, Observable } from 'rxjs';

@Component({
  selector: 'pm-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit, OnDestroy {
  user$: Observable<User>;
  subscriptions: Subscription[] = [];

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

  ngOnInit(): void {
    this.user$ = this.authService.user;

    this.subscriptions.push(this.authService.findMe().subscribe());
  }

  logout() {
    this.authService.logout();
    this.router.navigate(['/']);
  }

  ngOnDestroy() {
    this.subscriptions.forEach(s => s.unsubscribe());
  }
}

Go Top

Create Auth Module for adding new Auth Feature

Care auth module and move login and register components into that feture module.

Run below script to crate auth module

ng g m auth

Import Auth Module in Core Module

Complete code of Core Module src\app\core\core.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HeaderComponent } from './header/header.component';
import { SharedModule } from '../shared/shared.module';
import { RouterModule } from '@angular/router';
import { AuthModule } from '../auth/auth.module';

@NgModule({
  declarations: [HeaderComponent],
  imports: [CommonModule, SharedModule, RouterModule, AuthModule],
  exports: [HeaderComponent]
})
export class CoreModule {}

Moving Login and Register Folders into the Auth Feature Folder.

src\app\auth\login

src\app\auth\register

Declare Login and Register components and import shared module in auth module.

Complete code of AuthModule is:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LoginComponent } from './login/login.component';
import { RegisterComponent } from './register/register.component';
import { SharedModule } from '../shared/shared.module';

@NgModule({
  declarations: [LoginComponent, RegisterComponent],
  imports: [CommonModule, SharedModule,]
})
export class AuthModule {}

Commit Link: https://github.com/rupeshtiwari/mean-stack-angular-app-from-scratch/commit/d7fc78be3fc4d9e8b729430eadf27e4320b62fe2

Go Top

Move HTTP Interceptors in Core Module

Move http interceptors in core module and update the core module providers.

src\app\core\interceptors

Complete code of core module

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HeaderComponent } from './header/header.component';
import { SharedModule } from '../shared/shared.module';
import { RouterModule } from '@angular/router';
import { AuthModule } from '../auth/auth.module';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { AuthHeaderInterceptorService } from './interceptors/auth-header-interceptor.service';

@NgModule({
  declarations: [HeaderComponent],
  imports: [CommonModule, SharedModule, RouterModule, AuthModule],
  exports: [HeaderComponent],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthHeaderInterceptorService,
      multi: true
    }
  ]
})
export class CoreModule {}

Next remove the interceptor from appmodule complete code of appmodule.ts

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

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

import { HomeComponent } from './home/home.component';
import { HttpClientModule } from '@angular/common/http';
import { SharedModule } from './shared/shared.module';
import { CoreModule } from './core/core.module';

@NgModule({
  declarations: [AppComponent, HomeComponent],
  imports: [
    BrowserModule,
    AppRoutingModule,
    BrowserAnimationsModule,
    SharedModule,

    HttpClientModule,
    CoreModule
  ],

  bootstrap: [AppComponent]
})
export class AppModule {}

Commit Link: https://github.com/rupeshtiwari/mean-stack-angular-app-from-scratch/commit/700f92b00668efc9be4c16760f7ddf58e3028cb9

Go to Top

Cart Feature - Creating Cart Module

Add new Cart module with routing Run below script for cart module ng g m cart --routing

Products page alignment

Now let align the products page and show products in grid and show add to cart button

<mat-grid-list cols="4" rowHeight="700px" [gutterSize]="'10px'">
  <mat-grid-tile *ngFor="let product of (products | async)">
    <mat-card>
      <img mat-card-image src="{{ product.imgUrl }}" alt="{{ product.name }}" />
      <mat-card-content>
        <h2 class="mat-title">{{ product.name }}</h2>
        <h2 class="mat-display-2">$ {{ product.price }}</h2>
      </mat-card-content>
      <mat-card-actions>
        <button mat-button>LIKE</button>
        <button mat-button>SHARE</button>
      </mat-card-actions>
    </mat-card>
  </mat-grid-tile>
</mat-grid-list>

Add CartService in the Cart Module

We will add a shopping cart service using Behavior Subject Cart Service will have state management technique using behavior subject.

Run below script to add cart service

ng g s cart/cart

src\app\cart\cart.service.ts

import { Injectable } from '@angular/core'
import { BehaviorSubject } from 'rxjs'
import { initialState, CartState } from './cart-state'
import { map } from 'rxjs/operators'
import { Product } from '../product'

@Injectable({
    providedIn: 'root'
})
export class CartService {
    cartSubject = new BehaviorSubject<CartState>(initialState)

    constructor() {}

    get cartState() {
        return this.cartSubject.asObservable()
    }

    get cartCount() {
        return this.cartSubject.pipe(map(state => state.products.length))
    }

    addToCart(product: Product) {
        const newState = {
            ...this.currentState,
            products: [].concat(this.currentState.products, product)
        }
        this.cartSubject.next(newState)
    }

    get currentState() {
        return this.cartSubject.value
    }
}

Shopping Cart State & Initial State

src\app\core\product.ts

export interface Product {
    name: string
    price: number
}

src\app\cart\state\cart-state.ts

import { Product } from '../product'

export interface CartState {
    products: Product[]
}
export const initialState = {
    products: []
}

Sharing State Via Shared Component from Cart Feature

Now we want to show the cart item count. Therfore, lets create an component to show the cart item count in an icon with badge.

ng g c cart/cartItemCount

<mat-icon [matBadge]="cartItemCount$|async" matBadgeColor="warn"
    >shopping_cart</mat-icon
>

Injecting Angular Service in Directive

  cartItemCount$: Observable<number>;

  constructor(private cartService: CartService) {}

  ngOnInit() {
    this.cartItemCount$ = this.cartService.cartCount;
  }

export component to share src\app\cart\cart.module.ts

import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'

import { CartRoutingModule } from './cart-routing.module'
import { SharedModule } from '../shared/shared.module'
import { CartItemCountComponent } from './cart-item-count/cart-item-count.component'

@NgModule({
    declarations: [CartItemCountComponent],
    exports: [CartItemCountComponent],
    imports: [CommonModule, CartRoutingModule, SharedModule]
})
export class CartModule {}

Showing Cart Icon on Menu

src\app\app.component.html

<div>
    <pm-cart-item-count></pm-cart-item-count>
</div>

Shopping Cart Item Count In Dashboard page

Clone this wiki locally