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, I can’t promise you would understand all the steps I am going to list down without knowledge of previous parts.
- 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.
- 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 the menu is now loading from the database and it doesn’t know its page. Also, the view-page component in the client folder needs to be updated to load the page content from the database and render it.
- 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:
- In page.ts, we are creating two interfaces
IPage
andIFiles
, theIPage
is a primary interface to store the page information and it hasImages
variable ofIFiles
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. - Next, let’s set up the tinymce control in our application, there are a few steps involved. Let’s do them one by one:
- The first step is to install the tinymce control, I cannot guarantee you that latest version would work so I am explicitly mentioning 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
- 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:
- The first step is to install the tinymce control, I cannot guarantee you that latest version would work so I am explicitly mentioning version 2.1.2. Right-click on a mazharncoweb folder and select the option, Open in Terminal, in TERMINAL tab, enter the command:
- 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. - 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.
- 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.
- That’s pretty much it on tinymce configuration.
- 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 command:
ng 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. - 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;
}
- This is the same CSS we been using in rest of components, nothing new.
- 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>
- We are using our custom grid control to view all the page records from the database, it almost the 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.
- 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.
- Edit the app -> admin -> page -> page-list -> page-list.component.ts and replace its content with the following:
- The
displayedColumns
variable andloadData()
function is used to display the data grid with records but the rest of the 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 theManagePageComponent
that we are going to create in the next steps, theAdd 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. - Let’s create the
ManagePage
component and we will come back to thePageList
component after that. In TERMINAL tab, while a mazharnco folder is selected, enter the command:ng g c admin/page/ManagePage
- 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;
}
- 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>
<button type="submit" color="primary" [disabled]="pageFrm.invalid" mat-raised-button>{{modalBtnTitle}}</button>
</mat-card-actions>
</form>
</mat-card>
</mat-expansion-panel>
</mat-accordion>
- 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 a long page, theautoSaveEnabled()
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. - Edit the src -> app -> admin -> page -> manage-page -> manage-page.component.ts and replace its code with following:
- 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 a long page, theautoSaveEnabled()
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. - Edit the src -> app -> admin -> page -> manage-page -> manage-page.component.ts and replace its code with following:
- This file has pretty much all functions to manage the page information and content, let’s understand the important functions and blocks:
- 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 displays 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. - On the top of the page, we have listed all the APIs URL to add, update, delete page and image.
- The
pageImages
is the array to store the images path after successful image upload. - You can see a couple of input variables that we are passing from
PageList
component, the page variable will have a complete page record from the database that would be passed in case of edit or delete the page. thedpops
enumeration will store current operations to be performed e.g. add, update or delete. The rest of the variables are self-explanatory. - The output variables
crudOpDone
andhideeditPanel
will be used to refresh the data grid once the record is added, updated or deleted. - The
menuType
and status is enumerations and loading methods are written in util to be shared among different component in case it is required. - In
ngOnInit
event, we are creating the Model-Driven or Reactive formpageFrm
with all form elements matchingIPage
interface. Same in a server folder, it is matching pageMdl.js. - The
menuDDL
are loading through agetMenuddl()
function from the database. - 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. TheautoSave
slider is used for continuous updates. - 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) - The
doCUD()
method will see the input variabledbops
value and perform the Add, Update or Delete operation, in case of a successful response, it will open Angular Material Snack Bar control to show the message and disappear, the same goes with an error message with the brief error description. After a successful response, the output variablecrudOpDone
will emit true toPageList
component that will refresh the data grid. - The
manageAutoSave()
method will keep monitoring thepageFrm
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. - 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. - 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, we 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.
- 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 displays it, that’s why if you see server -> mazharnco.js file, there is a line of code
- We need to update the routing table now to add the Page Management section, edit the src -> app -> app-routing.module.ts and replace its content with the following:
- 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 a reference of
ManagePageComponent
because it is now displayed at the bottom of data grid control when the user does the CRUD operation. Thegridaction()
function is taking the gridaction parameter that is ofDBOperation
enumeration type.- The
addPage()
,editPage()
, anddeletePage()
functions are setting theManagePage
component UI e.g. page and button title, the other thing is it is assigning the values toManagePage
component’s input variables i.e.dbops
for CRUD operation (adding a new records, updating or deleting the existing record). - The
page
object with all page information from the database is to fill-up the form for an update and delete operations. At the end of each function, it is callingmanageForm()
function that is defined inManagePage
component to set up the initial records in pageFrm form and enable or disable it based on the operation. - The refereshData() function is being triggered by
ManagePage
component’s function doCUD() when a new record is added, existing record is updated or deleted. Remember the linethis.crudOpDone.emit(true)
indoCUD()
function and now check the src -> app -> admin -> page -> page-list.component.html, at the bottom where we are embedding themanage-page
component, thecrudOpDone
is defined as output variable and when it triggers, it is calling therefershData()
function with the input parameter value being passed fromManagePage
component (true or false).
- The
- For uploading the image, we need to send
content-type
ofmultipart/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:
- In the end, we created the uploaded API and specify the content-type of
multipart/form-data
instead ofapplication/json
that will send the image to the server API. Rest is a simple POST call. - 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;
}
- Cool, run both server and client application (server:
pm2 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 toPageList
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. - In the next part, we will develop a view-page to render the page dynamically once the user will select a top menu item.