1569292712
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.
The following tools, frameworks, and modules are required for this tutorial achievement:
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 .
<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
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.
#angular #mongodb #node-js #express #web-development
1595334123
I consider myself an active StackOverflow user, despite my activity tends to vary depending on my daily workload. I enjoy answering questions with angular tag and I always try to create some working example to prove correctness of my answers.
To create angular demo I usually use either plunker or stackblitz or even jsfiddle. I like all of them but when I run into some errors I want to have a little bit more usable tool to undestand what’s going on.
Many people who ask questions on stackoverflow don’t want to isolate the problem and prepare minimal reproduction so they usually post all code to their questions on SO. They also tend to be not accurate and make a lot of mistakes in template syntax. To not waste a lot of time investigating where the error comes from I tried to create a tool that will help me to quickly find what causes the problem.
Angular demo runner
Online angular editor for building demo.
ng-run.com
<>
Let me show what I mean…
There are template parser errors that can be easy catched by stackblitz
It gives me some information but I want the error to be highlighted
#mean stack #angular 6 passport authentication #authentication in mean stack #full stack authentication #mean stack example application #mean stack login and registration angular 8 #mean stack login and registration angular 9 #mean stack tutorial #mean stack tutorial 2019 #passport.js
1596094635
What is MEAN Stack Developer?
MEAN Stack Developer is a programmer who operates on the MEAN stack. He works on the backend plus the front end of the application. They are all JavaScript based and therefore a MEAN developer should have excellent JS knowledge, for which you can join MEAN Stack Online Training Program.
Skillets of MEAN Stack developer
• Knowledge of working on the Front-end and Back-end processes
• Work with HTML & CSS
• Understanding of programming templates and architecture design guidelines
• Knowledge of continuous integration, web development, and cloud technologies
• Excellent understanding of DB architecture
• Knowledge of SDLC and experience developing in an Agile environment
• Collaborate with the IT team to build robust systems to support business objectives
• Hands-on experience on Mongo, Angular, Express, Node.
Future of MEAN stack Developer
Being, a Mean stack developer is a highly desirable, challenging vocation. So, if you are ready to work on the diverse skill set and have the experience of working with various code languages and application, then you will become successful MEAN stack developer.
Scope of MEAN stack developer
MEAN Stack Development is the best career prospect today with unlimited growth and scope. The national Indian median salary was around 76K $ pa according to Glassdoor.com. All you need to do is get cracking on your skill set by joining MEAN Stack course in Delhi, earn your certification and through your job experience and continued experiential learning keep pace with newer iterations in technology.
Developers are available in various process streams like Backend, Frontend, Database, Testing, and Mobile Apps. Older practices were that as you gain experience and expertise you would pursue specialization and progress your career in that direction.
How Can I Start Learning Mean Stack Course Step By Step? Modern best practices have changed the trend.
Skill upgrades and building proficiency in:
• CSS
• HTML
• JavaScript
• Ruby, PHP, or Python which are all-purpose languages.
• Postgres, Oracle, or MySQL, relational-database systems.
• Any web-server which includes Nginx or Apache
• FreeBSD, Ubuntu, or CentOS deployment OS.
• Any system for instance GIT for version-control
By mastering one software technology required for every stack-part you will be in a position to create your own software and use it to add business value at your job.
#mean stack #mean stack training #mean stack certification online #mean stack online course #mean stack online training
1594717635
The 21st century customer is not easily impressed. Simply having an online presence is not enough to gain all the benefits that it can bring in. The customers today expect you to keep abreast of the latest trends and if you failed to do so, they are simply going to dismiss you as a brand that is out-dated and not relevant today.
It is not an easy task to create great impression to the customers today. They are aware, mature, and are demanding. If you want to go with market then join best MEAN Stack online training class. Simply establishing an online presence does not get you far with the modern customer, which means you end up missing out on some of the greatest benefits of creating an online identity. An aware customer is looking for businesses that are in tune with the latest trends and if you fall short of this expectation, chances are, you would be dismissed as a brand that is not relevant today. In fact, there are many brands that specialize in the craft of building websites and allow you to create a stronger brand presence with your own free logo while offering you free web hosting as well.
Now, this is precisely why your web developers need to keep on top of the latest trends pertaining to the web development framework. You would give you a greater opportunity to stay in sync with the latest developments and the chances of your turnovers sliding down are greatly minimized.
Now, there are multiple web development frameworks that are available for you to choose from and finding the one that suits you the best can turn into a nightmare if you didn’t even know where to begin.
Right Technology stack for Web Application includes:-
Out of the above mentioned domains, MEAN STACK is considered as the best technology for web application. Online Mean Stack training is used for building dynamic websites and applications.
It uses JavaScript as frameworks for Frond end and Back end development. Mean Stack is used for developing web applications that are fast and easy. Mean stack online training helps for Web Development, process is also called JavaScript Web Development.
Mean Stack is stands as Mongo Db. Express, Angular JS and Node.js. A complete online Mean Stack Training will complete all the major 4 sections discussed below:-
1.MongoDB: It is a system database. It is NOSQL database system. The database is written in C++ language. It helps to manage large sized tables with data. The whole application can be developed by JavaScript.
2.Express.js: Express.js is a Frond End framework. It is used to build web applications in Node. It is flexible and lightweight server application. It is designed for single, multiple and hybrid page web applications.
Angular.js: Angular.js is a Back End Framework. It is a JavaScript framework. The main work is to makes the development and testing easier. In Angular.js, HTML language is used for template.
Node.js: Node.js is a Back End run time environment. It is a server side JavaScript environment. It allows developers to create web servers and build web applications.
Benefits of Online MEAN STACK training:-
There is a simple project cycle followed by MEAN STACK which is successfully covered in mean stack online training. It can be simply understood from the below image.
By using the above image, any project can be easily represented and designed. A series of steps involved which makes your project establishment much easy.
If we talk about its benefits, they attracts very much for web application designs. You can find its benefits below:-
Therefore, if you are looking for right technology Web Application, then one must go for an online MEAN STACK training.
#mean stack online training #mean stack online course #mean stack certification online course #mean stack
1617860969
A MEAN Stack Developer is a professional who has the knowledge and experience in certain areas as it were. In contrast with Full Stack Developer, a Mean Stack Developer uses a blend of MongoDB databases and front-end technology like ExpressJS, AngularJS, and NodeJS to make a full-stack application. This is what is the basic difference between the full stack and mean stack developer and the present blog will deep dive in to further details that will clearly compare full-stack versus mean stack development in the forthcoming segments
Full-stack development is considered as one of the pursued and in demand in the software business as it is inclusive of both front-end and back-end technologies like Python, Ruby, Java, PHP, J2EE, C#, Perl, etc. A full-stack web developer is proficient in varied technologies and is generally looked out by varied associations as their team leads for bigger projects.
In the present-day technological scenario, front-end technologies are becoming very prevalent in terms of product and project development. A project or product is deemed to be successful when the customer interface is positive. Such a circumstance happens when the front-end technology implemented in its best version. In this way a full stack web developer needs to master some of the basic front end technologies namely
and furthermore be aware of outsider libraries, for example,
A full-stack developer uses the back-end technologies to administer and manage the processes carried out by the database. It is used for giving user authentication and to set up the website content. As a full-stack web developer one needs to be proficient in a few back end languages, for example,
A web application isn’t complete without the back-end support.
The “prerequisites for becoming a Full Stack Developer are few and they are listed below”:
Visit: Full Stack Course in Noida
Mean Stack is a technology stack that is a blend of JavaScript-based technologies that are being used for developing web-based applications. MEAN represents MongoDB, ExpressJS, AngularJS, and NodeJS. If you compare mean stack versus python, MEAN Stack is a fully working Full Stack JavaScript whereas Python is only a simple programming language.
Mean Stack can be defined as follows: M represents Mongo DB, which is a NoSQL database system E represents Express JS framework which used for building Node-based web application A represents Angular JS which is a Google-developed JavaScript framework. N represents Node JS which is a JavaScript bases execution environment.
The Front end application of MEAN Stack is created utilizing Angular JS which is an open-source JavaScript framework maintained by Google. In the present day of Jack of all trade that is Full-Stack versus MEAN, Full Stack seems to be versatile yet MEAN Stack is simple and easy to learn. Rakish JS uses HTML as its template language supporting a smarter web-based application.
MEAN Stack web development uses Node JS as its Back end technology including modules called Mongoose which goes about as the Mongo DB object modeling and leads to the website application framework utilizing ExpressJS. Node JS is swift and as compared to other back-end environments.
The “prerequisites for becoming a MEAN Stack Developer are few and they are listed below”:
Visit: MEAN Stack Course in Noida
#full stack #mean stack #mean stack course in noida #full stack course in noida
1591164641
Hire Dedicated MEAN Stack developers to boost your business
Our dedicated MEAN stack development team will cater to all your business needs to the core by adding value to your web application. Hiring expert MEAN stack developers and consultants will increase your business success rate to an unprecedented level. With cutting edge and flexible technology, our MEAN stack developers provide the much needed edge to your web application. Our MEAN Stack solutions will make your website more productive and trusted among your targeted audience.
Advantages of opting for MEAN Stack Development
MEAN Stack development is the process of development using MongoDB, Express.js, AngularJS, and Node.js. The strength of MEAN Stack lies in the way it makes optimum use of the best technology options.
Some of the core advantages of selecting MEAN Stack Development:
Handle large data volumes easily : MEAN Stack development is based on the document-oriented data model, meaning that it allows the developers in storing their data in a document and collection format, instead of a grid. This allows a web application developed with MEAN Stack to handle large data volumes smoothly.
Better Adaptability : Web applications built with MEAN Stack are highly adaptable, because of its ability to amalgamate Javascript and CSS3 seamlessly. Using MEAN Stack, the developers can easily build, test and integrate cloud services in a web application, making it one of the most flexible web application development systems.
No hiccups : MEAN Stack is based on Javascript, which happens to be the most popular programming language in the developer world. This translates into a better availability of developers and a great support community. All these aspects ensure a smooth completion of the MEAN Stack Development project.
Expand your business horizons through MEAN Stack Development Services
MEAN stack development doesn’t restrict the process of website or application development based on its open-source options for easy frontend or backend development. We have experienced and skilled MEAN stack developers to deal with varying requirements that are set-up essentially to add more value and features to your website. Our resourceful methodologies will decrease the time taken in the product development cycle and give you that extra time to release your product and stay a step ahead of your competitors.
Expert minds at using MEAN Stack as a technology
MEAN stack development process comprises of a myriad of technologies. Prismetric is a top MEAN Stack development company with proficiency in MongoDB, Express.js, AngularJS, and Node.js. Apart from website development, we give resolute MEAN stack development services for mobile app development. Hire MEAN Stack developers from Prismetric, who understand your business requirement and synchronize the development process to bring your vision to fruition. MEAN Stack as a technology enables you to handle everyday business challenges in a competent manner.
Read more at: https://www.prismetric.com/mean-stack-development/
#mean stack development services #mean stack development company #mean stack developers