In this MEAN Stack (Angular 8) tutorial, we will build a simple blog CMS that can add, edit, delete and view blog post and category. So, there are two entities or models that required for this simple blog CMS. There only authenticated users that can access this CMS. After user login, user can do CRUD (create, read, update, delete) operation on post and category models.
Table of Contents:
- New Node Express.js App using Express Generator
- Install Mongoose.js
- Install Passport.js
- Add Mongoose Models or Schemas
- Add Express Router for Login and Register
- Add Secure Express Router for Category CRUD
- Add Secure Express Router for Post CRUD
- Add Non-Secure Express Router for Front Page
- New Angular 8 Web App using Angular CLI
- Add Angular 8 Routing and Navigation
- Add a custom Angular 8 HttpInterceptor
- Add Angular 8 Service (HttpClient, RxJS, Observable)
- Add Angular 8 Material and CDK
- Add Angular Material Login and Register Components
- Add Angular Material Blog Category CRUD Component
- Add Angular Material Blog Post CRUD Component
- Secure the Components using Angular 8 Route Guard
- Add Angular Material Blog Front Page
- Run and Test the MEAN Stack (Angular 8) Blog CMS
The following tools, frameworks, and modules are required for this tutorial achievement:
- Node.js
- MongoDB
- Angular 8
- Angular CLI
- Express.js
- Passport.js
- Mongoose.js
- CKEditor 4
- Terminal or Command Line
- IDE or Text Editor
Before the move to the main steps of this tutorial, make sure that you have installed Node.js and MongoDB on your machine. You can check the Node.js version after installing it from the terminal or Node.js command line.
node -v
v10.15.1
npm -v
6.11.3
New Node Express.js App using Express Generator
As you see in the first paragraph of this tutorial the terms "E" is Express.js. Express is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications. To create Express.js app, we will be using Express generator. Type this command to install it.
sudo npm install -g express-generator
Next, create an Express.js app by typing this command.
express blog-cms --no-view
Go to the newly created blog-cms folder then install all NPM modules.
cd ./blog-cms
npm install
Now, we have this Express.js app structure for the blog-cms app.
To check and sanitize the Express.js app, run this app for the first time.
nodemon
or
npm start
Then you will see this page when open the browser and go to `localhost:3000`.
Install Mongoose.js
We will use Mongoose as the ODM for MongoDB. Mongoose provides a straight-forward, schema-based solution to model your application data. It includes built-in type casting, validation, query building, business logic hooks and more, out of the box. To install Mongoose.js and it's required dependencies, type this command.
npm install --save mongoose bluebird
Next, open and edit `app.js` then declare the Mongoose module.
var mongoose = require('mongoose');
Create a connection to the MongoDB server using these lines of codes.
mongoose.connect('mongodb://localhost/blog-cms', {
promiseLibrary: require('bluebird'),
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true
}).then(() => console.log('connection successful'))
.catch((err) => console.error(err));
Now, if you re-run again Express.js server after running MongoDB server or daemon, you will see this information in the console.
[nodemon] 1.18.6
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: *.*
[nodemon] starting `node ./bin/www`
connection successful
That's mean, the connection to the MongoDB is successful.
Install Passport.js
We will use Passport.js for authentication or user login. Passport is authentication middleware for Node.js. Extremely flexible and modular, Passport can be unobtrusively dropped into any Express-based web application. A comprehensive set of strategies support authentication using a username and password, Facebook, Twitter, and more. To install Passport.js and it's required dependencies, type this command.
npm install --save bcrypt-nodejs jsonwebtoken morgan passport passport-jwt
Create a new folder to holds configuration files then add configuration files to `config` folder.
mkdir config
touch config/settings.js
touch config/passport.js
Open and edit `config/settings.js` then add these lines of codes.
module.exports = {
'secret':'mevnsecure'
};
That file holds a secret code for generating a JWT token. Next, open and edit `config/passport.js` then add these lines of codes.
var JwtStrategy = require('passport-jwt').Strategy,
ExtractJwt = require('passport-jwt').ExtractJwt;
// load up the user model
var User = require('../models/user');
var settings = require('../config/settings'); // get settings file
module.exports = function(passport) {
var opts = {};
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderWithScheme("jwt");
opts.secretOrKey = settings.secret;
passport.use(new JwtStrategy(opts, function(jwt_payload, done) {
User.findOne({id: jwt_payload.id}, function(err, user) {
if (err) {
return done(err, false);
}
if (user) {
done(null, user);
} else {
done(null, false);
}
});
}));
};
This config is used for getting the user by matching JWT token with token get from the client. This configuration needs to create a User model. Now, Open and edit `app.js` then declare required library for initializing with the server by adding these lines of requires.
var passport = require('passport');
Declare a variable for Authentication, Category, and Post route.
var auth = require('./routes/auth');
var category = require('./routes/category');
var post = require('./routes/post');
Initialize passport by adding this line after the declaration of app variable.
app.use(passport.initialize());
Add API route to the endpoint URL after other `use` function.
app.use('/api/auth', auth);
app.use('/api/category', category);
app.use('/api/post', post);
Add Mongoose Models or Schemas
We will use MongoDB collections for User, Category, and Post. For that, we need to create new Mongoose models or schemas for them. First, create a new folder in the root of the project folder that holds the Mongoose models or schemas files then add those required models files.
mkdir models
touch models/User.js
touch models/Category.js
touch models/Post.js
Open and edit `models/User.js` then add these codes of the required Username and Password fields. Also, password encryption using Bcrypt and compare password that saved in the MongoDB collection and encrypted plain password from the request body.
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var bcrypt = require('bcrypt-nodejs');
var UserSchema = new Schema({
username: {
type: String,
unique: true,
required: true
},
password: {
type: String,
required: true
}
});
UserSchema.pre('save', function (next) {
var user = this;
if (this.isModified('password') || this.isNew) {
bcrypt.genSalt(10, function (err, salt) {
if (err) {
return next(err);
}
bcrypt.hash(user.password, salt, null, function (err, hash) {
if (err) {
return next(err);
}
user.password = hash;
next();
});
});
} else {
return next();
}
});
UserSchema.methods.comparePassword = function (passw, cb) {
bcrypt.compare(passw, this.password, function (err, isMatch) {
if (err) {
return cb(err);
}
cb(null, isMatch);
});
};
module.exports = mongoose.model('User', UserSchema);
Next, open and edit `models/Category.js` then add these Javascript codes of the required fields for the category.
var mongoose = require('mongoose');
var CategorySchema = new mongoose.Schema({
id: String,
catName: String,
catDesc: String,
catImgUrl: String,
catContent: String,
updated: { type: Date, default: Date.now },
});
module.exports = mongoose.model('Category', CategorySchema);
Next, open and edit `models/Post.js` then add these Javascript codes of the required fields for the article post including a reference from the Category collection.
var mongoose = require('mongoose'), Schema = mongoose.Schema;
var PostSchema = new mongoose.Schema({
category : { type: Schema.Types.ObjectId, ref: 'Category' },
id: String,
postTitle: String,
postAuthor: String,
postDesc: String,
postContent: String,
postReference: String,
postImgUrl: String,
created: { type: Date },
updated: { type: Date, default: Date.now },
});
module.exports = mongoose.model('Post', PostSchema);
Add Express Router for Login and Register
We will create a Router for the user login/register, restricted category, and post resources. In the Express routes folder creates a new Javascript file by type this command.
touch routes/auth.js
Open and edit `routes/auth.js` then declares all require variables of Mongoose models, Passport.js, JWT, Express Router, and Configuration file.
var mongoose = require('mongoose');
var passport = require('passport');
var config = require('../config/database');
require('../config/passport')(passport);
var express = require('express');
var jwt = require('jsonwebtoken');
var router = express.Router();
var User = require("../models/user");
Create a router to register the new user using just a username and password.
router.post('/login', function(req, res) {
if (!req.body.username || !req.body.password) {
res.json({success: false, msg: 'Please pass username and password.'});
} else {
var newUser = new User({
username: req.body.username,
password: req.body.password
});
// save the user
newUser.save(function(err) {
if (err) {
return res.json({success: false, msg: 'Username already exists.'});
}
res.json({success: true, msg: 'Successful created new user.'});
});
}
});
Create a router for login or sign-in using username and password.
router.post('/register', function(req, res) {
User.findOne({
username: req.body.username
}, function(err, user) {
if (err) throw err;
if (!user) {
res.status(401).send({success: false, msg: 'Authentication failed. User not found.'});
} else {
// check if password matches
user.comparePassword(req.body.password, function (err, isMatch) {
if (isMatch && !err) {
// if user is found and password is right create a token
var token = jwt.sign(user.toJSON(), config.secret);
// return the information including token as JSON
res.json({success: true, token: 'JWT ' + token});
} else {
res.status(401).send({success: false, msg: 'Authentication failed. Wrong password.'});
}
});
}
});
});
Create a router for logout.
router.post('/logout', passport.authenticate('jwt', { session: false}), function(req, res) {
req.logout();
res.json({success: true});
});
Next, export router as a module.
module.exports = router;
Add Secure Express Router for Category CRUD
Next, we will add a router that contains CRUD operation for the Category. First, create a new file inside the routes folder.
touch routes/category.js
Open and edit `routes/category.js` then add these required modules of the Mongoose model, Passport.js, JWT, Express Router, and Configuration file.
var passport = require('passport');
var config = require('../config/database');
require('../config/passport')(passport);
var express = require('express');
var jwt = require('jsonwebtoken');
var router = express.Router();
var Category = require("../models/category");
Add a route to get the list of the category.
router.get('/', passport.authenticate('jwt', { session: false}), function(req, res) {
var token = getToken(req.headers);
if (token) {
Category.find(function (err, categories) {
if (err) return next(err);
res.json(categories);
});
} else {
return res.status(403).send({success: false, msg: 'Unauthorized.'});
}
});
Add a route to get a single category by ID.
router.get('/:id', passport.authenticate('jwt', { session: false}), function(req, res, next) {
var token = getToken(req.headers);
if (token) {
Category.findById(req.params.id, function (err, category) {
if (err) return next(err);
res.json(category);
});
} else {
return res.status(403).send({success: false, msg: 'Unauthorized.'});
}
});
Add a route to post a category.
router.post('/', passport.authenticate('jwt', { session: false}), function(req, res, next) {
var token = getToken(req.headers);
if (token) {
Category.create(req.body, function (err, category) {
if (err) return next(err);
res.json(category);
});
} else {
return res.status(403).send({success: false, msg: 'Unauthorized.'});
}
});
Add a route to put a category by ID.
router.put('/:id', passport.authenticate('jwt', { session: false}), function(req, res, next) {
var token = getToken(req.headers);
if (token) {
Category.findByIdAndUpdate(req.params.id, req.body, function (err, category) {
if (err) return next(err);
res.json(category);
});
} else {
return res.status(403).send({success: false, msg: 'Unauthorized.'});
}
});
Add a route to delete a category by ID.
router.delete('/:id', passport.authenticate('jwt', { session: false}), function(req, res, next) {
var token = getToken(req.headers);
if (token) {
Category.findByIdAndRemove(req.params.id, req.body, function (err, category) {
if (err) return next(err);
res.json(category);
});
} else {
return res.status(403).send({success: false, msg: 'Unauthorized.'});
}
});
Add a function to get and extract the token from the request headers.
getToken = function (headers) {
if (headers && headers.authorization) {
var parted = headers.authorization.split(' ');
if (parted.length === 2) {
return parted[1];
} else {
return null;
}
} else {
return null;
}
};
Next, export router as a module.
module.exports = router;
Add Secure Express Router for Post CRUD
Next, we will add a router that contains CRUD operation for the Category. First, create a new file inside the routes folder.
touch routes/post.js
Open and edit `routes/post.js` then add these required modules of the Mongoose model, Passport.js, JWT, Express Router, and Configuration file.
var passport = require('passport');
var config = require('../config/database');
require('../config/passport')(passport);
var express = require('express');
var jwt = require('jsonwebtoken');
var router = express.Router();
var Post = require("../models/category");
Add a route to GET the list of posts.
router.get('/', passport.authenticate('jwt', { session: false}), function(req, res) {
var token = getToken(req.headers);
if (token) {
Post.find(function (err, posts) {
if (err) return next(err);
res.json(posts);
});
} else {
return res.status(403).send({success: false, msg: 'Unauthorized.'});
}
});
Add a route to GET a single post data by ID.
router.get('/:id', passport.authenticate('jwt', { session: false}), function(req, res, next) {
var token = getToken(req.headers);
if (token) {
Post.findById(req.params.id, function (err, post) {
if (err) return next(err);
res.json(post);
});
} else {
return res.status(403).send({success: false, msg: 'Unauthorized.'});
}
});
Add a route to POST a post data.
router.post('/', passport.authenticate('jwt', { session: false}), function(req, res, next) {
var token = getToken(req.headers);
if (token) {
Post.create(req.body, function (err, post) {
if (err) return next(err);
res.json(post);
});
} else {
return res.status(403).send({success: false, msg: 'Unauthorized.'});
}
});
Add a router to PUT a post data by ID.
router.put('/:id', passport.authenticate('jwt', { session: false}), function(req, res, next) {
var token = getToken(req.headers);
if (token) {
Post.findByIdAndUpdate(req.params.id, req.body, function (err, post) {
if (err) return next(err);
res.json(post);
});
} else {
return res.status(403).send({success: false, msg: 'Unauthorized.'});
}
});
Add a router to DELETE a post data by ID.
router.delete('/:id', passport.authenticate('jwt', { session: false}), function(req, res, next) {
var token = getToken(req.headers);
if (token) {
Post.findByIdAndRemove(req.params.id, req.body, function (err, post) {
if (err) return next(err);
res.json(post);
});
} else {
return res.status(403).send({success: false, msg: 'Unauthorized.'});
}
});
Add a function to get and extract the token from the request headers.
getToken = function (headers) {
if (headers && headers.authorization) {
var parted = headers.authorization.split(' ');
if (parted.length === 2) {
return parted[1];
} else {
return null;
}
} else {
return null;
}
};
Next, export the router a module.
module.exports = router;
Add Non-Secure Express Router for Front Page
We will use an existing Express.js `routes/index.js` to route the required data from the category and post. For that, open and edit `routes/index.js` then add the required Mongoose schema modules.
var Category = require("../models/category");
var Post = require("../models/post");
Next, add these lines of routers to GET category list, post list, and single post by ID.
router.get('/category', function(req, res, next) {
Category.find(function (err, categories) {
if (err) return next(err);
res.json(categories);
});
});
router.get('/bycategory/:id', function(req, res, next) {
Post.find({category: req.params.id}, function (err, posts) {
if (err) return next(err);
res.json(posts);
});
});
router.get('/post', function(req, res, next) {
Post.find(function (err, posts) {
if (err) return next(err);
res.json(posts);
});
});
router.get('/post/:id', function(req, res, next) {
Post.findById(req.params.id, function (err, post) {
if (err) return next(err);
res.json(post);
});
});
module.exports = router;
New Angular 8 Web App using Angular CLI
For the client-side, we will use Angular 8. First, we have to install the Angular 8 CLI. The Angular CLI is a tool to initialize, develop, scaffold and maintain Angular 8 applications. Go to your Node project folder then type this command for installing the Angular-CLI.
sudo npm install -g @angular/cli
Now, we have the latest version of Angular when this example was written.
_ _ ____ _ ___
/ \ _ __ __ _ _ _| | __ _ _ __ / ___| | |_ _|
/ △ \ | '_ \ / _` | | | | |/ _` | '__| | | | | | |
/ ___ \| | | | (_| | |_| | | (_| | | | |___| |___ | |
/_/ \_\_| |_|\__, |\__,_|_|\__,_|_| \____|_____|___|
|___/
Angular CLI: 8.3.5
Node: 10.15.1
OS: darwin x64
Angular:
...
Package Version
------------------------------------------------------
@angular-devkit/architect 0.803.5
@angular-devkit/core 8.3.5
@angular-devkit/schematics 8.3.5
@schematics/angular 8.3.5
@schematics/update 0.803.5
rxjs 6.4.0
Next, create an Angular 8 web app for this Blog CMS App by typing this command.
ng new client
Answer all questions like below which we will add Angular Routing and use SCSS as a stylesheet.
? Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? SCSS [ https://sass-lang.com/documentation/syntax#scss ]
Next, to sanitize the newly created Angular 8 project go to that project folder in the new terminal tab then run the Angular application.
cd ./client
ng serve
You will this page or view when you open "http://localhost:4200/" in your browser which means the Angular 8 is ready to go.
ng serve
Open the browser then go to this URL `localhost:4200` and you will see this Angular 8 landing page.
Add Angular 8 Routing and Navigation
On the previous steps, we have to add Angular 8 Routes when answering the questions. Now, we just added the required pages for Category CRUD, Post CRUD, Login, Register, Home Page, and Post Details. Type this commands to add the Angular 8 components or pages.
ng g component auth/login
ng g component auth/register
ng g component home
ng g component admin
ng g component bycategory
ng g component details
ng g component category
ng g component category/category-details
ng g component category/category-add
ng g component category/category-edit
ng g component post
ng g component post/post-details
ng g component post/post-add
ng g component post/post-edit
Open `src/app/app.module.ts` then you will see those components imported and declared in `@NgModule` declarations. Next, open and edit `src/app/app-routing.module.ts` then add these imports of login, register, home, details, category CRUD, post CRUD components.
import { LoginComponent } from './auth/login/login.component';
import { RegisterComponent } from './auth/register/register.component';
import { HomeComponent } from './home/home.component';
import { DetailsComponent } from './details/details.component';
import { AdminComponent } from './admin/admin.component';
import { CategoryComponent } from './category/category.component';
import { PostComponent } from './post/post.component';
import { CategoryDetailsComponent } from './category/category-details/category-details.component';
import { CategoryAddComponent } from './category/category-add/category-add.component';
import { CategoryEditComponent } from './category/category-edit/category-edit.component';
import { PostDetailsComponent } from './post/post-details/post-details.component';
import { PostAddComponent } from './post/post-add/post-add.component';
import { PostEditComponent } from './post/post-edit/post-edit.component';
import { BycategoryComponent } from './bycategory/bycategory.component';
Add these arrays of those components routes to the existing routes constant.
const routes: Routes = [
{
path: '',
redirectTo: 'home',
pathMatch: 'full'
},
{
path: 'home',
component: HomeComponent,
data: { title: 'Blog Home' }
},
{
path: 'admin',
component: AdminComponent,
data: { title: 'Blog Admin' }
},
{
path: 'bycategory/:id',
component: BycategoryComponent,
data: { title: 'Post by Category' }
},
{
path: 'details/:id',
component: DetailsComponent,
data: { title: 'Show Post Details' }
},
{
path: 'login',
component: LoginComponent,
data: { title: 'Login' }
},
{
path: 'register',
component: RegisterComponent,
data: { title: 'Register' }
},
{
path: 'category',
component: CategoryComponent,
data: { title: 'Category' }
},
{
path: 'category/details/:id',
component: CategoryDetailsComponent,
data: { title: 'Category Details' }
},
{
path: 'category/add',
component: CategoryAddComponent,
data: { title: 'Category Add' }
},
{
path: 'category/edit/:id',
component: CategoryEditComponent,
data: { title: 'Category Edit' }
},
{
path: 'post',
component: PostComponent,
data: { title: 'Post' }
},
{
path: 'post/details/:id',
component: PostDetailsComponent,
data: { title: 'Post Details' }
},
{
path: 'post/add',
component: PostAddComponent,
data: { title: 'Post Add' }
},
{
path: 'post/edit/:id',
component: PostEditComponent,
data: { title: 'Post Edit' }
}
];
Open and edit `src/app/app.component.html` and you will see the existing router outlet. Next, modify this HTML page to fit the CRUD page wrapped by <router-outlet>.
<div class="container">
<router-outlet></router-outlet>
</div>
Open and edit `src/app/app.component.scss` then replace all SCSS codes with this.
.container {
padding: 20px;
}
Add a custom Angular 8 HttpInterceptor
To intercept the JWT token that generated from the successful login to every secure HTTP request, we will use Angular 8 HttpInterceptor. Before creating a custom Angular 8 HttpInterceptor, create a folder with the name `client/src/app/interceptors`. Next, create a file for the custom Angular 8 HttpInterceptor with the name `client/src/app/interceptors/token.interceptor.ts`. Open and edit that file the add these imports of the required HTTP handler and RxJS.
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor,
HttpResponse,
HttpErrorResponse
} from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
import { Router } from '@angular/router';
Create a class that implementing HttpInterceptor method.
@Injectable()
export class TokenInterceptor implements HttpInterceptor {
}
Inject the required module to the constructor inside the class.
constructor(private router: Router) {}
Implement a custom Interceptor function.
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = localStorage.getItem('token');
if (token) {
request = request.clone({
setHeaders: {
'Authorization': token
}
});
}
if (!request.headers.has('Content-Type')) {
request = request.clone({
setHeaders: {
'content-type': 'application/json'
}
});
}
request = request.clone({
headers: request.headers.set('Accept', 'application/json')
});
return next.handle(request).pipe(
map((event: HttpEvent<any>) => {
if (event instanceof HttpResponse) {
console.log('event--->>>', event);
}
return event;
}),
catchError((error: HttpErrorResponse) => {
console.log(error);
if (error.status === 401) {
this.router.navigate(['login']);
}
if (error.status === 400) {
alert(error.error);
}
return throwError(error);
}));
}
Next, we have to register this custom HttpInterceptor and HttpClientModule. Open and edit `client/src/app.module.ts` then add these imports of HTTP_INTERCEPTORS, HttpClientModule, and TokenInterceptor.
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { TokenInterceptor } from './interceptors/token.interceptor';
Add `HttpClientModule` to the `@NgModule` imports array.
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule
],
Add the `Interceptor` modules to the provider array of the `@NgModule`.
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: TokenInterceptor,
multi: true
}
],
Now, the HTTP interceptor is ready to intercept any request to the API.
Add Angular 8 Service (HttpClient, RxJS, Observable)
To access or consume the Node Express REST API, we need to create a service for that. The Angular 8 service will contain all login, register, secure, and non-secure CRUD REST API call operation. Type these commands to generate the Angular 8 services from the client folder.
ng g service auth
ng g service home
ng g service category
ng g service post
Next, open and edit `client/src/app/auth.service.ts` then add these imports of HttpClient, RxJS Observable, of, catchError, and tap.
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
Declare a constant variable as Spring Boot REST API URL after the imports.
const apiUrl = 'http://localhost:3000/api/auth/';
Declare the variables before the constructor that will use by Angular 8 Route Guard.
@Output() isLoggedIn: EventEmitter<any> = new EventEmitter();
loggedInStatus = false;
redirectUrl: string;
Inject the `HttpClient` module inside the constructor.
constructor(private http: HttpClient) { }
Create all required functions for Login, Logout, Register, and helper functions.
login(data: any): Observable<any> {
return this.http.post<any>(apiUrl + 'login', data)
.pipe(
tap(_ => {
this.isLoggedIn.emit(true);
this.loggedInStatus = true;
}),
catchError(this.handleError('login', []))
);
}
logout(): Observable<any> {
return this.http.post<any>(apiUrl + 'logout', {})
.pipe(
tap(_ => {
this.isLoggedIn.emit(false);
this.loggedInStatus = false;
}),
catchError(this.handleError('logout', []))
);
}
register(data: any): Observable<any> {
return this.http.post<any>(apiUrl + 'register', data)
.pipe(
tap(_ => this.log('login')),
catchError(this.handleError('login', []))
);
}
private handleError<T>(operation = 'operation', result?: T) {
return (error: any): Observable<T> => {
console.error(error); // log to console instead
this.log(`${operation} failed: ${error.message}`);
return of(result as T);
};
}
private log(message: string) {
console.log(message);
}
Next, create an object class that represents category data `client/src/app/category/category.ts` then replace all file contents with these.
export class Category {
id: number;
catName: string;
catDesc: string;
catImgUrl: string;
catContent: string;
updated: Date;
}
Next, create an object class that represents category data `client/src/app/post/post.ts` then replace all file contents with these.
export class Post {
category: string;
id: string;
postTitle: string;
postAuthor: string;
postDesc: string;
postContent: string;
postReference: string;
postImgUrl: string;
created: Date;
updated: Date;
}
Next, open and edit `client/src/app/services/home.service.ts` then replace all codes with this REST API call for the home and details pages.
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { Category } from './category/category';
import { Post } from './post/post';
const apiUrl = 'http://localhost:3000/api/public/';
@Injectable({
providedIn: 'root'
})
export class HomeService {
constructor(private http: HttpClient) { }
getCategories(): Observable<Category[]> {
return this.http.get<Category[]>(apiUrl + 'category')
.pipe(
tap(_ => this.log('fetched Categories')),
catchError(this.handleError('getCategories', []))
);
}
getPosts(): Observable<Post[]> {
return this.http.get<Post[]>(apiUrl + 'post')
.pipe(
tap(_ => this.log('fetched Posts')),
catchError(this.handleError('getPosts', []))
);
}
getPostsByCategory(id: any): Observable<Post[]> {
return this.http.get<Post[]>(apiUrl + 'bycategory/' + id)
.pipe(
tap(_ => this.log('fetched Posts')),
catchError(this.handleError('getPosts', []))
);
}
getPost(id: any): Observable<Post> {
return this.http.get<Post>(apiUrl + 'post/' + id).pipe(
tap(_ => console.log(`fetched post by id=${id}`)),
catchError(this.handleError<Post>(`getPost id=${id}`))
);
}
private handleError<T>(operation = 'operation', result?: T) {
return (error: any): Observable<T> => {
console.error(error); // log to console instead
this.log(`${operation} failed: ${error.message}`);
return of(result as T);
};
}
private log(message: string) {
console.log(message);
}
}
Next, open and edit `client/src/app/services/category.service.ts` then replace all codes with this REST API call for the category CRUD operation.
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { Category } from './category/category';
const apiUrl = 'http://localhost:3000/api/category/';
@Injectable({
providedIn: 'root'
})
export class CategoryService {
constructor(private http: HttpClient) { }
getCategories(): Observable<Category[]> {
return this.http.get<Category[]>(apiUrl)
.pipe(
tap(_ => this.log('fetched Categories')),
catchError(this.handleError('getCategories', []))
);
}
getCategory(id: any): Observable<Category> {
const url = `${apiUrl}/${id}`;
return this.http.get<Category>(url).pipe(
tap(_ => console.log(`fetched category by id=${id}`)),
catchError(this.handleError<Category>(`getCategory id=${id}`))
);
}
addCategory(category: Category): Observable<Category> {
return this.http.post<Category>(apiUrl, category).pipe(
tap((prod: Category) => console.log(`added category w/ id=${category.id}`)),
catchError(this.handleError<Category>('addCategory'))
);
}
updateCategory(id: any, category: Category): Observable<any> {
const url = `${apiUrl}/${id}`;
return this.http.put(url, category).pipe(
tap(_ => console.log(`updated category id=${id}`)),
catchError(this.handleError<any>('updateCategory'))
);
}
deleteCategory(id: any): Observable<Category> {
const url = `${apiUrl}/${id}`;
return this.http.delete<Category>(url).pipe(
tap(_ => console.log(`deleted category id=${id}`)),
catchError(this.handleError<Category>('deleteCategory'))
);
}
private handleError<T>(operation = 'operation', result?: T) {
return (error: any): Observable<T> => {
console.error(error); // log to console instead
this.log(`${operation} failed: ${error.message}`);
return of(result as T);
};
}
private log(message: string) {
console.log(message);
}
}
Next, open and edit `client/src/app/services/post.service.ts` then replace all codes with this REST API call for the post CRUD operation.
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { Post } from './post/post';
const apiUrl = 'http://localhost:3000/api/post/';
@Injectable({
providedIn: 'root'
})
export class PostService {
constructor(private http: HttpClient) { }
getPosts(): Observable<Post[]> {
return this.http.get<Post[]>(apiUrl)
.pipe(
tap(_ => this.log('fetched Posts')),
catchError(this.handleError('getPosts', []))
);
}
getPost(id: any): Observable<Post> {
const url = `${apiUrl}/${id}`;
return this.http.get<Post>(url).pipe(
tap(_ => console.log(`fetched post by id=${id}`)),
catchError(this.handleError<Post>(`getPost id=${id}`))
);
}
addPost(post: Post): Observable<Post> {
return this.http.post<Post>(apiUrl, post).pipe(
tap((prod: Post) => console.log(`added post w/ id=${post.id}`)),
catchError(this.handleError<Post>('addPost'))
);
}
updatePost(id: any, post: Post): Observable<any> {
const url = `${apiUrl}/${id}`;
return this.http.put(url, post).pipe(
tap(_ => console.log(`updated post id=${id}`)),
catchError(this.handleError<any>('updatePost'))
);
}
deletePost(id: any): Observable<Post> {
const url = `${apiUrl}/${id}`;
return this.http.delete<Post>(url).pipe(
tap(_ => console.log(`deleted post id=${id}`)),
catchError(this.handleError<Post>('deletePost'))
);
}
private handleError<T>(operation = 'operation', result?: T) {
return (error: any): Observable<T> => {
console.error(error); // log to console instead
this.log(`${operation} failed: ${error.message}`);
return of(result as T);
};
}
private log(message: string) {
console.log(message);
}
}
Add Angular 8 Material and CDK
Next, for the user interface (UI) we will use Angular 8 Material and CDK. There's a CLI for generating a Material component like Table as a component, but we will create or add the Table component from scratch to existing component. Type this command to install Angular 8 Material (@angular/material).
ng add @angular/material
If there are questions like below, just use the default answer.
? Choose a prebuilt theme name, or "custom" for a custom theme: Indigo/Pink [
Preview: https://material.angular.io?theme=indigo-pink ]
? Set up HammerJS for gesture recognition? Yes
? Set up browser animations for Angular Material? Yes
We will register all required Angular 8 Material components or modules to `src/app/app.module.ts`. Open and edit that file then add these imports of required Angular Material Components.
import {
MatInputModule,
MatPaginatorModule,
MatProgressSpinnerModule,
MatSortModule,
MatTableModule,
MatIconModule,
MatButtonModule,
MatCardModule,
MatFormFieldModule,
MatMenuModule,
MatToolbarModule } from '@angular/material';
Also, modify `FormsModule` import to add `ReactiveFormsModule`.
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
Register the above modules to `@NgModule` imports.
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule,
BrowserAnimationsModule,
FormsModule,
ReactiveFormsModule,
MatInputModule,
MatTableModule,
MatPaginatorModule,
MatSortModule,
MatProgressSpinnerModule,
MatIconModule,
MatButtonModule,
MatCardModule,
MatFormFieldModule,
MatMenuModule,
MatToolbarModule
],
Add Angular Material Login and Register Components
This time for authentication part. Open and edit `client/src/app/auth/login/login.component.ts` then add these imports.
import { FormControl, FormGroupDirective, FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';
import { AuthService } from '../../auth.service';
import { Router } from '@angular/router';
import { ErrorStateMatcher } from '@angular/material/core';
Declare these variables before the constructor.
loginForm: FormGroup;
username = '';
password = '';
matcher = new MyErrorStateMatcher();
isLoadingResults = false;
Inject the imported modules to the constructor.
constructor(private formBuilder: FormBuilder, private router: Router, private authService: AuthService) { }
Initialize `NgForm` to the `NgOnInit` function.
ngOnInit() {
this.loginForm = this.formBuilder.group({
'username' : [null, Validators.required],
'password' : [null, Validators.required]
});
}
Add a function to submit the login form.
onFormSubmit(form: NgForm) {
this.authService.login(form)
.subscribe(res => {
console.log(res);
if (res.token) {
localStorage.setItem('token', res.token);
this.router.navigate(['admin']);
}
}, (err) => {
console.log(err);
});
}
Add a function to go to the Register page.
register() {
this.router.navigate(['register']);
}
Add a class that handles the form validation above this class.
/** Error when invalid control is dirty, touched, or submitted. */
export class MyErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const isSubmitted = form && form.submitted;
return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));
}
}
Next, open and edit `client/src/app/auth/login/login.component.html` then replace all HTML tags with these.
<div class="example-container mat-elevation-z8">
<div class="example-loading-shade"
*ngIf="isLoadingResults">
<mat-spinner *ngIf="isLoadingResults"></mat-spinner>
</div>
<mat-card class="example-card">
<form [formGroup]="loginForm" (ngSubmit)="onFormSubmit(loginForm.value)">
<mat-form-field class="example-full-width">
<input matInput type="email" placeholder="Email" formControlName="username"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!loginForm.get('username').valid && loginForm.get('username').touched">Please enter your username</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<input matInput type="password" placeholder="Password" formControlName="password"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!loginForm.get('password').valid && loginForm.get('password').touched">Please enter your password</span>
</mat-error>
</mat-form-field>
<div class="button-row">
<button type="submit" [disabled]="!loginForm.valid" mat-flat-button color="primary">Login</button>
</div>
<div class="button-row">
<button type="button" mat-flat-button color="primary" (click)="register()">Register</button>
</div>
</form>
</mat-card>
</div>
Next, for register page, open and edit `client/src/app/auth/register/register.component.ts` then replace all Typescript codes with these.
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroupDirective, FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';
import { AuthService } from '../../auth.service';
import { Router } from '@angular/router';
import { ErrorStateMatcher } from '@angular/material/core';
/** Error when invalid control is dirty, touched, or submitted. */
export class MyErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const isSubmitted = form && form.submitted;
return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));
}
}
@Component({
selector: 'app-register',
templateUrl: './register.component.html',
styleUrls: ['./register.component.scss']
})
export class RegisterComponent implements OnInit {
registerForm: FormGroup;
fullName = '';
username = '';
password = '';
isLoadingResults = false;
matcher = new MyErrorStateMatcher();
constructor(private formBuilder: FormBuilder, private router: Router, private authService: AuthService) { }
ngOnInit() {
this.registerForm = this.formBuilder.group({
fullName : [null, Validators.required],
username : [null, Validators.required],
password : [null, Validators.required]
});
}
onFormSubmit(form: NgForm) {
this.authService.register(form)
.subscribe(res => {
this.router.navigate(['login']);
}, (err) => {
console.log(err);
alert(err.error);
});
}
}
Next, open and edit `client/src/app/auth/register/register.component.html` then replace all HTML tags with these.
<div class="example-container mat-elevation-z8">
<div class="example-loading-shade"
*ngIf="isLoadingResults">
<mat-spinner *ngIf="isLoadingResults"></mat-spinner>
</div>
<mat-card class="example-card">
<form [formGroup]="registerForm" (ngSubmit)="onFormSubmit(registerForm.value)">
<mat-form-field class="example-full-width">
<input matInput type="fullName" placeholder="Full Name" formControlName="fullName"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!registerForm.get('fullName').valid && registerForm.get('fullName').touched">Please enter your Full Name</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<input matInput type="email" placeholder="Email" formControlName="username"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!registerForm.get('username').valid && registerForm.get('username').touched">Please enter your username</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<input matInput type="password" placeholder="Password" formControlName="password"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!registerForm.get('password').valid && registerForm.get('password').touched">Please enter your password</span>
</mat-error>
</mat-form-field>
<div class="button-row">
<button type="submit" [disabled]="!registerForm.valid" mat-flat-button color="primary">Register</button>
</div>
</form>
</mat-card>
</div>
Add Angular Material Blog Category CRUD Component
The blog category is part of the secure Angular component. We will add Angular Material CRUD components for this. The way to build this complete CRUD component is almost the same as previous steps on Angular login and register. So, we will show you directly through the source codes. Begin with the list of categories, open and edit `src/app/category/category.component.ts` then add these lines of codes. There should be a function to load the list of Categories that put to data array and declare a loading spinner and fields selection for the Angular Material table.
import { Component, OnInit } from '@angular/core';
import { CategoryService } from '../category.service';
import { Category } from './category';
@Component({
selector: 'app-category',
templateUrl: './category.component.html',
styleUrls: ['./category.component.scss']
})
export class CategoryComponent implements OnInit {
displayedColumns: string[] = ['catName', 'catDesc'];
data: Category[] = [];
isLoadingResults = true;
constructor(private api: CategoryService) { }
ngOnInit() {
this.api.getCategories()
.subscribe((res: any) => {
this.data = res;
console.log(this.data);
this.isLoadingResults = false;
}, err => {
console.log(err);
this.isLoadingResults = false;
});
}
}
Next, open and edit `src/app/category/category.component.html` then replace all HTML tags with these Angular Material components that contain a table to display a list of categories.
<div class="example-container mat-elevation-z8">
<div class="example-loading-shade"
*ngIf="isLoadingResults">
<mat-spinner *ngIf="isLoadingResults"></mat-spinner>
</div>
<div class="button-row">
<a mat-flat-button color="primary" [routerLink]="['add']"><mat-icon>add</mat-icon></a>
</div>
<div class="mat-elevation-z8">
<table mat-table [dataSource]="data" class="example-table"
matSort matSortActive="catName" matSortDisableClear matSortDirection="asc">
<!-- Category Name Column -->
<ng-container matColumnDef="catName">
<th mat-header-cell *matHeaderCellDef>Category Name</th>
<td mat-cell *matCellDef="let row">{{row.catName}}</td>
</ng-container>
<!-- Category Description Column -->
<ng-container matColumnDef="catDesc">
<th mat-header-cell *matHeaderCellDef>Category Description</th>
<td mat-cell *matCellDef="let row">{{row.catDesc}}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;" [routerLink]="['details/', row._id]"></tr>
</table>
</div>
</div>
Next, open and edit `src/app/category/category-details/category-details.component.ts` then replace all Typescript codes with these codes that contain the functions for load a single category by ID and delete a category by ID. The loaded category data hold put to category type data object.
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { CategoryService } from './../../category.service';
import { Category } from '../category';
@Component({
selector: 'app-category-details',
templateUrl: './category-details.component.html',
styleUrls: ['./category-details.component.scss']
})
export class CategoryDetailsComponent implements OnInit {
category: Category = { id: null, catName: '', catDesc: '', catImgUrl: '', catContent: '', updated: null };
isLoadingResults = true;
constructor(private route: ActivatedRoute, private api: CategoryService, private router: Router) { }
ngOnInit() {
this.getCategoryDetails(this.route.snapshot.params.id);
}
getCategoryDetails(id: any) {
this.api.getCategory(id)
.subscribe((data: any) => {
this.category = data;
console.log(this.category);
this.isLoadingResults = false;
});
}
deleteCategory(id: any) {
this.isLoadingResults = true;
this.api.deleteCategory(id)
.subscribe(res => {
this.isLoadingResults = false;
this.router.navigate(['/category']);
}, (err) => {
console.log(err);
this.isLoadingResults = false;
}
);
}
}
Next, open and edit `src/app/category/category-details/category-details.component.html` then replace all HTML tags with these Angular Material components that contain a card that displays category details.
<div class="example-container mat-elevation-z8">
<div class="example-loading-shade"
*ngIf="isLoadingResults">
<mat-spinner *ngIf="isLoadingResults"></mat-spinner>
</div>
<div class="button-row">
<a mat-flat-button color="primary" [routerLink]="['/category']"><mat-icon>list</mat-icon></a>
</div>
<mat-card class="example-card">
<mat-card-header>
<mat-card-title><h2>{{category.catName}}</h2></mat-card-title>
<mat-card-subtitle>{{category.catDesc}}</mat-card-subtitle>
</mat-card-header>
<img mat-card-image src="{{category.catImgUrl}}" alt="{{category.catName}}">
<mat-card-content>
<dl>
<dt>Category Content:</dt>
<dd [innerHTML]="category.catContent"></dd>
<dt>Updated At:</dt>
<dd>{{category.updated | date}}</dd>
</dl>
</mat-card-content>
<mat-card-actions>
<a mat-flat-button color="primary" [routerLink]="['/category/edit', category?.id || 'all']"><mat-icon>edit</mat-icon></a>
<a mat-flat-button color="warn" (click)="deleteCategory(category.id)"><mat-icon>delete</mat-icon></a>
</mat-card-actions>
</mat-card>
</div>
For add and edit category form we will need a Rich Text editor. For this, we will install CKEditor that fortunately supported Angular. To install it simply run this command.
npm install --save ckeditor4-angular
npm install mat-contenteditable --save
Next, open and edit `src/app/app.module.ts` then add this import.
import { CKEditorModule } from 'ckeditor4-angular';
import { MatContenteditableModule } from 'mat-contenteditable';
Add to the @NgModule imports.
imports: [
...
CKEditorModule,
MatContenteditableModule
],
Next, open and edit `src/app/category/category-add/category-add.component.ts` then replace all Typescript codes with these codes that contain the Angular Form, FormBuilder, FormGroup, FormControl, FormGroupDirective, Validators, ErrorStateMatcher, etc. The Angular FormGroup initialize by the FormBuilder then the passed validation form fields can submit to the REST API.
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { CategoryService } from '../../category.service';
import { FormControl, FormGroupDirective, FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';
import { ErrorStateMatcher } from '@angular/material/core';
/** Error when invalid control is dirty, touched, or submitted. */
export class MyErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const isSubmitted = form && form.submitted;
return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));
}
}
@Component({
selector: 'app-category-add',
templateUrl: './category-add.component.html',
styleUrls: ['./category-add.component.scss']
})
export class CategoryAddComponent implements OnInit {
categoryForm: FormGroup;
catName = '';
catDesc = '';
catImgUrl = '';
catContent = '';
isLoadingResults = false;
matcher = new MyErrorStateMatcher();
constructor(private router: Router, private api: CategoryService, private formBuilder: FormBuilder) { }
ngOnInit() {
this.categoryForm = this.formBuilder.group({
catName : [null, Validators.required],
catDesc : [null, Validators.required],
catImgUrl : [null, Validators.required],
catContent : [null, Validators.required]
});
}
onFormSubmit() {
this.isLoadingResults = true;
this.api.addCategory(this.categoryForm.value)
.subscribe((res: any) => {
const id = res._id;
this.isLoadingResults = false;
this.router.navigate(['/category-details', id]);
}, (err: any) => {
console.log(err);
this.isLoadingResults = false;
});
}
}
Next, open and edit `src/app/category/category-add/category-add.component.html` then replace all HTML tags with these Angular Material components that contain Angular FormGroup, FormControl, Material Input, Textarea, and Additional CKEditor 4 that previously installs.
<div class="example-container mat-elevation-z8">
<div class="example-loading-shade"
*ngIf="isLoadingResults">
<mat-spinner *ngIf="isLoadingResults"></mat-spinner>
</div>
<div class="button-row">
<a mat-flat-button color="primary" [routerLink]="['/category']"><mat-icon>list</mat-icon></a>
</div>
<mat-card class="example-card">
<form [formGroup]="categoryForm" (ngSubmit)="onFormSubmit()">
<mat-form-field class="example-full-width">
<input matInput placeholder="Category Name" formControlName="catName"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!categoryForm.get('catName').valid && categoryForm.get('catName').touched">Please enter Category Name</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<textarea matInput placeholder="Category Desc" formControlName="catDesc"
[errorStateMatcher]="matcher"></textarea>
<mat-error>
<span *ngIf="!categoryForm.get('catDesc').valid && categoryForm.get('catDesc').touched">Please enter Category Description</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<input matInput placeholder="Category Image URL" formControlName="catImgUrl"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!categoryForm.get('catImgUrl').valid && categoryForm.get('catImgUrl').touched">Please enter Category Image URL</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<ckeditor matCkeditor placehoder="Category Content" formControlName="catContent"></ckeditor>
<mat-error>
<span *ngIf="!categoryForm.get('catContent').valid && categoryForm.get('catContent').touched">Please enter Category Description</span>
</mat-error>
</mat-form-field>
<div class="button-row">
<button type="submit" [disabled]="!categoryForm.valid" mat-flat-button color="primary"><mat-icon>save</mat-icon></button>
</div>
</form>
</mat-card>
</div>
Next, open and edit `src/app/category/category-edit/category-edit.component.ts` then replace all Typescript codes with these codes that contain the Angular Form, FormBuilder, FormGroup, FormControl, FormGroupDirective, Validators, ErrorStateMatcher, etc. The Angular FormGroup initialize by the FormBuilder and fill with the category that loaded by getting category function then the passed validation form fields can submit to the REST API.
import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { CategoryService } from '../../category.service';
import { FormControl, FormGroupDirective, FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';
import { ErrorStateMatcher } from '@angular/material/core';
/** Error when invalid control is dirty, touched, or submitted. */
export class MyErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const isSubmitted = form && form.submitted;
return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));
}
}
@Component({
selector: 'app-category-edit',
templateUrl: './category-edit.component.html',
styleUrls: ['./category-edit.component.scss']
})
export class CategoryEditComponent implements OnInit {
categoryForm: FormGroup;
id = '';
catName = '';
catDesc = '';
catImgUrl = '';
catContent = '';
updated: Date = null;
isLoadingResults = false;
matcher = new MyErrorStateMatcher();
constructor(private router: Router, private route: ActivatedRoute, private api: CategoryService, private formBuilder: FormBuilder) { }
ngOnInit() {
this.getCategory(this.route.snapshot.params.id);
this.categoryForm = this.formBuilder.group({
catName : [null, Validators.required],
catDesc : [null, Validators.required],
catImgUrl : [null, Validators.required],
catContent : [null, Validators.required]
});
}
getCategory(id: any) {
this.api.getCategory(id).subscribe((data: any) => {
this.id = data.id;
this.categoryForm.setValue({
prod_name: data.prod_name,
prod_desc: data.prod_desc,
prod_price: data.prod_price
});
});
}
onFormSubmit() {
this.isLoadingResults = true;
this.api.updateCategory(this.id, this.categoryForm.value)
.subscribe((res: any) => {
const id = res.id;
this.isLoadingResults = false;
this.router.navigate(['/category-details', id]);
}, (err: any) => {
console.log(err);
this.isLoadingResults = false;
}
);
}
categoryDetails() {
this.router.navigate(['/category-details', this.id]);
}
}
Next, open and edit `src/app/category/category-edit/category-edit.component.html` then replace all HTML tags with these Angular Material components that contain Angular FormGroup, FormControl, Material Input, Textarea, and Additional CKEditor 4 that previously installs.
<div class="example-container mat-elevation-z8">
<div class="example-loading-shade"
*ngIf="isLoadingResults">
<mat-spinner *ngIf="isLoadingResults"></mat-spinner>
</div>
<div class="button-row">
<a mat-flat-button color="primary" (click)="categoryDetails()"><mat-icon>list</mat-icon></a>
</div>
<mat-card class="example-card">
<form [formGroup]="categoryForm" (ngSubmit)="onFormSubmit()">
<mat-form-field class="example-full-width">
<input matInput placeholder="Category Name" formControlName="catName"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!categoryForm.get('catName').valid && categoryForm.get('catName').touched">Please enter Category Name</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<textarea matInput placeholder="Category Desc" formControlName="catDesc"
[errorStateMatcher]="matcher"></textarea>
<mat-error>
<span *ngIf="!categoryForm.get('catDesc').valid && categoryForm.get('catDesc').touched">Please enter Category Description</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<input matInput placeholder="Category Image URL" formControlName="catImgUrl"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!categoryForm.get('catImgUrl').valid && categoryForm.get('catImgUrl').touched">Please enter Category Image URL</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<ckeditor matCkeditor placehoder="Category Content" formControlName="catContent"></ckeditor>
<mat-error>
<span *ngIf="!categoryForm.get('catContent').valid && categoryForm.get('catContent').touched">Please enter Category Description</span>
</mat-error>
</mat-form-field>
<div class="button-row">
<button type="submit" [disabled]="!categoryForm.valid" mat-flat-button color="primary"><mat-icon>save</mat-icon></button>
</div>
</form>
</mat-card>
</div>
Add Angular Material Blog Post CRUD Component
The blog post is part of the secure Angular component. We will add Angular Material CRUD components for this. The way to build this complete CRUD component is same as previous steps on the blog post. So, we will show you directly through the source codes. Begin with the list of post, open and edit `src/app/post/post.component.ts` then add these lines of codes. There should be a function to load the list of posts that put to data array and declare a loading spinner and fields selection for the Angular Material table.
import { Component, OnInit } from '@angular/core';
import { PostService } from '../post.service';
import { Post } from './post';
@Component({
selector: 'app-post',
templateUrl: './post.component.html',
styleUrls: ['./post.component.scss']
})
export class PostComponent implements OnInit {
displayedColumns: string[] = ['postTitle', 'postDesc'];
data: Post[] = [];
isLoadingResults = true;
constructor(private api: PostService) { }
ngOnInit() {
this.api.getPosts()
.subscribe((res: any) => {
this.data = res;
console.log(this.data);
this.isLoadingResults = false;
}, err => {
console.log(err);
this.isLoadingResults = false;
});
}
}
Next, open and edit `src/app/post/post.component.html` then replace all HTML tags with these Angular Material components that contain a table to display a list of posts.
<div class="example-container mat-elevation-z8">
<div class="example-loading-shade"
*ngIf="isLoadingResults">
<mat-spinner *ngIf="isLoadingResults"></mat-spinner>
</div>
<div class="button-row">
<a mat-flat-button color="primary" [routerLink]="['add']"><mat-icon>add</mat-icon></a>
</div>
<div class="mat-elevation-z8">
<table mat-table [dataSource]="data" class="example-table"
matSort matSortActive="postTitle" matSortDisableClear matSortDirection="asc">
<!-- Post Name Column -->
<ng-container matColumnDef="postTitle">
<th mat-header-cell *matHeaderCellDef>Post Title</th>
<td mat-cell *matCellDef="let row">{{row.postTitle}}</td>
</ng-container>
<!-- Post Description Column -->
<ng-container matColumnDef="postDesc">
<th mat-header-cell *matHeaderCellDef>Post Description</th>
<td mat-cell *matCellDef="let row">{{row.postDesc}}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;" [routerLink]="['details/', row._id]"></tr>
</table>
</div>
</div>
Next, open and edit `src/app/post/post-details/post-details.component.ts` then replace all Typescript codes with these codes that contain the functions for load a single post by ID and delete a post by ID. The loaded post data hold put to post type data object.
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { PostService } from './../../post.service';
import { Post } from '../post';
@Component({
selector: 'app-post-details',
templateUrl: './post-details.component.html',
styleUrls: ['./post-details.component.scss']
})
export class PostDetailsComponent implements OnInit {
post: Post = {
category: '',
id: '',
postTitle: '',
postAuthor: '',
postDesc: '',
postContent: '',
postReference: '',
postImgUrl: '',
created: null,
updated: null
};
isLoadingResults = true;
constructor(private route: ActivatedRoute, private api: PostService, private router: Router) { }
ngOnInit() {
this.getPostDetails(this.route.snapshot.params.id);
}
getPostDetails(id: any) {
this.api.getPost(id)
.subscribe((data: any) => {
this.post = data;
this.post.id = data._id;
console.log(this.post);
this.isLoadingResults = false;
});
}
deletePost(id: any) {
this.isLoadingResults = true;
this.api.deletePost(id)
.subscribe(res => {
this.isLoadingResults = false;
this.router.navigate(['/post']);
}, (err) => {
console.log(err);
this.isLoadingResults = false;
}
);
}
}
Next, open and edit `src/app/post/post-details/post-details.component.html` then replace all HTML tags with these Angular Material components that contain a card that displays post details.
<div class="example-container mat-elevation-z8">
<div class="example-loading-shade"
*ngIf="isLoadingResults">
<mat-spinner *ngIf="isLoadingResults"></mat-spinner>
</div>
<div class="button-row">
<a mat-flat-button color="primary" [routerLink]="['/post']"><mat-icon>list</mat-icon></a>
</div>
<mat-card class="example-card">
<mat-card-header>
<mat-card-title><h2>{{post.postTitle}}</h2></mat-card-title>
<p>Created by: {{post.postAuthor}}, {{post.created | date}}, updated: {{post.updated | date}}</p>
<mat-card-subtitle>{{post.postDesc}}</mat-card-subtitle>
</mat-card-header>
<img mat-card-image src="{{post.postImgUrl}}" alt="{{post.postTitle}}">
<mat-card-content>
<dl>
<dt>Post Content:</dt>
<dd [innerHTML]="post.postContent"></dd>
<dt>Reference:</dt>
<dd>{{post.postReference}}</dd>
</dl>
</mat-card-content>
<mat-card-actions>
<a mat-flat-button color="primary" [routerLink]="['/post/edit', post.id]"><mat-icon>edit</mat-icon></a>
<a mat-flat-button color="warn" (click)="deletePost(post.id)"><mat-icon>delete</mat-icon></a>
</mat-card-actions>
</mat-card>
</div>
Next, open and edit `src/app/post/post-add/post-add.component.ts` then replace all Typescript codes with these codes that contain the Angular Form, FormBuilder, FormGroup, FormControl, FormGroupDirective, Validators, ErrorStateMatcher, etc. The Angular FormGroup initialize by the FormBuilder then the passed validation form fields can submit to the REST API.
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { PostService } from '../../post.service';
import { FormControl, FormGroupDirective, FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';
import { ErrorStateMatcher } from '@angular/material/core';
import { CategoryService } from '../../category.service';
import { Category } from './../../category/category';
/** Error when invalid control is dirty, touched, or submitted. */
export class MyErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const isSubmitted = form && form.submitted;
return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));
}
}
@Component({
selector: 'app-post-add',
templateUrl: './post-add.component.html',
styleUrls: ['./post-add.component.scss']
})
export class PostAddComponent implements OnInit {
postForm: FormGroup;
category = '';
postTitle = '';
postAuthor = '';
postDesc = '';
postContent = '';
postReference = '';
postImgUrl = '';
isLoadingResults = false;
matcher = new MyErrorStateMatcher();
categories: Category[] = [];
constructor(
private router: Router,
private api: PostService,
private catApi: CategoryService,
private formBuilder: FormBuilder) { }
ngOnInit() {
this.getCategories();
this.postForm = this.formBuilder.group({
category : [null, Validators.required],
postTitle : [null, Validators.required],
postAuthor : [null, Validators.required],
postDesc : [null, Validators.required],
postContent : [null, Validators.required],
postReference : [null, Validators.required],
postImgUrl : [null, Validators.required]
});
}
onFormSubmit() {
this.isLoadingResults = true;
this.api.addPost(this.postForm.value)
.subscribe((res: any) => {
const id = res._id;
this.isLoadingResults = false;
this.router.navigate(['/post/details', id]);
}, (err: any) => {
console.log(err);
this.isLoadingResults = false;
});
}
getCategories() {
this.catApi.getCategories()
.subscribe((res: any) => {
this.categories = res;
console.log(this.categories);
this.isLoadingResults = false;
}, err => {
console.log(err);
this.isLoadingResults = false;
});
}
}
Next, open and edit `src/app/post/post-add/post-add.component.html` then replace all HTML tags with these Angular Material components that contain Angular FormGroup, FormControl, Material Input, Textarea, and Additional CKEditor 4 that previously installs.
<div class="example-container mat-elevation-z8">
<div class="example-loading-shade"
*ngIf="isLoadingResults">
<mat-spinner *ngIf="isLoadingResults"></mat-spinner>
</div>
<div class="button-row">
<a mat-flat-button color="primary" [routerLink]="['/post']"><mat-icon>list</mat-icon></a>
</div>
<mat-card class="example-card">
<form [formGroup]="postForm" (ngSubmit)="onFormSubmit()">
<mat-form-field class="example-full-width">
<mat-select formControlName="category" [errorStateMatcher]="matcher">
<mat-option *ngFor="let cat of categories" [value]="cat._id">
{{cat.catName}}
</mat-option>
</mat-select>
<mat-error>
<span *ngIf="!postForm.get('category').valid && postForm.get('category').touched">Please select Category</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<input matInput placeholder="Post Title" formControlName="postTitle"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!postForm.get('postTitle').valid && postForm.get('postTitle').touched">Please enter Post Title</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<input matInput placeholder="Post Author" formControlName="postAuthor"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!postForm.get('postAuthor').valid && postForm.get('postAuthor').touched">Please enter Post Author</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<textarea matInput placeholder="Post Desc" formControlName="postDesc"
[errorStateMatcher]="matcher"></textarea>
<mat-error>
<span *ngIf="!postForm.get('postDesc').valid && postForm.get('postDesc').touched">Please enter Post Description</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<ckeditor matCkeditor placehoder="Post Content" formControlName="postContent"></ckeditor>
<mat-error>
<span *ngIf="!postForm.get('postContent').valid && postForm.get('postContent').touched">Please enter Post Content</span>
</mat-error>
</mat-form-field>
<div class="button-row">
<mat-form-field class="example-full-width">
<input matInput placeholder="Post Reference" formControlName="postReference"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!postForm.get('postReference').valid && postForm.get('postReference').touched">Please enter Post Ref</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<input matInput placeholder="Post Image URL" formControlName="postImgUrl"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!postForm.get('postImgUrl').valid && postForm.get('postImgUrl').touched">Please enter Post Image URL</span>
</mat-error>
</mat-form-field>
<button type="submit" [disabled]="!postForm.valid" mat-flat-button color="primary"><mat-icon>save</mat-icon></button>
</div>
</form>
</mat-card>
</div>
Next, open and edit `src/app/post/post-edit/post-edit.component.ts` then replace all Typescript codes with these codes that contain the Angular Form, FormBuilder, FormGroup, FormControl, FormGroupDirective, Validators, ErrorStateMatcher, etc. The Angular FormGroup initialize by the FormBuilder and fill with the post that loaded by getting post function then the passed validation form fields can submit to the REST API.
import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { PostService } from '../../post.service';
import { FormControl, FormGroupDirective, FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';
import { ErrorStateMatcher } from '@angular/material/core';
import { CategoryService } from '../../category.service';
import { Category } from './../../category/category';
/** Error when invalid control is dirty, touched, or submitted. */
export class MyErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const isSubmitted = form && form.submitted;
return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));
}
}
@Component({
selector: 'app-post-edit',
templateUrl: './post-edit.component.html',
styleUrls: ['./post-edit.component.scss']
})
export class PostEditComponent implements OnInit {
postForm: FormGroup;
category = '';
id = '';
postTitle = '';
postAuthor = '';
postDesc = '';
postContent = '';
postReference = '';
postImgUrl = '';
updated: Date = null;
isLoadingResults = false;
matcher = new MyErrorStateMatcher();
categories: Category[] = [];
constructor(
private router: Router,
private route: ActivatedRoute,
private api: PostService,
private catApi: CategoryService,
private formBuilder: FormBuilder) { }
ngOnInit() {
this.getCategories();
this.getPost(this.route.snapshot.params.id);
this.postForm = this.formBuilder.group({
postTitle : [null, Validators.required],
postAuthor : [null, Validators.required],
postDesc : [null, Validators.required],
postContent : [null, Validators.required],
postReference : [null, Validators.required],
postImgUrl : [null, Validators.required]
});
}
getPost(id: any) {
this.api.getPost(id).subscribe((data: any) => {
this.id = data.id;
this.postForm.setValue({
postTitle: data.postTitle,
postAuthor: data.postAuthor,
postDesc: data.postDesc,
postContent: data.postContent,
postReference: data.postReference,
postImgUrl: data.postImgUrl
});
});
}
getCategories() {
this.catApi.getCategories()
.subscribe((res: any) => {
this.categories = res;
console.log(this.categories);
this.isLoadingResults = false;
}, err => {
console.log(err);
this.isLoadingResults = false;
});
}
onFormSubmit() {
this.isLoadingResults = true;
this.api.updatePost(this.id, this.postForm.value)
.subscribe((res: any) => {
const id = res.id;
this.isLoadingResults = false;
this.router.navigate(['/post-details', id]);
}, (err: any) => {
console.log(err);
this.isLoadingResults = false;
}
);
}
postDetails() {
this.router.navigate(['/post-details', this.id]);
}
}
Next, open and edit `src/app/post/post-edit/post-edit.component.html` then replace all HTML tags with these Angular Material components that contain Angular FormGroup, FormControl, Material Input, Textarea, and Additional CKEditor 4 that previously installs.
<div class="example-container mat-elevation-z8">
<div class="example-loading-shade"
*ngIf="isLoadingResults">
<mat-spinner *ngIf="isLoadingResults"></mat-spinner>
</div>
<div class="button-row">
<a mat-flat-button color="primary" (click)="postDetails()"><mat-icon>list</mat-icon></a>
</div>
<mat-card class="example-card">
<form [formGroup]="postForm" (ngSubmit)="onFormSubmit()">
<mat-form-field class="example-full-width">
<mat-select formControlName="category" [errorStateMatcher]="matcher">
<mat-option *ngFor="let cat of categories" [value]="cat.id">
{{cat.catName}}
</mat-option>
</mat-select>
<mat-error>
<span *ngIf="!postForm.get('category').valid && postForm.get('category').touched">Please select Category</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<input matInput placeholder="Post Title" formControlName="postTitle"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!postForm.get('postTitle').valid && postForm.get('postTitle').touched">Please enter Post Title</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<input matInput placeholder="Post Author" formControlName="postAuthor"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!postForm.get('postAuthor').valid && postForm.get('postAuthor').touched">Please enter Post Author</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<textarea matInput placeholder="Post Desc" formControlName="postDesc"
[errorStateMatcher]="matcher"></textarea>
<mat-error>
<span *ngIf="!postForm.get('postDesc').valid && postForm.get('postDesc').touched">Please enter Post Description</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<ckeditor matInput placehoder="Post Content" formControlName="postContent"></ckeditor>
<mat-error>
<span *ngIf="!postForm.get('postContent').valid && postForm.get('postContent').touched">Please enter Post Content</span>
</mat-error>
</mat-form-field>
<div class="button-row">
<mat-form-field class="example-full-width">
<input matInput placeholder="Post Reference" formControlName="postReference"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!postForm.get('postReference').valid && postForm.get('postReference').touched">Please enter Post Ref</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<input matInput placeholder="Post Image URL" formControlName="catImgUrl"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!postForm.get('catImgUrl').valid && postForm.get('catImgUrl').touched">Please enter Post Image URL</span>
</mat-error>
</mat-form-field>
<button type="submit" [disabled]="!postForm.valid" mat-flat-button color="primary"><mat-icon>save</mat-icon></button>
</div>
</form>
</mat-card>
</div>
Secure the Components using Angular 8 Route Guard
As we mention in the beginning that the Angular 8 application will use Angular 8 Route Guard to secure the category and post page. So, we have both securities for the Angular 8 component and for Node Express REST API. Type this command to generate a guard configuration file.
ng generate guard auth/auth
Open and edit that file and there are already Angular 8 or Typescript imports of the @angular/router CanActivate, ActivatedRouteSnapshot, and RouterStateSnapshot. So, just these imports of AuthService and Angular Router.
import { AuthService } from '../auth.service';
Inject the `AuthService` and the `Router` to the constructor params.
constructor(private authService: AuthService, private router: Router) {}
Replace a generated function of canActivate with this function.
Add the function for the Route Guard.
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): boolean {
const url: string = state.url;
return this.checkLogin(url);
}
Add the function to check the login status and redirect to the login page if it's not logged in and redirect to the Guarded page if it's logged in.
checkLogin(url: string): boolean {
if (this.authService.loggedInStatus) { return true; }
// Store the attempted URL for redirecting
this.authService.redirectUrl = url;
// Navigate to the login page with extras
this.router.navigate(['/login']);
return false;
}
Next, open and edit `src/app/app-routing.module.ts` then add this import.
import { AuthGuard } from './auth/auth.guard';
Modify the paths that should be secure, so it will look like this.
const routes: Routes = [
{
path: '',
redirectTo: 'home',
pathMatch: 'full'
},
{
path: 'home',
component: HomeComponent,
data: { title: 'Blog Home' }
},
{
path: 'admin',
canActivate: [AuthGuard],
component: AdminComponent,
data: { title: 'Blog Admin' }
},
{
path: 'bycategory/:id',
component: BycategoryComponent,
data: { title: 'Post by Category' }
},
{
path: 'details/:id',
component: DetailsComponent,
data: { title: 'Show Post Details' }
},
{
path: 'login',
component: LoginComponent,
data: { title: 'Login' }
},
{
path: 'register',
component: RegisterComponent,
data: { title: 'Register' }
},
{
path: 'category',
canActivate: [AuthGuard],
component: CategoryComponent,
data: { title: 'Category' }
},
{
path: 'category/details/:id',
canActivate: [AuthGuard],
component: CategoryDetailsComponent,
data: { title: 'Category Details' }
},
{
path: 'category/add',
canActivate: [AuthGuard],
component: CategoryAddComponent,
data: { title: 'Category Add' }
},
{
path: 'category/edit/:id',
canActivate: [AuthGuard],
component: CategoryEditComponent,
data: { title: 'Category Edit' }
},
{
path: 'post',
canActivate: [AuthGuard],
component: PostComponent,
data: { title: 'Post' }
},
{
path: 'post/details/:id',
canActivate: [AuthGuard],
component: PostDetailsComponent,
data: { title: 'Post Details' }
},
{
path: 'post/add',
canActivate: [AuthGuard],
component: PostAddComponent,
data: { title: 'Post Add' }
},
{
path: 'post/edit/:id',
canActivate: [AuthGuard],
component: PostEditComponent,
data: { title: 'Post Edit' }
}
];
Add Angular Material Blog Front Page
The main feature of the Blog website or application is the front page or home page. We will put a navigation toolbar and menu that contain the categories. The menu content depends on the logged-in status. If the user logged in then the menu should contain category and post. Otherwise, the menu just contains home and categories. To achieve that, open and edit `src/app/app.component.ts` then add these imports of category type, home, and auth service.
import { Router } from '@angular/router';
import { Category } from './category/category';
import { HomeService } from './home.service';
import { AuthService } from './auth.service';
Inject that home and auth service to the constructor by adding a constructor if it doesn't exist.
constructor(private api: HomeService, private authService: AuthService, private router: Router) { }
Add these variables that hold the categories array and the status of the logged-in user.
categories: Category[] = [];
loginStatus = false;
Put the logged-in status check inside the NgOnInit function followed by load categories from the REST API.
ngOnInit() {
this.authService.isLoggedIn.subscribe((status: any) => {
if (status === true) {
this.loginStatus = true;
} else {
this.loginStatus = false;
}
});
this.api.getCategories()
.subscribe((res: any) => {
this.categories = res;
console.log(this.categories);
}, err => {
console.log(err);
});
}
Add a function to logout the application.
logout() {
this.authService.logout()
.subscribe((res: any) => {
this.router.navigate(['/']);
}, err => {
console.log(err);
});
}
If there's no ngOnInit in this app.component.ts, add the import for it and add implements to the class name.
import { Component, OnInit } from '@angular/core';
export class AppComponent implements OnInit {
...
}
Next, open and edit `src/app/app.component.html` then replace all of the HTML tags with these.
<mat-toolbar>
<button mat-button [matMenuTriggerFor]="menu">Menu</button>
<mat-menu #menu="matMenu">
<div *ngIf="loginStatus === false">
<button mat-menu-item [routerLink]="['/']">Home</button>
<button mat-menu-item *ngFor="let cat of categories" [routerLink]="['/bycategory/', cat._id]">{{cat.catName}}</button>
</div>
<div *ngIf="loginStatus === true">
<button mat-menu-item [routerLink]="['/admin']">Home</button>
<button mat-menu-item [routerLink]="['/category']">Category</button>
<button mat-menu-item [routerLink]="['/post']">Post</button>
<button mat-menu-item (click)="logout()">Logout</button>
</div>
</mat-menu>
</mat-toolbar>
<router-outlet></router-outlet>
Next, we will show the list of post on the home page. For that, open and edit `src/app/home/home.component.ts` then replace all Typescript codes with these codes that contain a function to load post data.
import { Component, OnInit } from '@angular/core';
import { Post } from '../post/post';
import { HomeService } from '../home.service';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {
posts: Post[] = [];
isLoadingResults = true;
constructor(private api: HomeService) { }
ngOnInit() {
this.api.getPosts()
.subscribe((res: any) => {
this.posts = res;
console.log(this.posts);
this.isLoadingResults = false;
}, err => {
console.log(err);
this.isLoadingResults = false;
});
}
}
Next, open and edit `src/app/home/home.component.html` then replace with these HTML tags that contain the Angular Material component to display the grid of data.
<div class="example-container mat-elevation-z8">
<div class="example-loading-shade"
*ngIf="isLoadingResults">
<mat-spinner *ngIf="isLoadingResults"></mat-spinner>
</div>
<mat-grid-list cols="3">
<mat-grid-tile *ngFor="let post of posts">
<mat-card class="example-card" [routerLink]="['/details/', post._id]">
<mat-card-header>
<div mat-card-avatar class="example-header-image"></div>
<mat-card-title>{{post.postTitle}}</mat-card-title>
<mat-card-subtitle>{{post.updated}}</mat-card-subtitle>
</mat-card-header>
<img mat-card-image src="{{post.postImgUrl}}" alt="Photo of a Shiba Inu">
<mat-card-content>
{{post.postDesc}}
</mat-card-content>
</mat-card>
</mat-grid-tile>
</mat-grid-list>
</div>
Next, to display the post details, open and edit `src/app/details/details.component.ts` then add these lines of Typescript codes that contain Post objects variable and the call of REST API to get the Post data by ID.
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Post } from '../post/post';
import { HomeService } from '../home.service';
@Component({
selector: 'app-details',
templateUrl: './details.component.html',
styleUrls: ['./details.component.scss']
})
export class DetailsComponent implements OnInit {
post: Post = {
category: '',
id: '',
postTitle: '',
postAuthor: '',
postDesc: '',
postContent: '',
postReference: '',
postImgUrl: '',
created: null,
updated: null
};
isLoadingResults = true;
constructor(private route: ActivatedRoute, private api: HomeService, private router: Router) { }
ngOnInit() {
this.getPostDetails(this.route.snapshot.params.id);
}
getPostDetails(id: any) {
this.api.getPost(id)
.subscribe((data: any) => {
this.post = data;
console.log(this.post);
this.isLoadingResults = false;
});
}
}
Next, open and edit `src/app/details/details.component.html` then replace all HTML tags with these Angular Materials Card component that contain the post details.
<div class="example-container mat-elevation-z8">
<div class="example-loading-shade"
*ngIf="isLoadingResults">
<mat-spinner *ngIf="isLoadingResults"></mat-spinner>
</div>
<mat-card class="example-card" [routerLink]="['/details/', post.id]">
<mat-card-header>
<div mat-card-avatar class="example-header-image"></div>
<mat-card-title>{{post.postTitle}}</mat-card-title>
<p>By: {{post.postAuthor}}, {{post.updated | date: 'dd MMM yyyy'}}</p>
<mat-card-subtitle>{{post.postDesc}}</mat-card-subtitle>
</mat-card-header>
<img mat-card-image src="{{post.postImgUrl}}" alt="{{post.postTitle}}">
<mat-card-content [innerHTML]="post.postContent"></mat-card-content>
<mat-card-actions>
<button mat-button>LIKE</button>
<button mat-button>SHARE</button>
</mat-card-actions>
</mat-card>
</div>
Next, we need to add the page that displays the list of the post by category. This page is the action for the categories menu. Open and edit `src/app/bycategory/bycategory.component.ts` then replace all Typescript codes with these codes that contain a function to load post data by category.
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Post } from '../post/post';
import { HomeService } from '../home.service';
@Component({
selector: 'app-bycategory',
templateUrl: './bycategory.component.html',
styleUrls: ['./bycategory.component.scss']
})
export class BycategoryComponent implements OnInit {
posts: Post[] = [];
isLoadingResults = true;
constructor(private route: ActivatedRoute, private api: HomeService) { }
ngOnInit() {
this.route.params.subscribe(params => {
this.getPostsByCategory(this.route.snapshot.params.id);
});
}
getPostsByCategory(id: any) {
this.posts = [];
this.api.getPostsByCategory(id)
.subscribe((res: any) => {
this.posts = res;
console.log(this.posts);
this.isLoadingResults = false;
}, err => {
console.log(err);
this.isLoadingResults = false;
});
}
}
Next, open and edit `src/app/bycategory/bycategory.component.html` then replace all HTML tags with these Angular Material Grid component to display the list of the post by category.
<div class="example-container mat-elevation-z8">
<div class="example-loading-shade"
*ngIf="isLoadingResults">
<mat-spinner *ngIf="isLoadingResults"></mat-spinner>
</div>
<mat-grid-list cols="3">
<mat-grid-tile *ngFor="let post of posts">
<mat-card class="example-card" [routerLink]="['/details/', post._id]">
<mat-card-header>
<div mat-card-avatar class="example-header-image"></div>
<mat-card-title>{{post.postTitle}}</mat-card-title>
<mat-card-subtitle>{{post.updated}}</mat-card-subtitle>
</mat-card-header>
<img mat-card-image src="{{post.postImgUrl}}" alt="Photo of a Shiba Inu">
<mat-card-content>
{{post.postDesc}}
</mat-card-content>
</mat-card>
</mat-grid-tile>
</mat-grid-list>
</div>
Finally, we will adjust the style for all Angular Material component globally by open and edit `src/styles.scss` then add these lines of SCSS codes.
.example-container {
position: relative;
padding: 5px;
}
.example-table-container {
position: relative;
max-height: 400px;
overflow: auto;
}
table {
width: 100%;
}
.example-loading-shade {
position: absolute;
top: 0;
left: 0;
bottom: 56px;
right: 0;
background: rgba(0, 0, 0, 0.15);
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
}
.example-rate-limit-reached {
color: #980000;
max-width: 360px;
text-align: center;
}
/* Column Widths */
.mat-column-number,
.mat-column-state {
max-width: 64px;
}
.mat-column-created {
max-width: 124px;
}
.mat-flat-button {
margin: 5px;
}
.example-container {
position: relative;
padding: 5px;
}
.example-form {
min-width: 150px;
max-width: 500px;
width: 100%;
}
.example-full-width {
width: 100%;
}
.example-full-width:nth-last-child(0) {
margin-bottom: 10px;
}
.button-row {
margin: 10px 0;
}
.mat-flat-button {
margin: 5px;
}
Run and Test the MEAN Stack (Angular 8) Blog CMS
Now, we use another way to run the MEAN Stack (Angular 8). In our previous MEAN stack tutorial, we run the Express.js and Angular in the same server. But now, we run them separately to make development easier and lightweight. For that, we need to open 3 terminal tabs to run MongoDB daemon, Express.js, and Angular 8. Run this command in the first terminal tab.
mongod
In the second terminal tab inside the Blog-CMS folder run this command.
nodemon
In the third terminal tab inside the blog-cms/client folder run this command.
ng serve
And here the full demo of the MEAN Stack (Angular 8) tutorial from our YouTube Channel.
That it's, the MEAN Stack (Angular 8) Tutorial: Build a Simple Blog CMS. You can find the full working source codes from our GitHub.
If you don’t want to waste your time design your own front-end or your budget to spend by hiring a web designer then Angular Templates is the best place to go. So, speed up your front-end web development with premium Angular templates. Choose your template for your front-end project here.
That just the basic. If you need more deep learning about MEAN Stack, Angular, and Node.js, you can take the following cheap course:
- Master en JavaScript: Aprender JS, jQuery, Angular 8, NodeJS
- Angular 8 - Complete Essential Guide
- Learn Angular 8 by creating a simple Full Stack Web App
- Angular 5 Bootcamp FastTrack
- Angular 6 - Soft & Sweet
- Angular 6 with TypeScript
Thanks!