Professional Application Development in MEAN Stack - Part 6
Date Published: 07/04/2018
In this part, we will create the menu items APIs in node.js and dynamically load them instead of the hardcoded menu (Home, Services, Our Clients etc.). We will then update our client application to load the menu through API and create the menu links accordingly.

Download Source on GitHub

Introduction

In this part, let's move forward and create the Menu APIs to add, load and update the menu items in the database. Later in future parts, we will create the admin section in client side to manage the menu and secure our admin section by implementing the admin user login and log out. 

Let's Start

  1. Let's first create the Menu APIs in the server application but before that, create the menu model. In the server application, right click on models folder and select New File option, enter the file name menuMdl.js. Add following code in the newly created file:  
    const mongoose = require('mongoose');
    
    // Menu Schema
    const MenuSchema = mongoose.Schema({
    
      MenuName: {
        type: String,
        required: true,
        unique: true
      },
      MenuCode: {
        type: String,
        required: true,
        unique: true
      },
      MenuUrl: {
        type: String
      },
      MenuOrder: {
        type: Number,
        required: true,
        unique: true
      },
      DateAdded: {
        type: Date
      },
      DateUpdated: {
        type: Date
      },
      MenuType: {
        type: String,
        required: true
      },
      Status: {
        type: String,
        required: true
      },
      ParentMenuCode: {
        type: String
      },
      GroupName:
      {
        type: String,
        required: true
      }
    });
    
    const Menu = module.exports = mongoose.model('menu', MenuSchema);
  2. The above code is well explained in the previous part, just a brief description, the mongoose is a MongoDB ORM (Object Relational Mapping). You can map MongoDB document elements as a model and specify the data validation rules. Also, it provides the useful method to query the data. You can go to the mongoose official website to learn more. 
  3. So far, in the menu, we are saving menu name, code, order, parent menu(in case of child menu) and other helping information that is not directly used but good to save in DB.
  4. Next, let create the API routes for the menu to get, add, update and delete Menu. Right click on routes folder and select Create File, enter the name menu.js. Add the following content in it:  
    const express = require('express');
    const router = express.Router();
    const Menu = require('../models/menuMdl');
    const config = require('../config/database');
    
    router.get('/menu', function (req, res) {
        Menu.find({}, function (err, menus) {
            if (err) {
                res.json({ success: false, msg: err });
            } else {
                res.json({ success: true, data: menus });
            }
        }).sort({ OrderBy: 1 });
    });
    
    router.post('/menu', function (req, res) {
        let menuObj = new Menu(req.body);
        menuObj.DateAdded = new Date();
        menuObj.DateUpdated = new Date();
        menuObj.save(function (err) {
            if (err) {
                res.json({ success: false, msg: err });
                return;
            } else {
                res.json({ success: true, msg: "Successfully added the menu!" });
            }
        });
    });
    
    router.put('/menu',function (req, res) {
        let menuObj = new Menu(req.body);
        menuObj.DateUpdated = new Date();
    
        let query = { _id: req.body._id }
    
        Menu.update(query, menuObj, function (err) {
            if (err) {
                res.json({ success: false, msg: err });
                return;
            } else {
                res.json({ success: true, msg: "Successfully updated the menu!" });
            }
        });
    });
    
    router.delete('/menu/:id', function (req, res) {
        let query = { _id: req.params.id }
        Menu.findById(req.params.id, function (err) {
            Menu.remove(query, function (err) {
                if (err) {
                    res.json({ success: false, msg: err });
                    return;
                } else {
                    res.json({ success: true, msg: "Successfully deleted the menu!" });
                }
            });
        });
    });
    
    module.exports = router;
    
  5. Above code is quite self-explanatory, we are creating CRUD APIs for the menu. 
    1. GET API: Will pull all menu from Menu collection and sort it by OrderBy field ascending. We are also saving the menu status, so if you want to get only active menus, you can add the condition here. I am skipping that condition, for now, to keep the things simple.
    2. POST API: Is created to add a new menu. This API will get the Menu information from the client side and map it to Menu model object we created in mongoose. We are explicitly assigning Date Added and Updated here, you can also set the default date option in Menu model while declaring it. Take it as an exercise and try to implement it. We will do it end while refactoring the code. 
    3. PUT API: Is created to update the existing Menu record in the database.
    4. DELETE API: Is created the delete the Menu record from the database, it will first search for a record by calling a findById method and delete the found record. 
    5. For all APIs, we are creating the custom JSON response object with success variable as true or false based on response type and msg variable. We would use both variables on the client side to show a message to the user. 
  6. Next, we need to add the Menu route in main class mazharnco.js like we did for contact API earlier, edit the mazharnco.js and replace it with following code:  
    const express = require('express');
    const path = require('path');
    const bodyParser = require('body-parser');
    const cors = require('cors');
    const contacts = require('./routes/contact');
    const menus = require('./routes/menu');
    const mongoose = require('mongoose');
    const config = require('./config/database');
    
    mongoose.connect(config.database, function (err) {
        if (err) {
            console.log('Not connected to the database: ' + err);
        } else {
            console.log('Successfully connected to MongoDB');
        }
    });
    
    var app = express();
    
    app.engine('html', require('ejs').renderFile);
    app.use(express.static(path.join(__dirname, './views')));
    
    app.use(bodyParser.urlencoded({ extended: false }));
    app.use(bodyParser.json());
    app.use(cors());
    app.use("/api", contacts);
    app.use("/api", menus);
    
    app.get('*', function (req, res) {
        res.render(path.join(__dirname, './views/index.html')); // load our public/index.html file
    });
    
    const port =  process.env.PORT;
    
    app.listen(port, function () {
        console.log('Server started on port ' + port);
    });
  7. Just a minor update, we added the Menu router reference and then add the route path "/api", so now menu APIs can be accessed from the server through http://localhost:3000/api/menu
  8. That's pretty much it on the server side, for now, let's update our client side to load the menu dynamically from a database. We need to update DataService, AppComponent class and it's template. 
  9. Let's go to the mazharncoweb folder that is our client-side application in Angular. Edit the src -> app -> service -> data -> data.service.ts and replace it's content with the following:   
    import { Injectable } from '@angular/core';
    import { Observable } from "rxjs/Observable";
    import { Http, Response, Headers, RequestOptions } from '@angular/http';
    import 'rxjs/add/operator/map';
    import 'rxjs/add/operator/do';
    import 'rxjs/add/operator/catch';
    import { environment } from "../../../environments/environment";
    
    @Injectable()
    export class DataService {
    
      SERVER_URL = environment.api_url;
    
      constructor(public _http_unsecure: Http) { }
    
      get(url: string): Observable<any> {
        return this._http_unsecure.get(this.SERVER_URL + url)
        .map((response: Response) => <any>response.json());
      }
    
      post(url: string, model: any): Observable<any> {
        let body = JSON.stringify(model);
        let headers = new Headers({ 'Content-Type': 'application/json' });
        let options = new RequestOptions({ headers: headers });
        return this._http_unsecure.post(this.SERVER_URL+url, body, options)
          .map((response: Response) => <any>response.json())
      }
    
    }
    
  10. In the above code, we added one more function for GET API that will load the data from given HTTP API URL, just FYI, we will keep refactoring and updating this file in upcoming parts. 
  11. Before updating the AppComponent, let's create the Menu interface first to store the Menu fields or element (whatever you want to call them), right click on src -> app -> model and select option New File, enter the file name menu.ts and add following code: 
    export interface IMenu {
        _id:string,
        MenuName:string,
        MenuCode: string,
        MenuUrl: string,
        MenuOrder: number,
        DateAdded: Date,
        DateUpdated: Date,
        MenuType: string,
        Status:string,
        ParentMenuCode: string,
        GroupName:string
    }
    
  12. This interface is exactly matching the Menu model we created on the server side in the Node.js application through mongoose schema function. So it will easily map fields returned by APIs from the server side. If you are .Net developer, I am thinking about AutoMapper library now.
  13. Next, Let's update the AppComponent, go to the app -> src -> app.component.ts file and replace its content with the following:  
    import { Component, OnInit, ViewEncapsulation } from '@angular/core';
    import { NgxCarousel, NgxCarouselStore } from 'ngx-carousel';
    import { MouseEvent } from '@agm/core';
    import { MatDialog } from "@angular/material/dialog";
    import { Router } from "@angular/router";
    import { DataService } from "./service/data/data.service";
    import { IMenu } from "./model/menu";
    import { FormGroup, Validators, FormBuilder } from "@angular/forms";
    
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css'],
      encapsulation: ViewEncapsulation.None
    })
    
    export class AppComponent implements OnInit {
    
      GET_ALL_URL: string = "/api/menu";
      mainMenus: IMenu[];
      menus: IMenu[];
    
      searchFrm: FormGroup;
    
      public carouselBanner: NgxCarousel;
      title = 'app';
    
      constructor(private _fb: FormBuilder,
                  private dialog: MatDialog,
                  private _dataService: DataService,
        private router: Router) { }
    
      ngOnInit() {
        this.searchFrm = this._fb.group({
          TextSearch: ['', [Validators.minLength(3)]]
        });
    
        this.carouselBanner = {
          grid: { xs: 1, sm: 1, md: 1, lg: 1, all: 0 },
          slide: 2,
          speed: 400,
          interval: 4000,
          point: {
            visible: true,
            pointStyles: `
              .ngxcarouselPoint {
                list-style-type: none;
                text-align: center;
                padding: 12px;
                margin: 0;
                white-space: nowrap;
                overflow: auto;
                position: absolute;
                width: 100%;
                bottom: 20px;
                left: 0;
                box-sizing: border-box;
              }
              .ngxcarouselPoint li {
                display: inline-block;
                border-radius: 999px;
                background: rgba(255, 255, 255, 0.55);
                padding: 5px;
                margin: 0 3px;
                transition: .4s ease all;
              }
              .ngxcarouselPoint li.active {
                  background: white;
                  width: 10px;
              }
            `
          },
          load: 2,
          loop: true,
          touch: true
        }
    
        this._dataService.get(this.GET_ALL_URL)
          .subscribe(menus => { this.menus = menus.data; this.mainMenus =this.menus!=null?this.menus.filter(x => x.MenuType == 'Main'):null; }
          );
    
      }
    
      getChildMenu(menuCode) {
        return this.menus.filter(x => x.ParentMenuCode == menuCode)
      }
      
    }
    
  14. Though the code is quite self-explanatory let's go through the updates;
    1. we are creating two Menu interface type list to keep the main menus that would display on Navbar and simple menus that will hold all menus records. You will understand why we are doing it once we will update the AppComponent HTML template.
    2. In ngOnInit lifecycle hook, we added our GET API call to load menus from a server side (i.e. from MongoDB). GET_ALL_URL is defined in starting of class and we are subscribing to Data Service get function since it returns IObservable that cannot execute until we subscribe them.
    3. Next is a getChildMenu function that is taking menuCode as an input parameter and returning the corresponding menu item from menus list that we populated through get API (step 1).
  15. Next let's update the AppComponent HTML template, edit the app -> src -> app.component.html and replace its content with following code:   
    <div class="main_div">
      <mat-grid-list cols="10" rowHeight="2:1">
        <div>
          <mat-grid-tile>
            <div style="padding-left: 5px;">
                <a [routerLink]="['home']"> <img src='assets/images/logo1.png' class="mnc-logo" /></a>
            </div>
          </mat-grid-tile>
          <mat-grid-tile [colspan]=8>
            <div class="socail_btn_padding">
              <a target="_blank" href="https://www.facebook.com/mazhar.mahmood.16"><img src="assets/images/fb_btn1.jpg" class="socail_btn image" /></a>
              <a href="skype:plug-shop?mazharmahmood786"><img src="assets/images/skype_btn1.jpg" class="socail_btn image" /></a>
              <img src="assets/images/twtr_btn1.jpg" class="socail_btn image" />
              <img src="assets/images/lkdin_btn1.jpg" class="socail_btn image" />
            </div>
          </mat-grid-tile>
          <mat-grid-tile [colspan]=1>
            <div style="text-align: right">
              <img src="assets/images/login_btn.png" class="lgn_btn image" />
            </div>
          </mat-grid-tile>
        </div>
        <mat-grid-tile [colspan]=10 [rowspan]=3>
          <ngx-carousel [inputs]="carouselBanner" [moveToSlide]="1">
            <ngx-item NgxCarouselItem>
              <div><img src='assets/images/banner_1.jpg' width="100%" /></div>
            </ngx-item>
    
            <ngx-item NgxCarouselItem>
              <div><img src='assets/images/banner_2.jpg' width="100%" /></div>
            </ngx-item>
            <button NgxCarouselPrev>&lt;</button>
            <button NgxCarouselNext>&gt;</button>
          </ngx-carousel>
        </mat-grid-tile>
      </mat-grid-list>
      <div>
        <nav class="navbar navbar-default navbar_shadow">
          <div class="container-fluid">
            <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
              <ul class="nav navbar-nav">
                <li><a [routerLink]="['home']">Home</a></li>
                <li class="dropdown" *ngFor="let menu of mainMenus" style="cursor: pointer;">
                  <a [routerLink]="['page', menu.MenuCode]" class="dropdown-toggle" role="button" aria-haspopup="true" aria-expanded="false">{{menu.MenuName}}<span *ngIf="getChildMenu(menu.MenuCode).length>0" class="caret"></span></a>
                  <ul class="dropdown-menu" *ngIf="getChildMenu(menu.MenuCode).length>0">
                    <li *ngFor="let chldMenu of getChildMenu(menu.MenuCode)">
                      <a [routerLink]="['page', chldMenu.MenuCode]">{{chldMenu.MenuName}}</a>
                    </li>
                  </ul>
                </li>
                <li><a [routerLink]="['contact']">Contact Us</a></li>
              </ul>
              <ul class="nav navbar-nav navbar-right">
                <form class="navbar-form navbar-left">
                  <div class="form-group">
                    <input type="text" class="form-control" placeholder="Search">
                  </div>
                  <button type="submit" class="btn btn-default">Submit</button>
                </form>
              </ul>
            </div>
            <!-- /.navbar-collapse -->
          </div>
          <!-- /.container-fluid -->
        </nav>
      </div>
      <div class="clearfix" style="padding-top:10px; padding-bottom: 10px">
        <router-outlet></router-outlet>
      </div>
      <app-footer></app-footer>
      <br>
    </div>
  16. We already learned about the header code, here let's try to understand the navbar update that was hard-coded links before but now we are dynamically creating it.
    1. We are looping through the mainMenus list and showing it on top and in inner loop, we are calling the getChildMenu method by passing current main menu, if menu main has child/children menus, it will loop through it an create the link accordingly. 
    2. [routerlink] directive is taking the first parameter a router and second is the input parameter (kind of query string), we need it to determine what page to load from the database since page content would also come from database eventually. 
    3. Hopefully, now AppComponent class methods would start making sense to you.
    4. One thing at a bottom is, a footer component that was not there, I just created the separate component since this page was getting too big. 
    5. An exercise for you is to create a sperate component for the header.
  17. let's create the footer component, revise the Angular CLI commands and try to create the footer component in src -> app folder.
  18. Right-click on the mazharncoweb folder and select the option Open in Terminal, Integrated Terminal would appear on right bottom side, enter the command: ng generate component footer --flat
  19. Edit the app -> src -> footer.component.html page and replace its content with the following: 
    <div style="padding-top: 30px">
      <mat-card>
        <mat-card-header class="header">
          <div mat-card-avatar class="vst_img"></div>
          <mat-card-title class="header_title">Visit Us</mat-card-title>
        </mat-card-header>
        <mat-card-content class="address_card_content">
          <div class="address_card_content_div">
            <div class="col-md-6">
              <agm-map [latitude]="lat" [longitude]="lng" [zoom]="zoom" [disableDefaultUI]="false" [zoomControl]="false">
                <agm-marker *ngFor="let m of markers; let i = index" [latitude]="m.lat" [longitude]="m.lng" [label]="m.label">
                  <agm-info-window>
                    <strong>Mazhar & Co. Office # 19, First Floor, Mall Plaza, Mall Road                                                                                    
                    Rawalpindi, Pakistan  </strong>
                  </agm-info-window>
                </agm-marker>
              </agm-map>
            </div>
            <div class="col-md-1"></div>
            <div class="col-md-5 address">
              Email:<a href="mailto:mazhar_rawalpindi@yahoo.com"> mazhar_rawalpindi@yahoo.com</a>
              <br>Website: <a href="http://www.mazharnco.com"> www.mazharnco.com</a>
              <br> Skype: <a href="skype:plug-shop?mazharmahmood786"> mazharmahmood786</a>
              <br> Cell: <a href="tel:+92 333 5104584"> +92 333 5104584</a>
              <br> Tel: <a href="tel:+92 51 5680138"> +92 51 5680138</a> & <a href="tel:+92 51 2291858"> +92 51 2291858</a>            <br>
              <hr><b> Head Office:</b>
              <br> Office # 19, First Floor, Mall Plaza, Mall Road
              <br> Rawalpindi, Pakistan
              <br> Tel & Fax:
              <a href="tel:+92 51 5562241"> +92 51 5562241 </a><br>
              <hr><b> Branch office: </b>
              <br> H # 24, St # 54, F-11/3
              <br> Islamabad, Pakistan
              <br> Tel: <a href="tel:+92 51 2291858"> +92 51 2291858</a><br>
            </div>
          </div>
        </mat-card-content>
        <mat-card-actions class="main_footer">
          <div>
            <div>
              © 2018 Mazhar & Co. All Rights Reserved.
              <button mat-button>Privacy Policy</button>
              <button mat-button>Contact Us</button>
            </div>
            <div class="main_footer_cred">
              Designed and Developed by <a style="color:#bfc4cc" href="https://fullstackhub.io" target="_blank"> Fullstack Hub</a>
            </div>
          </div>
        </mat-card-actions>
      </mat-card>
    </div>
    <br>
  20. We already learned about it in previous parts so let's not waste time on it, edit the footer.component.ts file and replace its content with the following:   
    import { Component, OnInit } from '@angular/core';
    
    interface marker {
      lat: number;
      lng: number;
      label?: string;
      draggable: boolean;
    }
    
    
    @Component({
      selector: 'app-footer',
      templateUrl: './footer.component.html',
      styleUrls: ['./footer.component.css']
    })
    export class FooterComponent implements OnInit {
      zoom: number = 8;
      lat: number = 33.593742;
      lng: number = 73.050849;
    
      markers: marker[] = [
        {
          lat: 33.593742,
          lng: 73.050849,
          label: 'A',
          draggable: false
        }
      ];
      constructor() { }
    
      ngOnInit() {
      }
    
    }
    
  21. So our AppComponent and FooterComponent are done and we are also pretty much done with this part. Run both client and server side application and test it. You would only see two menu items Home and Contact Us those are hardcoded in AppComponent HTML template because our database collection is not created yet, we need to create the Menu items through our Menu POST API that we will be doing in upcoming parts in Admin section.
  22. Since we don't have admin section yet to insert menu from the application, you can call Menu POST API to insert menu using Postman, this would be a good practice how to use Postman, so after running the application, make a POST call to http://localhost:3000/api/menu and in the body section, add following JSON object: 
    {
        "MenuName" : "Services",
        "MenuCode" : "SVC",
        "MenuUrl" : "",
        "MenuOrder" : 100,
        "MenuType" : "Main",
        "Status" : "Active",
        "DateAdded" : "2018-07-13",
        "DateUpdated" : "2018-07-13",
        "ParentMenuCode" : "",
        "GroupName" : "SVC"
    }
     
  23. In next part, we will update PageComponent to dynamically load page content from the database and also we will plan our admin section. Till then keep practicing the menu APIs through Postman.



    Keywords: Angular Reactive Form tutorial, Node.js nodeemailer, MEAN stack tutorial, Angular 5 tutorial for beginners, MEAN Stack tutorial for beginners, Rxjs tutorial or beginner, Rxjs vs Promise,nodemailer,pm2, Node.js MongoDB, Node.js mongoose configuration