Professional Application Development in MEAN Stack - Part 9 - B
Date Published: 10/30/2018
In this part of Part 9, we will develop the front-end in Angular, we will be able to create the pages, add the content (text, images, links etc.) and attached them with the menu items that we have developed in earlier part as Menus Management. You would be able to create as many pages as you want and update them anytime without touching the application code, this will act as a simple Content Management System. For the page editor, we will use tinymce control for Angular that works great to provide us with different features to create an awesome front-end page. In fact, the article you are ready now and all others are written using Angular tinymce control.

Introduction

This is the second part of Part 9 where we will develop client side of Page Management in Angular, we will create page interface IPage and Page Mangement pages in admin portal with a view, add, update and delete page functionalities. As discussed in Part A, to edit the page content we will use tinymce control for Angular that provides text formatting, paragraphs, links, image, videos embedding etc. features and more and less can be compared with Microsoft WordPad, the key is that non-technical user can also edit the page content without programming expertise, basic Micorost Word or WordPad skills are required though.  

Let's Start

It is strongly recommended to read all previous parts before moving to this one, can't promise you would understand all the steps I am going to list down without knowledge of previous parts.

  1. Make sure the Part 9-A is fully functional without any error. Test all APIs in Postman if possible before moving to this part will help to implement the front-end smoothly. 
  2. Currently, in our front-end Angular application, there is no functionality related to Pages Management, before developing the Menu Management, we had Services Page with static content that is not working anymore cause menu is now loading from the database and it doesn't know its page. Also, the view-page component in client folder needs to be updated to load the page content from the database and render it.
  3. In a mazharncoweb folder, locate the app -> model folder, right click on it and select option New File, enter the file name page.ts. (For your exercise, create a page.ts interface through Angular CLI). Edit the newly create a file and paste the following code in it:  
    export interface IPage {
        _id: string,
        Title: string,
        Content: string,
        DatePublished: Date,
        DateAdded: Date,
        DateUpdated: Date,
        Status: string,
        MenuCode: string,
        Images: IFiles[],
    }
    
    export interface IFiles {
        _id: string,
        OrigName:string,
        ActualName:string,
        FileType:string,
        Path:string
    }
  4. In page.ts, we are creating two interfaces IPage and IFiles, the IPage is a primary interface to store the page information and it has Images variable of IFiles type to store the multiple images for one page. In the previous part we created the MongoDB document schema using Mongoose, just look at the server -> models -> pageMdl.js file, you would see a lot of similarities.
  5. Next, let's set up the tinymce control in our application, there are few steps involved. Let's do them one by one:
    1. The first step is to install the tinymce control, I cannot guarantee you that latest version would work so I am explicitly mentioning the version 2.1.2. Right click on a mazharncoweb folder and select the option, Open in Terminal, in TERMINAL tab, enter the command: npm install angular2-tinymce@^2.1.2 --save
    2. Once the tinymce is installed, let's configure it in AppModule, edit the src -> app -> app.module.ts and replace the code with the following:  
      import { BrowserModule } from '@angular/platform-browser';
      import { NgModule } from '@angular/core';
      import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
      import { FormsModule, ReactiveFormsModule } from '@angular/forms';
      //Http Module For GET, POST, PUT and DELETE RESTful APIs
      import { HttpModule } from '@angular/http';
      import { TinymceModule } from 'angular2-tinymce';
      
      import {
        MatAutocompleteModule,
        MatButtonModule,
        MatButtonToggleModule,
        MatCardModule,
        MatCheckboxModule,
        MatChipsModule,
        MatDatepickerModule,
        MatDialogModule,
        MatExpansionModule,
        MatGridListModule,
        MatIconModule,
        MatInputModule,
        MatListModule,
        MatMenuModule,
        MatNativeDateModule,
        MatPaginatorModule,
        MatProgressBarModule,
        MatProgressSpinnerModule,
        MatRadioModule,
        MatRippleModule,
        MatSelectModule,
        MatSidenavModule,
        MatSliderModule,
        MatSlideToggleModule,
        MatSnackBarModule,
        MatSortModule,
        MatTableModule,
        MatTabsModule,
        MatToolbarModule,
        MatTooltipModule,
        MatStepperModule
      } from '@angular/material';
      
      import { HttpClientModule } from '@angular/common/http';
      
      import { AppComponent } from './app.component';
      import 'hammerjs';
      
      import { AgmCoreModule } from '@agm/core';
      import { NgxCarouselModule } from 'ngx-carousel';
      
      import { HomeComponent } from './client/home/home.component';
      import { AppRoutingModule } from "./app-routing.module";
      import { ContactComponent } from './client/contact/contact.component';
      import { DataService } from './service/data/data.service';
      import { Util } from "./shared/util";
      import { ViewPageComponent } from './client/view-page/view-page.component';
      import { FooterComponent } from './footer.component';
      import { AdminComponent } from './admin/admin/admin.component';
      import { DatagridComponent } from './shared/datagrid/datagrid.component';
      import { ConfirmDeleteComponent } from './shared/messages/confirm-delete/confirm-delete.component';
      import { ManageMenuComponent } from './admin/menu/manage-menu/manage-menu.component';
      import { MenuListComponent } from './admin/menu/menu-list/menu-list.component';
      import { UserMessageComponent } from './admin/user-message/user-message.component';
      
      @NgModule({
        declarations: [
          AppComponent,
          HomeComponent,
          ContactComponent,
          ViewPageComponent,
          FooterComponent,
          AdminComponent,
          DatagridComponent,
          ConfirmDeleteComponent,
          ManageMenuComponent,
          MenuListComponent,
          UserMessageComponent
        ],
        imports: [
          TinymceModule.withConfig({
            allow_script_urls: true,
            convert_urls: false,
            valid_elements: '*[*]',
            content_css: '/assets/css/prism.css',
            plugins: ['searchreplace', 'table', 'textpattern', 'textcolor', 'anchor', 'hr', 'emoticons', 'lists advlist', 'codesample', 'link', 'autolink', 'image', 'imagetools', 'insertdatetime',
              'fullscreen', 'media', 'template', 'code', 'preview'],
            toolbar: 'searchreplace,table,sizeselect | bold italic | fontselect |  fontsizeselect,textpattern,forecolor backcolor,anchor,hr,emoticons,bold italic underline | bullist numlist outdent indent,alignleft aligncenter alignright alignjustify,codesample,link,autolink,image,imagetools,insertdatetime,fullscreen,media,template,code,preview'
          }
          ),
          HttpClientModule,
          HttpModule,
          FormsModule,
          ReactiveFormsModule,
          AgmCoreModule.forRoot({
            apiKey: 'AIzaSyCKHGctDoGx1_YdAbRsPlJYQqlQeC6kR2E'
          }),
          NgxCarouselModule,
          BrowserModule,
          AppRoutingModule,
          BrowserAnimationsModule,
          MatButtonModule,
          MatCheckboxModule,
          MatCardModule,
          MatInputModule,
          MatRadioModule,
          MatSelectModule,
          MatTabsModule,
          MatSortModule,
          MatPaginatorModule,
          MatTableModule,
          MatSnackBarModule,
          MatIconModule,
          MatDialogModule,
          MatAutocompleteModule,
          MatGridListModule,
          MatMenuModule,
          MatProgressBarModule,
          MatExpansionModule,
          MatTooltipModule,
          MatSlideToggleModule
        ],
        providers: [DataService,Util],
        bootstrap: [AppComponent],
        entryComponents: [ManageMenuComponent,ConfirmDeleteComponent]
      })
      export class AppModule { }
      
    3. In the imports section, you can see we have tinymce configuration,TinymceModule.withConfig({ ... it contains all the menu and toolbar items that can be helpful for us to edit the page. The name of items are quite self-explanatory and you may not need all menu items e.g. code, insert/Edit code sample etc. but let's keep it, just go through them, you may need them for any other application.  
    4. Next, go to mazharncoweb -> node_modules -> tinymce and copy the plugins, skins and themes folders. Go to src -> assets and create the folder tinymce. Paste all these three folders (plugins, skins, and themes) in there.
    5. This is an optional step but good to do, copy the mazharncoweb -> node_modules -> tinymce -> plugins -> codesample -> css -> prism.css file and paste it in src -> assets -> css folder. This is used to copy the code block, mouse hovers on any code block in this article and on the top right corner, you would see the copy button.
    6. That's pretty much it on tinymce configuration.
  6. Next, let create the Admin portal pages for Pages Managment, right click on the mazharncoweb folder and select the option, Open in Terminal, in TERMINAL tab, enter the commandng g c admin/page/PageList, this page will have our custom data grid control. It will load and show all available pages in the database. 
  7. Edit the app -> admin -> page -> page-list -> page-list.component.css and paste the following CSS in it:  
    mat-card
    {
      margin: 0px;
      padding: 0px;
    }
    
    mat-card-noshadow{ 
        background: #ECECF4;
        box-shadow: none !important;
    }
    
    mat-card-content{
      margin: 0px;
      padding: 0px;
    }
    
    .header
    {
    background: #E90C0C;
    border-bottom: 5px solid #790606;
    height: 50px;
    padding-left: 5px;
    }
    
    .subheader
    {
    height: 40px;
    background: #5B5C60;
    }
    
    .subheader_title{
        vertical-align:baseline;
        padding-top: 5px;
        padding-bottom: 0px;
        padding-left: 5px;
        font-size: 13pt;
        font-family: Arial, Helvetica, sans-serif;
        color: #C8CCD2;
    }
    
    .header_title{
        vertical-align:baseline;
        padding-top: 10px;
        padding-bottom: 0px;
        padding-left: 5px;
        font-size: 16pt;
        font-family: Arial, Helvetica, sans-serif;
        color: #FFFFFF;
    }
    
    .card_cntnt
    {
        padding: 15px;
        padding-bottom: 15px;
    }
    
    .card_footer
    {
        text-align: left;
        padding-left: 40px;
    }
    
    .frm-ctrl {
        width: 90%;
    }
    
    .icon_align
    {
        vertical-align: middle;
    }
    .phone_cntnt
    {
        font-weight: bold;
    }
    
  8. This is the same CSS we been using in rest of components, nothing new.
  9. Edit the app -> admin -> page -> page-list -> page-list.component.html and replace its content with the following:  
    <div>
      <mat-card>
        <mat-card-header class="header">
          <mat-card-title class="header_title"><i class="material-icons">pages</i> Page Management</mat-card-title>
        </mat-card-header>
        <mat-card-content class="card_cntnt">
          <div>
            <app-datagrid
            [displayedColumns]="displayedColumns"
            [filterPlaceholder]="filterPlaceholder"
            [hdrBtn]="hdrBtn"
            [data]="data"
            (btnclick)="gridaction($event)"
            ></app-datagrid>
          </div>
        </mat-card-content>
      </mat-card>
    </div>
    <button [routerLink]="['../../admin']" mat-button color="primary">Back to Menu</button>
    <div [hidden]="isHideManagePanel">
      <manage-page [dbops]="dbops" [modalTitle]="modalTitle" [modalBtnTitle]="modalBtnTitle" [page]="page" (crudOpDone)="refreshData($event)"
        (hideeditPanel)="hideeditPanel($event)" #manageForm>
      </manage-page>
    </div>
  10. We are using our custom grid control to view all the page record from the database, it almost same as Menu List and User Messages except the page title. We already have learned about data grid parameters, you shouldn't have any issue if you are following this article series.
  11. At the bottom, you can see we are embedding manage-page component that we are going to create in upcoming steps. This component will be used to add, update and delete the page, unlike Menu Management page, where CRUD operation is being done in modal pop, here it will be done at the bottom of the data grid due to specific reason i.e. to make the tinymce control fullscreen so that user has maximized view to edit the page. 
  12. Edit the app -> admin -> page -> page-list -> page-list.component.ts and replace its content with the following:   
    import { Component, OnInit, ViewChild } from '@angular/core';
    import { DBOperation } from "../../../shared/enum";
    import { IPage } from "../../../model/page";
    import { DataService } from "../../../service/data/data.service";
    import { MatDialog } from "@angular/material";
    import { Util } from "../../../shared/util";
    import { ManagePageComponent } from "../manage-page/manage-page.component";
    
    @Component({
      selector: 'app-page-list',
      templateUrl: './page-list.component.html',
      styleUrls: ['./page-list.component.css']
    })
    export class PageListComponent implements OnInit {
    
      @ViewChild('manageForm') manageForm: ManagePageComponent;
    
      isHideManagePanel: boolean = true;
      isHideCommentPanel: boolean = true;
      menuDDL: any;
      selectedValue: string;
      articleId: string;
    
    
      dbops: DBOperation;
      modalTitle: string;
      modalBtnTitle: string;
      page: IPage;
      data: any;
      url: string = "/api/page";
    
      displayedColumns: any[] = [
        {
          display: 'Title',
          variable: 'Title',
          type: 'text'
        },
        {
          display: 'Menu Code',
          variable: 'MenuCode',
          type: 'text'
        },
        {
          display: 'Status',
          variable: 'Status',
          type: 'text'
        },
        {
          display: '',
          variable: 'Edit',
          action: DBOperation.update,
          type: 'btn'
        },
        {
          display: '',
          variable: 'Delete',
          action: DBOperation.delete,
          type: 'btn'
        }
      ];
    
      hdrBtn: any[] = [
        {
          title: 'Add New Page',
          keys: [],
          action: DBOperation.create
        }];
    
      filterPlaceholder: string = "Search Menu";
      constructor(private _dataService: DataService, private dialog: MatDialog, private _util: Util) {
      }
    
      ngOnInit() {
        this.loadData();
      }
    
      loadData() {
        this.data = this._dataService.get(this.url).map(data => data.data);
      }
    
      
      gridaction(gridaction: any): void {
        switch (gridaction.action) {
          case DBOperation.create:
            this.addPage();
            break;
          case DBOperation.update:
            this.editPage(gridaction.row);
            break;
          case DBOperation.delete:
            this.deletePage(gridaction.row);
            break;
        }
      }
    
    
      addPage() {
        this.isHideManagePanel = false;
        this.dbops = DBOperation.create;
        this.modalTitle = "Add New Page";
        this.modalBtnTitle = "Add";
        this.manageForm.dbops = DBOperation.create;
        this.manageForm.manageForm();
      }
    
      editPage(page: IPage) {
        this.isHideManagePanel = false;
        this.dbops = DBOperation.update;
        this.modalTitle = "Edit Article";
        this.modalBtnTitle = "Update";
        delete page["__v"];
        this.page = <IPage>page;
        this.manageForm.dbops = DBOperation.update;
        this.manageForm.page = this.page;
        this.manageForm.manageForm();
      }
    
      deletePage(article: IPage) {
        this.isHideManagePanel = false;
        this.dbops = DBOperation.delete;
        this.modalTitle = "Confirm to Delete?";
        this.modalBtnTitle = "Delete";
        delete article["__v"];
        this.page = <IPage>article;
        this.manageForm.dbops = DBOperation.delete;
       this.manageForm.page = this.page;
       this.manageForm.manageForm();
      }
    
      refreshData(isRefershGrid: boolean) {
         if (isRefershGrid) {
          this.loadData();
          if (this.dbops == DBOperation.delete)
            this.isHideManagePanel = true;
        } 
      }
    
      hideeditPanel(ishidePanel: boolean) {
        this.isHideManagePanel = ishidePanel;
      }
      
    }
    
  13. The displayedColumns variable and loadData() function are used to display the data grid with records but rest of methods are used to add, update and delete the pages. Before digging into them, let me explain how they will work, you can see we are referencing the ManagePageComponent that we are going to create in the next steps, the Add New Page button will open a new page screen under the data grid control. This is slightly different than the Menu Management where add, edit and delete screens are opening in modal pop up. 
  14. Let's create the ManagePage component and we will come back to the PageList component after that. In TERMINAL tab, while a mazharnco folder is selected, enter the command: ng g c admin/page/ManagePage
  15. Edit the src -> app -> admin -> page -> manage-page -> manage-page.component.css and add the following CSS:  
    mat-card
    {
      margin: 0px;
      padding: 0px;
    }
    
    mat-card-noshadow{ 
        background: #ECECF4;
        box-shadow: none !important;
    }
    
    mat-card-content{
      margin: 0px;
      padding: 0px;
    }
    
    .header
    {
    background: #E90C0C;
    border-bottom: 5px solid #790606;
    height: 50px;
    padding-left: 5px;
    width: 100%;
    }
    
    .subheader
    {
    height: 40px;
    background: #5B5C60;
    }
    
    .subheader_title{
        vertical-align:baseline;
        padding-top: 5px;
        padding-bottom: 0px;
        padding-left: 5px;
        font-size: 13pt;
        font-family: Arial, Helvetica, sans-serif;
        color: #C8CCD2;
    }
    
    .header_title{
        vertical-align:baseline;
        padding-top: 10px;
        padding-bottom: 0px;
        padding-left: 5px;
        font-size: 16pt;
        font-family: Arial, Helvetica, sans-serif;
        color: #FFFFFF;
    }
    
    .card_cntnt
    {
        padding: 15px;
        padding-bottom: 15px;
    }
    
    .card_footer
    {
        text-align: left;
        padding-left: 35px;
    }
    
    .frm-ctrl {
        width: 90%;
    }
    
    .icon_align
    {
        vertical-align: middle;
    }
    .auto_save
    {
        background-color:#0D2B4A;
         padding: 5px;
         font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
         color:#FFFFFF;
         font-weight:bold;
         font-size: larger;
    }
    .mce-fullscreen {
        z-index: 1050;
    }
  16. Edit the src -> app -> admin -> page -> manage-page -> manage-page.component.html and replace its HTML with following:   
    <mat-accordion>
      <mat-expansion-panel [expanded]=true>
        <mat-expansion-panel-header style="background-color:gainsboro">
          <mat-panel-title>
          <span style="color:darkolivegreen; font-size:large">  Manage Page Panel</span>
          </mat-panel-title>
        </mat-expansion-panel-header>
      <br>
        <mat-card>
          <mat-card-header class="header">
            <mat-card-title class="header_title">
              <span><mat-icon>create</mat-icon>{{modalTitle}}</span>
            </mat-card-title>
          </mat-card-header>
          <form novalidate (ngSubmit)="onSubmit(pageFrm)" [formGroup]="pageFrm">
            <mat-card-content class="card_cntnt">
              <div class="auto_save">
                <mat-slide-toggle #autoSave (change)="autoSaveEnabled($event)">
                  Auto Save
                </mat-slide-toggle>
              </div>
              <div>
                <mat-form-field class="frm-ctrl">
                  <input matInput placeholder="Title" formControlName="Title">
                </mat-form-field>
              </div>
              <div class="row">
                <div class="col-md-6">
                  <mat-form-field class="frm-ctrl">
                    <mat-select placeholder="Select Menu/Sub Menu" formControlName="MenuCode">
                      <mat-option *ngFor="let menu of menuDDL" [value]="menu.key">
                        {{ menu.value }}
                      </mat-option>
                    </mat-select>
                  </mat-form-field>
                </div>
                <div class="col-md-6">
                  <mat-form-field class="frm-ctrl">
                    <mat-select formControlName="Status" placeholder="Page Status">
                      <mat-option *ngFor="let sts of status" [value]="sts">
                        {{sts}}
                      </mat-option>
                    </mat-select>
                  </mat-form-field>
                </div>
              </div>
              <div class="row">
                <div class="col-md-3">
                  <input type="file" #fileInput class=" btn btn-primary" />
                </div>
                <div class="col-md-9">
                  <button type="button" mat-mini-fab color="primary" (click)="upload()"><mat-icon>file_upload</mat-icon></button>
                </div>
              </div>
              <div class="clearfx" style="padding-top:10px">
              </div>
              <div class="container">
                <div class="row">
                  <div class="col-sm-2" *ngFor="let image of PageImages" style="text-align:center">
                    <img class="img-rounded imgcontainer" src="{{SERVER_URL}}/uploads/{{image.ActualName}}" width="100" height="100" />
                    <div>
                      <button type="button" mat-mini-fab color="warn" (click)="deleteImage(image._id,image.ActualName)"><mat-icon>delete</mat-icon></button>
                    </div>
                  </div>
                </div>
              </div>
              <div class="clearfx" style="padding-top:10px">
              </div>
              <div>
                <app-tinymce formControlName="Content"></app-tinymce>
              </div>
            </mat-card-content>
            <mat-card-actions class="card_footer">
              <button color="warn" type="button" mat-raised-button (click)="hidePanel()">Cancel</button>&nbsp;
              <button type="submit" color="primary" [disabled]="pageFrm.invalid" mat-raised-button>{{modalBtnTitle}}</button>
            </mat-card-actions>
          </form>
        </mat-card>
    
      </mat-expansion-panel>
    </mat-accordion>​
  17. Finally, after a long time, we are seeing Angular Material UI components and Model Driven form. This is quite a rich HTML page where we are taking page information from the user e.g. Page Title, Menu, Page Status, Upload button, and Page Content. For the Page Content, we are using tinymce control that we set up in earlier steps, you can see how easy to use this control since we are using Model Driven form, we only specify formControlName like rest of Angular Material UI component and that's it. On top, you can see we use Angular Material Slide Toggle control for autosave, this is quite helpful if you are editing long page, the autoSaveEnabled() will keep detecting the page content change and save it in database real-time. The menu items will be loaded in a dropdown from a database that we can select for the page. These menu items are managed by Manage Menu section that we developed earlier. Page Status is only used to control page visibility to end user, these are hardcoded values and not loading from any database. The Published status will make the page available to view. The upload load control will be used to upload images for the page. Remember, the same page will be used to Add, Edit and Delete the page. 
  18. Edit the src -> app -> admin -> page -> manage-page -> manage-page.component.ts and replace its code with following:   
    import { Component, OnInit, Input, Output, EventEmitter, ViewChild } from '@angular/core';
    import { FormControl, FormGroup, FormBuilder, Validators } from '@angular/forms';
    import { environment } from "../../../../environments/environment";
    import { IFiles, IPage } from "../../../model/page";
    import { DBOperation, IMenuType, IYesNo, IPageStatus, ResponseSnackbar } from "../../../shared/enum";
    import { DataService } from "../../../service/data/data.service";
    import { Util } from "../../../shared/util";
    
    @Component({
      selector: 'manage-page',
      templateUrl: './manage-page.component.html',
      styleUrls: ['./manage-page.component.css']
    })
    export class ManagePageComponent implements OnInit {
    
      @ViewChild('fileInput') fileInput;
      @ViewChild('autoSave') autoSave;
    
      SERVER_URL = environment.api_url;
    
      UPLOAD_URL: string = "/api/page/upload";
      POST_URL: string = "/api/page";
      RST_URL: string = "/api/page/id";
      GET_ALL_URL: string = "/api/page";
      DELETE_IMG: string = "/api/page/img/id/iid/fn";
    
      PageImages: IFiles[] = [];
      pages: IPage[];
    
      menuDDL: any;
      autoSaveEvnt: any;
    
    
      @Input() page: IPage;
      @Input() dbops: DBOperation;
      @Input() modalTitle: string;
      @Input() modalBtnTitle: string;
      @Input() checked: boolean = false;
    
      @Output() crudOpDone: EventEmitter<boolean> = new EventEmitter<boolean>();
      @Output() hideeditPanel: EventEmitter<boolean> = new EventEmitter<boolean>();
    
      selectedOption: string;
    
      pageFrm: FormGroup;
    
      menuType = this._util.getEnumArray(IMenuType);
      status = this._util.getEnumArray(IPageStatus);
      subEmail = this._util.getEnumArray(IYesNo);
    
      constructor(private _fb: FormBuilder, private _dataService: DataService, private _util: Util) { }
    
      ngOnInit() {
        this.menuDDL = this.getMenuddl();
        this.pageFrm = this._fb.group({
          _id: [''],
          Title: ['', [Validators.required, Validators.maxLength(500)]],
          Content: ['', [Validators.required]],
          Status: ['', [Validators.required]],
          MenuCode: [''],
          DatePublished: [''],
          DateAdded: [''],
          DateUpdated: [''],
          Images: [[]],
        });
      }
    
      getMenuddl(): any[] {
        let menuURL = "/api/menu";
        let dataMenu: { key: string, value: string }[] = [];
        this._dataService.get(menuURL).subscribe(
          data => {
            if (data.success == true) //Success
            {
              data.data.forEach((element: any) => {
                dataMenu.push({ key: element.MenuCode, value: element.MenuName });
              });
            }
            else {
              alert(JSON.stringify(data.msg));
            }
          },
          error => {
          }
        );
        return dataMenu;
      }
    
      manageForm() {
        this.autoSave.checked = this.checked;
        this.manageAutoSave(this.checked);
        this.PageImages = [];
        if (this.dbops == DBOperation.delete || this.dbops == DBOperation.update) {
          this.pageFrm.setValue(this.page);
    
          this.PageImages = this.page.Images == null ? [] : this.page.Images;
          this.pageFrm.enable();
        }
    
        else if (this.dbops == DBOperation.create) {
          this.pageFrm.reset();
          this.pageFrm.enable();
          this.PageImages = [];
        }
    
        if (this.dbops == DBOperation.delete)
          this.pageFrm.disable();
      }
    
      onSubmit(formData: any) {
        this.doCUD(formData.value);
      }
    
      doCUD(formData: any) {
        if (this.pageFrm.invalid)
          return;
        switch (this.dbops) {
          case DBOperation.create:
            delete formData._id;
            this._dataService.post(this.POST_URL, formData).subscribe(
              data => {
                if (data.success == true) //Success
                {
                  this.page = data.data;
                  delete this.page["__v"];
                  this.dbops = DBOperation.update;
                  this.modalTitle = "Edit Page";
                  this.modalBtnTitle = "Update";
                  this.manageForm();
                  this._util.openSnackBar(data.msg, ResponseSnackbar.Sucess);
                  this.crudOpDone.emit(true);
                }
                else {
                  this._util.openSnackBar(JSON.stringify(data.msg), ResponseSnackbar.Error);
                }
              },
              error => {
              });
            break;
          case DBOperation.update:
            this._dataService.put(this.POST_URL, formData).subscribe(
              data => {
                if (data.success == true) //Success
                {
                  this.PageImages = data.data;
                  this._util.openSnackBar(data.msg, ResponseSnackbar.Sucess);
                  this.crudOpDone.emit(true);
                }
                else {
                  this._util.openSnackBar(JSON.stringify(data.msg), ResponseSnackbar.Error);
                }
              },
              error => {
              });
            break;
          case DBOperation.delete:
            this._util.confirmDelete().subscribe(result => {
              if (<boolean>result == true) {
                this._dataService.delete(this.RST_URL.replace("id", formData._id)).subscribe(
                  data => {
                    if (data.success == true) //Success
                    {
                      this._util.openSnackBar(data.msg, ResponseSnackbar.Sucess);
                      this.crudOpDone.emit(true);
                    }
                    else {
                      this._util.openSnackBar(JSON.stringify(data.msg), ResponseSnackbar.Error);
                    }
                  },
                  error => {
                  });
              }
            });
            break;
        }
      }
    
      autoSaveEnabled(state) {
        this.manageAutoSave(state.checked);
      }
    
      manageAutoSave(state: boolean) {
        if (state)
          this.autoSaveEvnt = this.pageFrm.valueChanges.debounceTime(2000).subscribe(
            formData => this.doCUD(formData)
          );
        else
          if (this.autoSaveEvnt)
            this.autoSaveEvnt.unsubscribe();
      }
    
      deleteImage(imageId: string, filename: string) {
        imageId = imageId == null ? 'x' : imageId;
        this._dataService.delete(this.DELETE_IMG.replace("id", this.page != null ? this.page._id : "x")
          .replace("iid", imageId)
          .replace("fn", filename)).subscribe(
          data => {
            if (data.success == true) //Success
            {
              this._util.openSnackBar(data.msg, ResponseSnackbar.Sucess);
              this.PageImages.splice(this.PageImages.findIndex(x => x._id == imageId), 1);
              this.pageFrm.controls["Images"].patchValue(this.PageImages);
            }
            else {
              this._util.openSnackBar(JSON.stringify(data.msg), ResponseSnackbar.Error);
            }
          },
          error => {
          });
      }
    
      upload() {
        let fileBrowser = this.fileInput.nativeElement;
        if (fileBrowser.files && fileBrowser.files[0]) {
          let formData = new FormData();
          formData.append("photo", fileBrowser.files[0]);
    
          this._dataService.upload(this.UPLOAD_URL, formData).subscribe(
            data => {
              if (data.success == true) //Success
              {
                let imageData = data.data;
                let imageObj: IFiles = {
                  _id: null,
                  ActualName: imageData.filename,
                  OrigName: imageData.originalname,
                  FileType: imageData.mimetype,
                  Path: imageData.path
                };
                delete imageObj._id;
                this.PageImages.push(imageObj);
                this.pageFrm.controls["Images"].patchValue(this.PageImages);
    
                //this._util.openSnackBar(data.msg,"Success");
              }
              else {
                this._util.openSnackBar(JSON.stringify(data.msg), ResponseSnackbar.Error);
              }
            },
            error => {
            });
        }
      }
    
      hidePanel() {
        this.hideeditPanel.emit(true);
      }
    }
    
  19. This file has pretty much all functions to manage the page information and content, let's understand the important functions and blocks:
    1. First, let's understand the basic functionalities, this page will take page title, menu, status, upload images, and page content by tinymce control. When the user will upload the image, it will directly save in the database. We already have created the upload API in PART A that is saving the image file in disk storage and database. After saving, upload API is returning the image path on a disk where it is stored. We are saving the return paths in an array and sending this array to add and update APIs. When a page gets render, it directly loads the images from storage disk and display it, that's why if you see server -> mazharnco.js file, there is a line of code app.use('/uploads', express.static(path.join(__dirname, '/uploads'))); that makes it enabled. 
    2. On the top of the page, we have listed all the APIs URL to add, update, delete page and image. 
    3. The pageImages is the array to store the images path after successful image upload.
    4. You can see a couple of input variables that we are passing from PageList component, the page variable will have complete page record from the database that would be passed in case of edit or delete the page. the dpops enumeration will store current operation to be performed e.g. add, update or delete. Rest of variables are self-explanatory. 
    5. The output variables crudOpDone and hideeditPanel will be used to refresh the data grid once the record is added, updated or deleted. 
    6. The menuType and status are enumerations and loading methods are written in util to be shared among different component in case it is required. 
    7. In ngOnInit event, we are creating the Model Driven or Reactive form pageFrm with all form elements matching IPage interface. Same in a server folder, it is matching pageMdl.js
    8. The menuDDL are loading through a getMenuddl() function from the database.
    9. The manageForm function is used to enabled, disabled or resets the form based on CRUD operation, e.g. for deletion, we are disabling the form, for updating, we are loading existing page content and other information. The autoSave slider is used for continues update. 
    10. The onSubmit is used to submit the form. We are calling the doCUD method will take the form data and call the corresponding API. (I explicitly name the method CUD cause it will do Create, Update and Delete)
    11. The doCUD() method will see the input variable dbops value and perform the Add, Update or Delete operation, in case of successful response, it will open Angular Material Snack Bar control to show the message and disappear, same goes with an error message with the brief error description. After a successful response, the output variable crudOpDone will emit true to PageList component that will refresh the data grid.
    12. The manageAutoSave() method will keep monitoring the pageFrm element values and in case of an update, it will call doCUD operation to save the updates in the database. It is only selected if the slider is on otherwise it will unsubscribe the autosave event. 
    13. The upload function will be used to upload the images, you can see we are calling the upload API and getting the response back that is a full path of the image stored on a disk. This image is pushed in a pageImages array variable. 
    14. The deleteImage function is used to delete the image from disk and database. We are sending a couple of IDs to API, first we will track the page ID, then within the page, need to find the image ID since one page can have multiple images and images are defined as an array. Once an image is identified, it is deleted from an array and since we have a full image path, it will remove the image from disk after. 
  20. We need to update the routing table now to add the Page Mangement section, edit the src -> app -> app-routing.module.ts and replace its content with the following: 
    import { NgModule } from '@angular/core';
    import { Routes, RouterModule } from '@angular/router';
    import { HomeComponent } from "./client/home/home.component";
    import { ContactComponent } from "./client/contact/contact.component";
    import { ViewPageComponent } from "./client/view-page/view-page.component";
    import { AdminComponent } from './admin/admin/admin.component';
    import { MenuListComponent } from './admin/menu/menu-list/menu-list.component';
    import { UserMessageComponent } from './admin/user-message/user-message.component';
    import { PageListComponent } from './admin/page/page-list/page-list.component';
    
    const routes: Routes = [
      {
        path: '',
        redirectTo: 'home',
        pathMatch: 'full'
      },
      {
        path: 'home',
        component: HomeComponent,
      },
      {
        path: 'contact',
        component: ContactComponent,
      },
      {
        path: 'page/:id',
        component: ViewPageComponent,
      },
      {
        path:'admin',
        component: AdminComponent
      },
      {
        path:'admin/managemenu',
        component: MenuListComponent,
      }
      ,
      {
        path:'admin/usermsg',
        component:UserMessageComponent
      },
      {
        path:'admin/managepage',
        component: PageListComponent
      }
    ];
    
    @NgModule({
      imports: [RouterModule.forRoot(routes)],
      exports: [RouterModule]
    })
    export class AppRoutingModule { }
    
  21. Now if you go back to src -> app -> admin -> page -> page-list.component.ts, hopefully the code will make sense to you now. We have reference of ManagePageComponent because it is now displayed at the bottom of data grid control when user does the CRUD operation. The gridaction() function is taking the gridaction parameter that is of DBOperation enumeration type.
    1. The addPage(), editPage(), and deletePage() functions are setting the ManagePage component UI e.g. page and button title, the other thing is it is assigning the values to ManagePage component's input variables i.e. dbops for CRUD operation (adding new record, updating or deleting the existing record).
    2. The page object with all page information from database is to fill-up the form for an update and delete operations. At the end of each function, it is calling manageForm() function that is defined in ManagePage component to set up the initial records in pageFrm form and enable or disable it based on the operation.
    3. The refereshData() function is being triggered by ManagePage component's function doCUD() when new record is added, existing record is updated or deleted. Remember the line this.crudOpDone.emit(true) in doCUD() function and now check the src -> app -> admin -> page -> page-list.component.html, at the bottom where we are embedding the manage-page component, the crudOpDone is defined as output variable and when it triggers, it is calling the refershData() function with the input parameter value being passed from ManagePage component (true or false).
  22. For uploading the image, we need to send content-type of multipart/form-data, we are going to create a separate upload API of POST verb, so let's edit the app -> service -> data -> data.service.ts and replace the 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";
    
    import { HttpClient, HttpHeaders } from '@angular/common/http';
    
    @Injectable()
    export class DataService {
    
      SERVER_URL = environment.api_url;
    
      constructor(public _http: HttpClient) { }
    
      get(url: string): Observable<any> {
        let headers = new HttpHeaders({ 'Content-Type': 'application/json' });
        return this._http.get(this.SERVER_URL + url, { headers: headers });
      }
    
      post(url: string, model: any): Observable<any> {
        let body = JSON.stringify(model);
        let headers = new HttpHeaders({ 'Content-Type': 'application/json' });
        return this._http.post(this.SERVER_URL + url, body, { headers: headers });
      }
    
      put(url: string, model: any): Observable<any> {
        let body = JSON.stringify(model);
        let headers = new HttpHeaders({ 'Content-Type': 'application/json' });
        return this._http.put(this.SERVER_URL + url, body, { headers: headers });
      }
    
      delete(url: string): Observable<any> {
        let headers = new HttpHeaders({ 'Content-Type': 'application/json' });
        return this._http.delete(this.SERVER_URL + url, { headers: headers });
      }
    
      upload(url: string, model: any): Observable<any> {
        let body = model;
        let headers = new HttpHeaders({ 'Content-Type': 'multipart/form-data' });
        return this._http.post(this.SERVER_URL + url, body);
      }
    
    }
    
  23. In the end, we created the uploaded API and specify the content-type of multipart/form-data instead of application/json that will send the image to server API. Rest is a simple POST call. 
  24. next, we are simplifying the app.component.css, edit the src -> app -> app.component.css and replace the content with the following:  
    .mat-dialog-container {
      padding: 0 !important;
      margin: 0 !important;
      }
    
    .main_div{
      overflow-x: hidden;
      overflow-y: hidden;
      padding-left: 5px;
      padding-right: 5px; 
    }
    .mnc-logo{
      width: 100%;
      max-width: 350px;
      height: auto;
    }
    .socail_btn_padding{
       padding-top:0px; 
    }
    .socail_btn{
      width: 100%;
      max-width: 40px;
      height: auto;
      cursor: pointer;
    }
    .image {
      opacity: 1;
      transition: .5s ease;
      backface-visibility: hidden;
    }
    
    .image:hover {
      opacity: 0.4;
    }
    
    .navbar_shadow{
      z-index:1;
      box-shadow: 5px 5px 8px #bfc4cc
    }
    .map_div{
        padding-top: 10px;
    }
    .vst_img{
        width: 48px;
        height: 48px;
        background-image: url("../assets/images/visitus.png");
         box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
        background-size: cover;
      }
    .header{
    background: #45484d;
    border-bottom: 5px solid #393B3E;
    height: 50px;
    padding-left: 5px;
    }
    .header_title{
        vertical-align:baseline;
        padding-top: 10px;
        padding-bottom: 0px;
        padding-left: 5px;
        font-size: 16pt;
        font-family: Arial, Helvetica, sans-serif;
        color: #A1A9AF;
    }
    mat-card{
      margin: 0px !important;;
      padding: 0px !important;;
    }
    mat-card-content{
      margin: 0px;
      padding: 0px;
    }
    agm-map {
        height: 335px;
      }
    .map_div{
        width: 100%;
        max-width: 500px;
        height: auto;
      }
    .lgn_btn
      {
        width: 100%;
        max-width: 130px;
        height: auto;
        cursor: pointer;
      }
    .bannerStyle h1 {
        background-color: #ccc;
        min-height: 300px;
        text-align: center;
        line-height: 300px;
    }
    
    .dropdown-menu .sub-menu {
      left: 100%;
      position: absolute;
      top: 0;
      visibility: hidden;
      margin-top: -1px;
    }
    
    .dropdown-menu li:hover .sub-menu {
      visibility: visible;
    }
    
    .dropdown:hover .dropdown-menu {
      display: block;
    }
    
    .nav-tabs .dropdown-menu, .nav-pills .dropdown-menu, .navbar .dropdown-menu {
      margin-top: 0;
    }
    
    .dropdown:hover {
      background-color: #f2f0f0;
    }
    
    .navbar .sub-menu:before {
      border-bottom: 7px solid transparent;
      border-left: none;
      border-right: 7px solid rgba(0, 0, 0, 0.2);
      border-top: 7px solid transparent;
      left: -7px;
      top: 10px;
    }
    
    .navbar .sub-menu:after {
      border-top: 6px solid transparent;
      border-left: none;
      border-right: 6px solid #fff;
      border-bottom: 6px solid transparent;
      left: 10px;
      top: 11px;
      left: -6px;
    }
    
  25. Cool, run both server and client application (serverpm2 start environment.json --env development, Client: ng serve -o) and browse the URL: http://localhost:4200/admin Click on Pages Management link, you should be landed to PageList component, if you tested the Page API in Part A through Postman, you might be seeing some records. Try to add, update and delete the new page.
  26. In the next part, we will develop view-page to render the page dynamically once user will select a top menu item. 


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, angular data grid control, free angular data grid control, tinymce angular, angular text editor, angular dynamic page, multer, mime-types, save files on storage in node.js