Build A Full Stack Web Application Using Angular and Firebase
Build A Full Stack Web Application Using Angular and Firebase
In this book, we are going to create a blogging application using Angular on the frontend and
Google cloud Firestore as our database. We will also learn how to deploy the application on
Firebase.
● Material design
● Add a new blog post
● Edit an existing blog
● Delete an existing blog
● Authorization with Google account
● Role-based authentication
● Pagination for the blogs
● Post comment on each blog
● Option to share the blog on social channels
We will learn about the following Angular concepts in this book:
By the end of this book, you will have mastered the advanced concepts of the Angular
framework. You will be able to create a rich and interactive web application using Angular
and Google’s Firebase.
About The Author
Acknowledgment
I would like to thank my mother for her continuous support throughout the process of writing
this book.
Finally, I would like to thank Mahesh Chand and the C# Corner team for their support in
reviewing and publishing the book.
Prerequisites for the reader
The readers are expected to have a basic understanding of web development in general. They
are also required to have a basic knowledge of HTML, CSS, and JavaScript. This book will
not teach you about the basics of Angular. A fundamental understanding of the Angular
framework is required before you proceed. Please learn and understand the following basic
concepts of Angular.
● What is Angular?
● How Angular works in general?
● Angular component
● Angular Module
● Angular Services
If you have a basic idea of these concepts, then let’s dive deep and start with our application.
Contents
Preface 1
About The Author 2
Acknowledgment 2
Prerequisites for the reader 3
What is a Single Page Application (SPA)? 7
What is Typescript? 7
What is Angular? 7
Features of Angular 8
Angular lifecycle hook 8
What is Firebase? 9
Why should you use Firebase? 10
What is Angular Material? 11
Setting up the Angular development environment 11
Source Code 14
Prerequisites for the application 14
Create a new Angular app 14
Open Visual Studio Code 14
Configuring firebase 15
Creating a project on firebase 15
Add Firebase configuration to your application 16
Create the Cloud Firestore database 19
Install @angular/fire and firebase 20
Install Angular Material packages 20
Add a material theme 21
Add the module for Angular material 22
What is Bootstrap? 23
Add Bootstrap CSS package 24
Serve the application 24
Add the navigation bar 25
Create the Home Page 26
Add Router module 26
Update the AppComponent 27
Add Forms module 28
Creating the data model 28
Create the blog service 29
Install CKEditor package 30
Add the blog editor 30
Add a route for the addpost page 31
Add CKEditor to BlogEditorComponent 31
Update the BlogEditorComponent template 32
Add a new blog 34
Add buttons in Navbar 35
Add styles to BlogEditorComponent 35
Checkpoint 1 36
Create custom pipes 38
Get the blogs from database 39
Add a BlogCardComponent 39
Add the BlogCardComponent to the home page 43
Checkpoint 2 43
Add Font Awesome library 44
Read a blog post 44
Checkpoint 3 47
Create the Snackbar service 48
Delete a blog post 49
Checkpoint 3 50
Edit an existing blog post 51
Checkpoint 4 54
Pagination on the home page 55
Create the PaginatorComponent 56
Add the PaginatorComponent to the BlogCard 58
Update the BlogCardComponent template 59
Checkpoint 5 60
Add the Google authentication 61
Create the AppUser model 61
Create the Authentication service 62
Update the AppComponent 64
Add Login button in the Navigation bar 65
Update the App module 67
Authenticated access for Edit and Delete 67
Capture the Author name 68
Secure the routes 70
Add route guards in App module 71
Checkpoint 6 71
Update Firestore database security rules 73
Implementing Authorization 74
Configure the Firestore database for admin role 74
Create the admin-auth guard 75
Update the BlogCardComponent 77
Add route guards in App module 77
Checkpoint 7 78
Adding the author profile 79
Checkpoint 8 82
Add the scroller to the blog page 82
Post comment on the blog 85
Create the comment model 85
Create the comment service 85
Create the Comment Component 87
Update the BlogCardComponent 92
Creating an index on the Firestore database 93
Checkpoint 9 94
Adding the share option for the blog 96
Install ngx-sharebuttons 96
Create the social-share component 98
Configure the icon pack 99
Checkpoint 10 100
Deploy the app on Firebase 102
References and Useful Links 105
Personal blog 105
Connect with me 105
What is a Single Page Application
(SPA)?
A Single Page Application is a Web app having only one HTML page. The initial HTML
page is downloaded from the server when we launch the application. Thereafter everything
else is handled in the browser. After the initial page load, there is no HTML sent over the
network by the server. The browser requests only data from the server and the server
responds with the requested data. The data is re-written on the current page without reloading
the page. In an SPA, the web page only refreshes (not reloaded) whenever a new set of data is
requested from the server. This results in smooth user experience.
What is Typescript?
According to the official documents “TypeScript is a typed superset of JavaScript that
compiles to plain JavaScript.” It is an object-oriented, strongly-typed, compiled language. It
was created by Anders Hejlsberg at Microsoft. He is also the creator of the C# language.
Typescript is open source and is supported by all the browsers and operating systems.
What is Angular?
Angular is a JavaScript framework that allows us to create a client-side web application,
using Typescript as the language. Angular is a Single Page Application (SPA) framework. It
allows us to build apps for multiple platforms such as web, mobile web, native mobile, and
native desktop. Angular is an open-source framework that is developed and maintained by
Google.
Features of Angular
Some of the features of Angular are mentioned below:
A fundamental understanding of the Angular framework is required to get the most out of this
book. If you have no prior knowledge of Angular, I recommend you familiarize yourself with
the Angular framework before proceeding further.
The directive has the same set of lifecycle hooks as that of a component. A component or a
directive will not implement all of the lifecycle hooks. A hook method is only invoked if it is
defined.
What is Firebase?
Firebase is a mobile and web application development platform. It was developed by
Firebase, Inc. in 2011, then acquired by Google in 2014. Firebase is considered as Backend-
as-a-Service – BaaS.
1. Build apps fast, without managing infrastructure: Firebase allows us to build our
apps faster by providing us features like analytics, databases, messaging and crash
reporting.
2. Supported by Google: Firebase is built on Google infrastructure and scales
automatically, for even the largest apps.
3. One platform, with products that work better together: Firebase products work
great individually but share data and insights, so they work even better together.
Firebase supports a variety of platforms such as IOS, Android, Web, Unity, and C++.
Firebase is a comprehensive app development platform that provides 18 products. These
products are divided into the following three categories:
A few of the products from each category are mentioned in the table shown below. You can
refer to https://firebase.google.com/products to get the complete details on each product.
Important Note:
● Spark Plan: This is a Free plan. This is suitable for small business and demo apps.
● Blaze Plan: This is a Pay as you go plan. This is suitable for large enterprises.
● Node.js
Node.js will act as our development server. It will allow our Angular application to
run in the local machine.
and run the command shown below to find the node version.
node -v
The version of NPM is 6.13.4. Run the command shown below to find the NPM version.
npm -v
Angular CLI is a command-line interface tool that allows us to develop, scaffold and
initialize an Angular application. Angular CLI provides us the tools and commands out of the
box to facilitate Angular app development.
Open a command window and run the following command to install the Angular CLI.
At the time of writing this book, the version of Angular CLI is 9.0.1. Open a command
window and run the command shown below to find the Angular CLI version.
ng version
Visual Studio Code is a free and open-source IDE (Integrated Development Environment)
developed by Microsoft. It is a lightweight code editor and supports development in
languages such as C++, C#, Java, PHP, Python, Typescript, etc. It is available for Windows,
macOS, and Linux. Visual Studio Code is one of the most widely-used IDEs for Angular
development. Angular is supported by other IDEs also such as WebStorm, Sublime Text,
Atom, etc. However, we will use the Visual Studio Code as our preferred IDE in this book.
Navigate to https://code.visualstudio.com/ and install the latest version of Visual Studio code
cd blogsite
code .
Configuring Firebase
We will create a project on firebase and configure the Google cloud Firestore database for it.
We will use this database for our Angular application. The steps are shown below.
Step 3: Enter your project name. You can give any name of your choice. Here we will use the
name blogsite. Click on Continue.
Step 4: On the next screen, disable the “Enable Google Analytics for this project” button.
Click on “Create project”.
Refer to the image shown below for reference.
firebaseConfig: {
apiKey: "AIzaSyCxqWK4SVAAJkozEkURuteREIW9197z6-s",
authDomain: "blogsite-b165e.firebaseapp.com",
databaseURL: "https://blogsite-b165e.firebaseio.com",
projectId: "blogsite-b165e",
storageBucket: "blogsite-b165e.appspot.com",
messagingSenderId: "1057108181105",
appId: "1:1057108181105:web:ac5bbb18e5f34c7e575bd0"
}
In the “Create database” popup, select “Start in test mode”. Click "Next". On the next screen,
keep the default value for “Clod Firestore location” and click on Done. The Cloud Firestore
database is now configured for your Firebase project. Refer to the image shown below.
Setting the database in test mode is recommended for a sample application. However, this
is not correct from a security point of view. As you can see a warning message is being
displayed: “Anyone with your database reference will be able to read or write to your
database for 30 days”. We will update this rule to allow only the authenticated users to
write to our database in the latter part of this book.
@NgModule({
...
imports: [
// other imports
AngularFireModule.initializeApp(environment.firebaseConfig),
AngularFirestoreModule,
],
...
})
After the successful installation of the Angular material packages, we will import the libraries
into the src/app/app.module.ts file as shown below.
@NgModule({
...
imports: [
...
BrowserAnimationsModule,
],
})
● deeppurple-amber.css
● indigo-pink.css
● pink-bluegrey.css
● purple-green.css
@import "~@angular/material/prebuilt-themes/indigo-pink.css";
ng g m ng-material
Open src/app/ng-material/ng-material.module.ts and replace the existing code with the code
shown below.
@NgModule({
declarations: [],
imports: [
CommonModule,
MatToolbarModule,
MatButtonModule,
MatCardModule,
MatInputModule,
MatIconModule,
MatDividerModule,
MatMenuModule,
MatSelectModule,
MatSnackBarModule,
MatProgressSpinnerModule,
MatTooltipModule,
],
exports: [
CommonModule,
MatToolbarModule,
MatButtonModule,
MatCardModule,
MatInputModule,
MatIconModule,
MatDividerModule,
MatMenuModule,
MatSelectModule,
MatSnackBarModule,
MatProgressSpinnerModule,
MatTooltipModule,
]
})
export class NgMaterialModule { }
We are importing all the required modules for Angular material components which we are
going to use in this application. A separate module for Angular material will make the
application easy to maintain.
@NgModule({
...
imports: [
...
NgMaterialModule,
],
})
What is Bootstrap?
Bootstrap is a CSS framework for building responsive, mobile-first web apps. It is an open-
source framework and is widely used by web developers across the globe. It is considered as
the world’s most popular CSS framework for web development. Bootstrap 4 is the latest
version of Bootstrap. Bootstrap 4 supports all major browsers except IE9.
@import "~bootstrap/dist/css/bootstrap.css";
ng serve -o
The Angular CLI will now serve the application at localhost:4200. The default browser will
open this URL. You can see the window as shown in the image below.
The application will recompile and reload whenever a file changes. We will keep the server
running and proceed with creating our components.
ng g c components/nav-bar
.nav-bar {
background-color: #1565C0;
color: #FFFFFF;
position: fixed;
top: 0;
z-index: 99;
}
button:focus {
outline: none;
border: 0;
}
ng g c components/home
At this point, we will not add any code to HomeComponent. We will revisit it in the latter
part of this book.
@NgModule({
...
imports: [
...
RouterModule.forRoot([
{ path: '', component: HomeComponent, pathMatch: 'full' },
{ path: '**', component: HomeComponent }
]),
],
})
The empty path represents the default path for the application. If the path in the URL is
empty, the app will render the HomeComponent. The ** path is a wildcard. The router will
select this route if the requested URL doesn't match any paths for routes defined earlier in the
configuration.
The order in which we define the routes for the application is very important. The router
uses a first-match wins strategy when matching routes. Therefore, more specific routes
should be placed above less-specific routes. The wildcard route should be placed at the end
because it matches every URL and should be selected only if no other routes are matched
first.
<app-nav-bar></app-nav-bar>
<div class="container">
<router-outlet></router-outlet>
</div>
The <router-outlet> is a placeholder that Angular dynamically fills with the component
based on the current state of the router.
body {
background-color: #fafafa;
}
.container {
padding-top: 60px;
}
@NgModule({
...
imports: [
...
FormsModule,
],
})
constructor() {
this.content = '';
}
}
We have initialized the content property to an empty string in the constructor of the class.
This is to ensure that the content is not shown as “undefined” while binding it to a form-field.
ng g s services/blog
Now we will add the method to create a new post. Put the following method definition in the
blog.service.ts file.
createPost(post: Post) {
const postData = JSON.parse(JSON.stringify(post));
return this.db.collection('blogs').add(postData);
}
This method will accept a parameter of type Post. We will parse the parameter to a JSON
object and add it to the ‘blogs’ collection in our database. If the collection already exists then
the JSON object will get added to it. However, if the collection does not exist in the database,
the add method will create the collection and then add the new object to it.
Execute the command shown below to install the CKEditor editor component for Angular.
Run the command shown below to install one of the official editors builds which is the
classic editor.
@NgModule( {
imports: [
...
CKEditorModule,
],
})
Add the blog editor
We will create a new component for adding and editing the blog. Execute the command
shown below.
ng g c components/blog-editor
RouterModule.forRoot([
...
{ path: 'addpost', component: BlogEditorComponent },
...
])
We will also initialize some properties for this component. Add the following lines of code in
the BlogEditorComponent class.
setEditorConfig() {
this.ckeConfig = {
removePlugins: ['ImageUpload', 'MediaEmbed'],
heading: {
options: [
{ model: 'paragraph', title: 'Paragraph', class: 'ck-
heading_paragraph' },
{ model: 'heading1', view: 'h1', title: 'Heading 1', class: 'ck-
heading_heading1' },
{ model: 'heading2', view: 'h2', title: 'Heading 2', class: 'ck-
heading_heading2' },
{ model: 'heading3', view: 'h3', title: 'Heading 3', class: 'ck-
heading_heading3' },
{ model: 'heading4', view: 'h4', title: 'Heading 4', class: 'ck-
heading_heading4' },
{ model: 'heading5', view: 'h5', title: 'Heading 5', class: 'ck-
heading_heading5' },
{ model: 'heading6', view: 'h6', title: 'Heading 6', class: 'ck-
heading_heading6' },
{ model: 'Formatted', view: 'pre', title: 'Formatted' },
]
}
};
}
This method is used to set the configuration of the classic editor. We will remove the plugins
‘ImageUpload’ and ‘MediaEmbed’ from the editor as we won’t be using these functionalities
in our application. We will set the formatting options for the editor to include headings,
paragraphs, and formatted text.
We will invoke this method inside the ngOnInit method as shown below.
ngOnInit() {
this.setEditorConfig();
}
Update the BlogEditorComponent
template
Open blog-editor.component.html and put the following code into it.
<h1>{{formTitle}} Post</h1>
<hr />
<form #myForm="ngForm" (ngSubmit)="myForm.form.valid && saveBlogPost()"
accept-charset="UTF-8" novalidate>
<input type="text" class="blogHeader" placeholder="Add title..."
class="form-control" name="postTitle"
[(ngModel)]="postData.title" #postTitle="ngModel" required />
<span class="text-danger" *ngIf="myForm.submitted &&
postTitle.errors?.required">
Title is required
</span>
<br />
<div class="form-group">
<ckeditor name="myckeditor" [config]="ckeConfig"
[(ngModel)]="postData.content" #myckeditor="ngModel"
debounce="300" [editor]="Editor"></ckeditor>
</div>
<div class="form-group">
<button type="submit" mat-raised-button
color="primary">Save</button>
<button type=" button" mat-raised-button color="warn"
(click)="cancel()">CANCEL</button>
</div>
</form>
We have defined a template-driven form which will invoke the saveBlogPost method on
successful submission. The form has two fields, one will accept the title of the blog whereas
the other field will accept the content of the blog. The title field is a required field. The
content field contains the CKEditor editor component. The editor property is used to set the
type of editor. Here we are using the classic editor version of CKEditor. We will bind the
config property of the ckeditor component to the ckeConfig variable which we have
configured in the previous section. We will bind the content of the ckeditor component to the
content property of the postData object which is of type Post.
The ckeditor component will return the content in the HTML format, not in plain text
format. We will save each blog post as HTML in the database. This is to ensure that the
formatting of the content is not lost while saving and retrieving the data from the database.
Add the provider for the DatePipe under in the @Component decorator section as shown
below.
@Component({
...
providers: [DatePipe]
}
We will add the definition for the saveBlogPost method as shown below.
saveBlogPost() {
this.postData.createdDate = this.datePipe.transform(Date.now(), 'MM-
dd-yyyy HH:mm');
this.blogService.createPost(this.postData).then(
() => {
this.router.navigate(['/']);
}
);
}
We will add the current date in the postData object as the creation date for the blog post. We
will then call the createPost method from the BlogService to add a new blog post to our
database. This method will be invoked on clicking the Save button.
We will add the following method definition for the cancel method which will be invoked on
click of the Cancel button.
cancel() {
this.router.navigate(['/']);
}
.ck-editor__editable {
max-height: 350px;
min-height: 350px;
}
pre {
display: block;
padding: 9.5px;
margin: 0 0 10px;
font-size: 13px;
line-height: 1.42857143;
color: #333;
word-break: break-all;
word-wrap: break-word;
background-color: #f5f5f5;
border: 1px solid #ccc;
border-radius: 4px;
}
blockquote {
display: block;
padding: 10px 20px;
margin: 0 0 20px;
font-size: 17.5px;
border-left: 5px solid #eee;
}
.spacer {
flex: 1 1 auto;
}
img{
max-width: 100%;
}
Checkpoint 1
Since the server is running, the web page will refresh to reflect the new changes. Open the
browser and click on the “AddPost” button on the nav-bar. You will be navigated to the
addpost page. Add a new blog and click on the save button to save the blog in the database.
Refer to the image shown below.
Once we click on the Save button, the blog post will be added to our cloud Firestore database
and we will be navigated to the home page of the application. To verify if the data has been
added successfully or not, open the Firebase console. Navigate to your project overview page
and click on the “Database” link in the menu on the left. Here you can see the record for your
newly added blog. The “content” field contains the content of the blog in HTML format.
Create custom pipes
We will add two custom pipes in our app
● Excerpt: This will show a summary of the post on the blog card.
● Slug: This will show the URL slug for a post.
ng g p customPipes/excerpt
Open the src/app/customPipes/excerpt.ts file and replace the transform method with the code
shown below.
transform(content: string) {
const postSummary = content.replace(/(<([^>]+)>)/ig, '');
if (postSummary.length > 300) {
return postSummary.substr(0, 300) + ' [...]';
} else {
return postSummary;
}
}
This pipe will accept the contents of the blog as a string and return the first 300 characters as
the summary of the blog. Since the content of the blog is in HTML format, we will remove
all the HTML tags before extracting the summary.
ng g p customPipes/slug
Open the src/app/customPipes/slug.ts file and replace the transform method with the code
shown below.
transform(title: string) {
const urlSlug = title.trim().toLowerCase().replace(/ /g, '-');
return urlSlug;
}
This pipe will accept the title of the blog and return the title as a URL slug. We will replace
the white space character between the words in the title with a ‘-’ character to create the URL
slug.
getAllPosts(): Observable<Post[]> {
const blogs = this.db.collection<Post>('blogs', ref =>
ref.orderBy('createdDate', 'desc'))
.snapshotChanges().pipe(
map(actions => {
return actions.map(
c => ({
postId: c.payload.doc.id,
...c.payload.doc.data()
}));
}));
return blogs;
}
We will fetch the blog in the descending order of the created date to ensure that the latest
blog is always on the top.
Add a BlogCardComponent
Run the command shown below to create the blog card component.
ng g c components/blog-card
Inject the Blog Service in the constructor of BlogCardComponent class as shown below.
ngOnInit() {
this.getBlogPosts();
}
getBlogPosts() {
this.blogService.getAllPosts()
.pipe(takeUntil(this.unsubscribe$))
.subscribe(result => {
this.blogPost = result;
});
}
We will implement the OnDestroy interface on the BlogCardComponent class. Inside the
ngOnDestroy method, we will complete the unsubscribe$ subscription. Refer to the code
snippet shown below.
...
ngOnDestroy() {
this.unsubscribe$.next();
this.unsubscribe$.complete();
}
}
<ng-template #emptyblog>
<div class="spinner-container">
<mat-spinner></mat-spinner>
</div>
</ng-template>
<ng-container *ngIf="blogPost && blogPost.length>0; else emptyblog">
<div *ngFor="let post of blogPost">
<mat-card class="blog-card mat-elevation-z2">
<mat-card-content>
<a class="blog-title" [routerLink]="['/blog/',
post.postId, post.title | slug]">
<h2>{{ post.title}} </h2>
</a>
</mat-card-content>
<mat-card-content>
<div [innerHTML]="post.content | excerpt"></div>
</mat-card-content>
<mat-divider></mat-divider>
<mat-card-actions align="end">
<ng-container>
<button mat-raised-button color="accent"
[routerLink]="['/editpost',post.postId]">Edit</button>
<button mat-raised-button color="warn"
(click)="delete(post.postId)">Delete</button>
</ng-container>
<span class="spacer"></span>
<button mat-raised-button color="primary"
[routerLink]="['/blog/', post.postId, post.title | slug]">Read
More</button>
</mat-card-actions>
</mat-card>
</div>
<mat-divider></mat-divider>
</ng-container>
We will display the blog title and summary in a mat-card. Mat-card is an Angular material
component that can be used to display the content in a card layout. We have used the slug
pipe to create the URL for a blog. We have used the excerpt pipe to get the summary of each
blog. Each blog card will have buttons to Edit and Delete the blog post. At this point, any
user can access the Edit and Delete buttons. In the future, we will add authorization to our
application so that only the Admin used will have access to these functionalities.
At this point, you might get an error in this file. We have added a Delete button and defined a
delete method on the click event of the button. However, we have not added any delete
method yet. Therefore, we will get a compile-time error. To resolve this issue, add the
following code in the src/app/components/blog-card.component.ts file.
delete(postId: string) {
// Method definition to be added later
}
Here, we have defined an empty method. We will add delete logic to this method in the latter
part of the book.
.blog-card {
margin-bottom: 15px;
}
.blog-title {
text-decoration: none;
}
a:hover {
color: indianred;
}
.spinner-container{
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.left-panel {
margin-top: 15px;
}
Checkpoint 2
At this point, we have successfully added the blog card to the home page. Open the browser
and you can see the blog displayed in a card on the home page. This is the same blog which
we have added in checkpoint 1. The card will also have Edit, Delete and Read More buttons.
But at this point, they are non-functional. We will add the logic for these buttons in the latter
part of this book. Refer to the image shown below.
Add Font Awesome library
We will add Font Awesome and Material Icons libraries in our application. We will use icon
sets provided by this library for styling our app. Add the following lines in the index.html
file.
<link href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet">
<link rel="stylesheet" type="text/css"
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-
awesome.min.css" />
ng g c components/blog
Add the router link for this component in app.module.ts as shown below.
We have defined two parameters in this route. The id is the unique id of each blog, whereas
the slug is the URL slug which is created from the title of the blog.
Add the following method definition in the blog.service.ts file. This method will fetch
the blog details based on the id from the ‘blogs’ collections.
ngOnInit() {
this.blogService.getPostbyId(this.postId)
.pipe(takeUntil(this.unsubscribe$))
.subscribe(
(result: Post) => {
this.postData = result;
}
);
}
ngOnDestroy() {
this.unsubscribe$.next();
this.unsubscribe$.complete();
}
}
In the constructor of the class, we will get the id from the URL. In the ngOnInit, we will
call the getPostbyID method of the BlogService and pass the postId as the parameter
to fetch the details of a particular blog. Inside the ngOnDestroy method, we will complete
the unsubscribe$ subscription.
<div class="docs-example-viewer-wrapper">
<h1 class="entry-title">{{postData.title}}</h1>
<mat-card-subtitle class="blog-info">
<i class="fa fa-calendar" aria-hidden="true"></i>
{{postData.createdDate | date:'longDate'}}
</mat-card-subtitle>
<mat-divider></mat-divider>
<div class="docs-example-viewer-body">
<div [innerHTML]="postData.content">
</div>
</div>
</div>
This component is used to display the full blog post. We will show the blog created date and
also display a calendar icon using font-awesome. Since the content of the blog is stored and
fetched as HTML, we will bind the innerHTML property of <div> element to the content of
the blog. This will ensure that the blog is displayed as plain text on the screen.
.docs-example-viewer-body {
padding: 20px;
align-content: center;
align-items: center;
font-size: 14px;
}
.docs-example-viewer-wrapper {
border: 1px solid rgba(0, 0, 0, .03);
box-shadow: 0 1px 1px rgba(0, 0, 0, .24), 0 0 2px rgba(0, 0, 0, .12);
border-radius: 4px;
margin: 1em auto;
background-color: #FFFFFF;
}
.entry-title {
margin: 20px;
}
.blog-info {
margin: 15px 20px 10px;
align-content: center;
align-items: center;
.fa-user {
margin-left: 10px;
}
}
Checkpoint 3
Open the browser and click on the “Read More” button on the blog card. You will be
navigated to a new page that will display the full blog. The blog creation date is displayed
just below the title of the blog. Also, notice the URL for this page. The URL contains the id
and the title of the blog as a slug. Refer to the screenshot below.
Create the Snackbar service
We will create a Snackbar service which will help us to show the user-friendly messages on
the screen as snack-bar notifications. Create the service using the command shown below.
ng g s services/Snackbar
@Injectable({
providedIn: 'root'
})
export class SnackbarService {
showSnackBar(message: string) {
this.snackBar.open(message, 'Close', {
duration: 2000,
panelClass: 'snackbar-ribon',
verticalPosition: 'top',
horizontalPosition: 'center'
});
}
}
We have created the showSnackBar method which will accept the message to display as a
parameter. The display duration is set to 2000 milliseconds. We have defined the position in
such a way that the snack-bar will be displayed in the top-center of the page.
.snackbar-ribon {
color: #FFFFFF;
background: #17a2b8;
}
Delete a blog post
We will add the feature of deleting a blog. We will delete the blog from the ‘blogs’ collection
based on the postId. Add the following lines of code in the
src/app/services/blog.service.ts file.
deletePost(postId: string) {
return this.db.doc('blogs/' + postId).delete();
}
constructor(
// other service injection
private snackBarService: SnackbarService
) { }
We will also update the delete method in the blog-card.component.ts file. The method
definition is shown below.
delete(postId: string) {
if (confirm('Are you sure')) {
this.blogService.deletePost(postId).then(
() => {
this.snackBarService.showSnackBar('Blog post deleted
successfully');
}
);
}
}
The delete method will accept the postId as a parameter. It will display an alert box asking
for the confirmation to delete. If the user clicks on “OK”, then we will invoke the
deletePost method of the BlogService. Upon the successful deletion of the blog, we
will show the success message using a snack-bar.
Checkpoint 3
Open the browser and click on the “Delete” button on the blog card. A JavaScript
confirmation box will open asking for the confirmation to delete the blog. Refer to the image
shown below.
If you click on the OK button, the blog will be deleted and you will get a confirmation
message in a snackbar. Refer to the image shown below.
Edit an existing blog post
We will now implement the functionality to edit an existing blog. Add the following code
definition in blog.service.ts.
The updatePost method will accept the postId and an object of type Post as the parameter. We
will parse the Post object to a JSON object and then update the object in the ‘blogs’
collection.
RouterModule.forRoot([
...
{ path: 'editpost/:id', component: BlogEditorComponent },
...
])
Declare a new Subject variable. Refer to the code snippet shown below.
Open blog-editor.component.ts and add the following code in the constructor. Here,
we are fetching the id of the blog from the URL with the help of the ActivatedRoute class.
if (this.route.snapshot.params['id']) {
this.postId = this.route.snapshot.paramMap.get('id');
}
We will add the method to set the edit form when we click on the “Edit” button on the blog
card on the home page. The method definition is shown below.
setPostFormData(postFormData) {
this.postData.title = postFormData.title;
this.postData.content = postFormData.content;
}
Update the ngOnInit method inside the BlogEditorComponent class as shown below.
ngOnInit() {
this.setEditorConfig();
if (this.postId) {
this.formTitle = 'Edit';
this.blogService.getPostbyId(this.postId)
.pipe(takeUntil(this.unsubscribe$))
.subscribe(
result => {
this.setPostFormData(result);
}
);
}
}
If the postId is set then it means that this is an Edit request. We will set the title of the form to
“Edit”. We will call the getPostbyId method from BlogService to fetch the details of
the blog corresponding to the postId.
Upon clicking Save we need to handle the case of both adding a new blog as well as editing
an existing blog. Hence we will update the saveBlogPost as shown below.
saveBlogPost() {
if (this.postId) {
this.blogService.updatePost(this.postId, this.postData).then(
() => {
this.router.navigate(['/']);
}
);
} else {
this.postData.createdDate = this.datePipe.transform(Date.now(), 'MM-
dd-yyyy HH:mm');
this.blogService.createPost(this.postData).then(
() => {
this.router.navigate(['/']);
}
);
}
}
ngOnDestroy() {
this.unsubscribe$.next();
this.unsubscribe$.complete();
}
Checkpoint 4
Open the browser and click on the “Edit” button in the blog card on the Home page. You will
be navigated to the “Edit Post” page. You can see the blog editor with the content of the post
already populated inside it. The URL of the page will contain the postId for the particular
blog. Refer to the image shown below.
You can edit the content of the blog inside the blog editor. Click on save to update the blog
content and return to the home page. You can see the updated content in the blog excerpt.
Refer to the image shown below.
Pagination on the home page
We will add the feature of pagination on the home page. We will use the ngx-pagination for
this purpose. It is an open-source component which provides a simple and easy to use
pagination feature for Angular apps.
Execute the command shown below to install the ngx-pagination component for Angular.
@NgModule( {
imports: [
...
NgxPaginationModule,
],
})
Create the PaginatorComponent
Run the following command, in the original terminal, to generate a Paginator component.
ng g c components/paginator
We will add two Input properties for this component as shown below.
@Input()
pageSizeOptions: [];
@Input()
config: any;
We will add the method to handle the pageChange event for our paginator. The definition of
this method is shown below.
pageChange(newPage: number) {
this.router.navigate(['/page/', newPage]);
}
We will add the changePageItemCount method in the PaginatorComponent class. This
method is used to set up a dynamic page size for the paginator. It will set the number of items
to show on each page based on a selection from the drop-down list.
changePageItemCount(selectedItem) {
localStorage.setItem('pageSize', selectedItem.value);
this.config.itemsPerPage = selectedItem.value;
}
We are storing the value selected by the user in the local storage. This is to ensure that the
value won’t be lost on page refresh and the application has a smooth user experience.
<div class="paginator-controls">
<div>
<pagination-controls (pageChange)="pageChange($event)" class="my-
pagination"></pagination-controls>
</div>
<div>
<mat-form-field>
<mat-label>Items per page: </mat-label>
<mat-select [(ngModel)]="config.itemsPerPage"
(selectionChange)="changePageItemCount($event)">
<mat-option *ngFor="let page of pageSizeOptions" [value]="page">
{{ page }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
Now we will add a new router link in app.module.ts to support the pagination as shown
below.
config: any;
pageSizeOptions = [];
Add the following codes snippet inside the constructor of BlogCardComponent class to
initialize the newly declared properties.
ngOnInit() {
this.route.params.subscribe(
params => {
this.config.currentPage = +params['pagenum'];
this.getBlogPosts();
}
);
}
<app-paginator [pageSizeOptions]="pageSizeOptions"
[config]="config"></app-paginator>
We will also add a paginate pipe in the ngFor directive while iterating through the list of blog
posts as shown below.
Checkpoint 5
Open the browser and you can see a paginator on the home page. You can jump through the
pages and the URL will change with the updated page number. You can also see a drop-down
list besides the paginator which will allow you to select the number of items to be shown on
each page. Refer to the image shown below.
Add the Google authentication
We will add the feature of “login with Google account” into our application. We need to
enable the authentication feature from the Firebase console. Follow the steps mentioned
below to enable Google authentication on Firebase.
Now we will configure our application to use the Google authentication provided by
Firebase.
Create the AppUser model
Create a new file src/app/models/appuser.ts and put the following code.
ng g s services/auth
Open the src/app/services/auth.service.ts file and add the following import definitions.
appUser$: Observable<AppUser>;
constructor(
public afAuth: AngularFireAuth,
private route: ActivatedRoute,
private router: Router,
private db: AngularFirestore
)
The observable appUser$ will get the auth state of the user. If the user is logged in, it will
fetch the user details from the Firestore database, else it will return null. Put the following
code in the constructor of the AuthService class.
this.appUser$ = this.afAuth.authState.pipe(
switchMap(user => {
if (user) {
return this.db.doc<AppUser>(`appusers/${user.uid}`).valueChanges();
} else {
return of(null);
}
})
);
We will also add a method updateUserData to save the user data into our database upon
successful login. We will store the name, email address and photo URL of the photo in
Google account for each user in our database. The method definition is shown below.
private updateUserData(user) {
const userRef = this.db.doc(`appusers/${user.uid}`);
const data = {
name: user.displayName,
email: user.email,
photoURL: user.photoURL
};
return userRef.set(data, { merge: true });
}
Add the method definition for login and logout in the AuthService class as shown below.
async login() {
const returnUrl = this.route.snapshot.queryParamMap.get('returnUrl')
|| this.router.url;
localStorage.setItem('returnUrl', returnUrl);
async logout() {
await this.afAuth.auth.signOut().then(() => {
this.router.navigate(['/']);
});
}
We will get the returnUrl from the route and store its value in the local storage. We are
storing the value in the local storage to ensure that the value won’t be lost during page
redirect or page refresh.
constructor(
private authService: AuthService,
private router: Router
) { }
We will subscribe to the observable appUser$ inside the ngOnInit method of the
AppComponent class as shown below.
this.authService.appUser$.subscribe(user => {
if (!user) {
return;
} else {
const returnUrl = localStorage.getItem('returnUrl');
if (!returnUrl) {
return;
}
localStorage.removeItem('returnUrl');
this.router.navigateByUrl(returnUrl);
}
});
If the user is logged in, we will fetch the value of the return URL from the local storage. If
the return URL is available, the user will be navigated to it. We will then remove the return
URL from the local storage.
We will declare a property to hold the user data. We will also Inject the AuthService in the
constructor. Refer to the code snippet shown below.
appUser: AppUser;
We will also add the method to handle login and logout from our application inside the
NavBarComponent class as shown below.
login() {
this.authService.login();
}
logout() {
this.authService.logout();
}
We will update the template for the navigation bar. Open the nav-bar.component.html
file and replace the existing code with the code shown below.
<ng-template #anonymousUser>
<button mat-button (click)="login()">Login with Google</button>
</ng-template>
<ng-container *ngIf="appUser; else anonymousUser">
<img mat-card-avatar class="user-avatar" src={{appUser.photoURL}}>
<button mat-button [matMenuTriggerFor]="menu">
{{appUser.name}}<mat-icon>arrow_drop_down</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item (click)=logout()>Logout</button>
</mat-menu>
</ng-container>
</mat-toolbar>
If the user is logged in, we will display the name and the photo as per the user’s Google
account. If the user is not logged in, we will display a “Login with Google” button on the
nav-bar.
.user-avatar {
height: 40px;
width: 40px;
border-radius: 50%;
flex-shrink: 0;
}
@NgModule({
...
imports: [
...
AngularFireAuthModule,
],
})
Authenticated access for Edit and Delete
Open src/app/components/blog-card.component.ts and add the following two
import definitions at the top.
appUser: AppUser;
constructor(
// other services
private authService: AuthService) {}
We will subscribe to the observable appUser$ from AuthService and set the appUser
property. Add the following line of code in the ngOnInit method of the
BlogCardComponent class.
<ng-container *ngIf="appUser">
At this point in time, we are allowing the Edit and Delete feature for any logged-in user. In
the next section, we will implement the feature of authorization. We will then update this
code to allow these features for admin users only.
We will declare a property to hold the user data. We will Inject the AuthService in the
constructor of BlogEditorComponent. Inside the ngOnInit method, we will subscribe to
the observable appUser$ from AuthService and set the appUser property. Refer to the
code snippet shown below.
appUser: AppUser;
constructor(
// other service injection
private authService: AuthService) { }
ngOnInit() {
...
this.authService.appUser$.subscribe(appUser => this.appUser =
appUser);
}
We will set the author's property of the postData object while saving a new blog. Add the
following line of code inside the else section of the saveBlogPost method, just before
invoking the createPost method of BlogService.
this.postData.author = this.appUser.name;
Open the src/app/guards/auth.guard.ts file and add the following import definitions.
Add a constructor in the AuthGuard class. Inject Router and AuthService in the
constructor as shown in the below code snippet.
constructor(
private router: Router,
private authService: AuthService
) { }
We will update the canActivate method to handle the unauthenticated access to the routes.
Add the following code in this method.
We will subscribe to the observable appUser$ to fetch the authentication state of the user. If
the user is logged in, we will return true. If the user is not logged in, the following three
things will happen: -
● Set the query parameter called returnUrl to the value of the current URL.
● Navigate the user to the home page.
● Return false from the method.
Checkpoint 6
Open the browser and you can observe that the blog card doesn’t have the Edit and Delete
button. Also, the nav-bar does not show the “Add post” button. This is because the user has
not logged in. You can also see a “Login with Google” button on the top right corner of the
nav-bar. Click on this button, a pop-up modal will open asking you to login with your Google
account. Refer to the image shown below for the reference.
The page will refresh once Google authentication is successful. You can see that the route for
the “Add post” appears on the nav-bar. The top right corner of the nav-bar also displays the
user name of the logged-in user. The blog card will show the Edit and Delete button now.
Refer to the image shown below.
Update Firestore database security
rules
While creating the database, we created it in the “test” mode. We will now update the
database security rule to allow the write operation only for the authenticated user.
Select “Database” under the “Develop” section from the menu on the left. Click on the
“Rules” tab. Update the rules as shown below.
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read;
allow write: if request.auth.uid != null;
}
}
}
Click on the “Publish” button to publish the rule. Refer to the image shown below.
Implementing Authorization
We will implement role-based authorization for our website. We will define a role – admin
and only the users with the admin role have the permissions to edit and delete a blog. In a
future section, we will add the feature of posting comments on the blog. The admin user will
have permission to delete the comments also.
Configure the Firestore database for admin
role
We have defined a Boolean property isAdmin in the AppUser class. This property is used to
define the admin role for a user. We will manually add a new field called isAdmin for the
users we want to provide admin access. We will make the changes in the appusers
collection inside the Firestore database.
Navigate to the Firestore database and open the appusers collection. This collection stores
the record of all the users who have logged in to our application. Refer to the image shown
below.
Click on the “Add field” button. A popup box will open asking you to define a new field. Set
the field name as isAdmin, Type as boolean and Value as true. Click on the Add button to
add the new field. Refer to the image shown below.
The new field will be added to the collection. Refer to the image shown below.
Upon successful login, we will fetch the user details from the appusers collection and bind
it to the object of type AppUser class. We will then use the isAdmin property to restrict the
access for the user.
Since we are creating a demo application having only one role, therefore we are setting the
isAdmin property manually. This is not recommended for an enterprise-level application
where we might have to manage multiple roles. Ideally, we should create a separate
application to manage roles for all the users.
constructor(
private router: Router,
private authService: AuthService) { }
We will update the canActivate method to handle unauthorized access to the routes.
Update the canActivate method by adding the following code.
We will subscribe to the observable appUser$ to fetch the authentication state of the user. If
the user is logged in and the value of the isAdmin property for the user is set to true, we will
return true. Similar to the behavior of the AuthGuard class, if the user is not logged in, the
following three things will happen: -
● Set the query parameter called returnUrl to the value of the current URL.
● Navigate the user to the home page.
● Return false from the method.
<ng-container *ngIf="appUser?.isAdmin">
Here we are restricting the ability to edit the post to the admin users only. When the user tries
to access the “editpost” route, the AdminAuthGuard class will be invoked. If the
AdminAuthGuard class returns true, the user will be allowed to access to the “editpost” route,
otherwise the access to this route will be denied for the user.
Checkpoint 7
Open the browser and log in to the application with a non-admin user. After login, you can
see the “AddPost” link on the nav-bar, but the Edit and Delete buttons won’t be visible on the
blog card. This means the user can add a new blog, but he does not have the permission to
edit or delete any existing blog. Refer to the image shown below.
Navigate to the Firestore database and add a new field isAdmin for the user as we have
discussed earlier. Alternatively, you can also log out and log in again with another user for
which the isAdmin field is set to true. This time you can see the Edit and Delete buttons on
the blog card. Refer to the image shown below.
Adding the author profile
We will show the author’s profile on the home page. Execute the command shown below to
create the author-profile component.
ng g c components/author-profile
We are going to show an image of the author along with his social media links. The image
will be served from within the app itself. We will place the author’s image inside the
src\assets folder.
Open src/app/components/author-profile/author-profile.component.html
and put the following code inside it.
Open src/app/components/author-profile/author-profile.component.scss
and put the following style definitions in it.
.fa-twitter-square {
color: #55acee;
}
.fa-facebook-square {
color: #3b5998;
}
.fa-linkedin-square {
color: #0976b4;
}
.fa-github-square {
color: #333;
}
.fa {
font-size: 3em;
width: 1em;
margin-top: 5px;
cursor: pointer;
}
.mat-card-avatar {
width: 100px;
height: 100px;
margin: auto
padding: 5px;
}
.authorimagecontainer {
text-align: center;
}
.rightdivtext {
color: #636467;
text-transform: uppercase;
padding: 2px;
}
.rightpanel-card {
margin-bottom: 15px;
}
To show the author profile on the home page, we need to add the
AuthorProfileComponent to the HomeComponent.
Checkpoint 8
Open the browser and you can observe an author profile on the right-hand side of the home
page. The profile will show the image of the author along with his social media links. Refer
to the image shown below.
@Component({
selector: 'app-scroller',
templateUrl: './scroller.component.html',
styleUrls: ['./scroller.component.scss']
})
export class ScrollerComponent {
showScroller: boolean;
showScrollerPosition = 100;
@HostListener('window:scroll')
checkScroll() {
const scrollPosition = window.pageYOffset ||
document.documentElement.scrollTop || document.body.scrollTop || 0;
gotoTop() {
window.scroll({
top: 0,
left: 0,
behavior: 'smooth'
});
}
}
We will set the value of the variable showScrollerPosition to 100. This is the value in
pixels after which the scroller will be visible. The checkScroll method will listen for the
scroll event using the @HostListener decorator. We will calculate the current scroll
position of the page. If the current scroll position is greater than the
showScrollerPosition, we will display the scroll to top button on the page.
The gotoTop method will set the behavior of the scroller. This method will scroll the page to
the top with a smooth transition.
.scroll-to-top {
display: block;
background: rgba(100, 100, 100, 0.4);
color: #ffffff;
bottom: 4%;
cursor: pointer;
position: fixed;
right: 20px;
z-index: 999;
font-size: 24px;
text-align: center;
width: 45px;
height: 45px;
border-radius: 50%;
.fa {
font-weight: 900;
}
}
.scroll-to-top:hover {
background-color: #b2b2b2;
}
Now we will add the ScrollerComponent to the BlogComponent. Open
src/app/components/blog/blog.component.html and add the following line at the
end of the file.
<app-scroller></app-scroller>
We have successfully created a scroller for the blog page. We will see the output demo for
the scroller along with the comment feature in the next section.
ng g s services/Comment
Open src/app/services/comment.service.ts and add the following import
statements at the top.
Now add the following method definitions inside the CommentService class.
saveComment(comment: Comments) {
const commentData = JSON.parse(JSON.stringify(comment));
return this.db.collection('comments').add(commentData);
}
deleteAllCommentForBlog(blogId: string) {
const commentsToDelete = this.db.collection('comments', ref =>
ref.where('blogId', '==', blogId)).snapshotChanges();
commentsToDelete.forEach(
commentList => {
commentList.forEach(comment => {
this.db.doc('comments/' + comment.payload.doc.id).delete();
});
}
);
}
deleteSingleComment(commentId: string) {
return this.db.doc('comments/' + commentId).delete();
}
}
The saveComment method will accept an object of type Comments as a parameter. We will
parse the parameter to a JSON object and add it to the ‘comments’ collection in our database.
If the collection already exists then the JSON object will get added to it. However, if the
collection does not exist in the database, the add method will create the collection and then
add the new object to it.
The getAllCommentsForBlog method will accept the blog Id as a parameter. This method
will query the ‘comments’ collection and return the list of all the comments corresponding to
the blog Id passed to it. The list of comments is sorted by commentDate in a descending
fashion. This is to ensure that the latest comment stays on the top of the list.
ng g c components/Comments
Add the provider for the DatePipe under in the @Component decorator section as shown
below.
@Component({
...
providers: [DatePipe]
}
@Input()
blogId;
appUser: AppUser;
public comments = new Comments();
commentList: Comments[] = [];
private unsubscribe$ = new Subject<void>();
This component will accept blogId as an input. We will inject the services in the constructor
of the class.
Now add the following method definitions inside the CommentsComponent class.
ngOnInit() {
this.authService.appUser$.subscribe(appUser => this.appUser =
appUser);
this.getAllComments();
}
onCommentPost(commentForm) {
this.comments.commentDate = this.datePipe.transform(Date.now(), 'MM-
dd-yyyy HH:mm:ss');
this.comments.blogId = this.blogId;
this.commentService.saveComment(this.comments).then(
commentForm.resetForm()
);
}
getAllComments() {
this.commentService.getAllCommentsForBlog(this.blogId)
.pipe(takeUntil(this.unsubscribe$))
.subscribe(result => {
this.commentList = result;
});
}
deleteComment(commentId) {
if (confirm('Do you want to delete this comment!!!')) {
this.commentService.deleteSingleComment(commentId).then(
() => {
this.snackBarService.showSnackBar('Comment Deleted
successfully');
}
);
}
}
login() {
this.authService.login();
}
ngOnDestroy() {
this.unsubscribe$.next();
this.unsubscribe$.complete();
}
Inside the onCommentPost method, we will set the commentDate to the current date. We
will also set the blogId property of the comment object to the id of the blog for which the
comment is posted. We will invoke the saveComment method of the CommentService to
store the comment in the database.
The deleteComment method will allow us to delete a particular comment. It will display a
confirmation box. If the user confirms the delete action, we will invoke the
deleteSingleComment method from the CommentService to delete a comment.
The login method will allow the user to login to the application using the Google account.
<ng-template #anonymousUser>
<mat-card class="comment-card mat-elevation-z2">
<a (click)="login()">Login with Google</a> to post comments
</mat-card>
</ng-template>
<mat-card *ngIf="appUser; else anonymousUser" class="comment-card mat-
elevation-z2">
<mat-card-title>
LEAVE A REPLY
</mat-card-title>
<mat-card-subtitle>
Your email address will not be published. Required fields are
marked *
</mat-card-subtitle>
<mat-card-content>
<form #commentForm="ngForm" (ngSubmit)="commentForm.form.valid
&& onCommentPost(commentForm)" novalidate>
<mat-form-field class="full-width">
<input matInput placeholder="Name" name="commentedBy"
[(ngModel)]="comments.commentedBy"
#commentedBy="ngModel" required>
<mat-error *ngIf="commentForm.submitted &&
commentedBy.errors?.required">Name is required</mat-error>
</mat-form-field>
<mat-form-field class="full-width">
<input matInput placeholder="Email" name="email"
[(ngModel)]="comments.email" #email="ngModel" email
required>
<mat-error *ngIf="commentForm.submitted &&
email.errors?.required">Email is required</mat-error>
<mat-error *ngIf="commentForm.submitted &&
email.errors?.email">Invalid email</mat-error>
</mat-form-field>
<mat-form-field class="full-width">
<textarea matInput placeholder="Comment" name="content"
[(ngModel)]="comments.content"
#content="ngModel" required></textarea>
<mat-error *ngIf="commentForm.submitted &&
content.errors?.required">Comment is required</mat-error>
</mat-form-field>
<mat-card-actions>
<button type="
submit" mat-raised-button color="primary">Post
Comment</button>
</mat-card-actions>
</form>
</mat-card-content>
</mat-card>
<mat-card *ngFor="let comment of commentList" class="comment-card mat-
elevation-z2">
<mat-card-title>
<div class="comment-card-title">
<div>
{{comment.commentedBy}}
</div>
<div *ngIf="appUser?.isAdmin">
<button mat-icon-button matTooltip="Delete comment"
matTooltipPosition="before" color="accent"
(click)="deleteComment(comment.commentId)">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
</mat-card-title>
<mat-card-subtitle>{{comment.commentDate | date:'medium'}}</mat-
card-subtitle>
<mat-card-content>
<p>{{comment.content}}</p>
</mat-card-content>
</mat-card>
The feature to post a comment on the blog is available to logged-in users only. If the user is
not logged in, we will display a “Login with Google” link asking the user to login with the
Google account to post comments.
We are using a template-driven form to capture the user comments. This form will invoke the
onCommentPost method on successful submission. The form will have the following three
fields
● Name – This is a required field and used to capture the name of the person posting the
comment.
● Email – This is a required field and used to capture the name of the person posting the
comment.
● Comment – This is a required field and used to capture the comment text.
We will also display the comments posted on the blog in a card layout using a <mat-card>
element. We will display the name of the person who posted the comment, the date of the
comment and the comment text. The list of comments will be displayed just below the
comment form. If the user is an admin user, we will display a delete icon on each comment
which will allow the admin to delete a comment.
a:not([href]):not([tabindex]) {
text-decoration: underline;
cursor: pointer;
color: #1565C0;
}
.comment-card-title{
display: flex;
justify-content: space-between;
}
.comment-card {
margin: 10px 0 15px 0;
}
.full-width {
width: 100%;
}
<mat-divider></mat-divider>
<app-comments [blogId]="postId"></app-comments>
We will update the delete method inside the BlogCardComponent class. We will call the
deleteAllCommentForBlog method defined in the CommentService. Add the following
line of code inside the then block of delete method just before invoking the
showSnackBar method. This will ensure that while deleting a blog, all the comments
associated with that blog will also be deleted.
this.commentService.deleteAllCommentForBlog(postId);
Creating an index on the Firestore database
If you open the browser at this point and navigate to the blog details page, you will get an
error in the browser console. The error says “ERROR FirebaseError: The query requires an
index”. Refer to the image below for the reference.
Since we are using a where clause with the equality operator inside the
getAllCommentsForBlog method, we need to create an index on our database. This index
is required for our query to work. You can observe that the error message is also providing a
URL to create the index. Click on the URL and you will be navigated to the firebase console.
You can see the screen as shown below.
Here you can see a “Create a composite index” popup box on the screen. The required
configuration for the index is already set up. Click on the “Create index” button to create the
index. The index will take a few minutes to build. Once the index is created successfully, you
can see the status of the index as “Enabled”. Refer to the image shown below.
Checkpoint 9
Open the browser and logout from the application if you are already logged in. Navigate to
any blog page. Scroll to the bottom of the page. You can see the “scroll to top” button on the
right side of the page. If you click on this button, the page will scroll up to the top with a
smooth transition effect. Refer to the image shown below.
You can also see a message which will ask you to log in with Google to start posting
comments. Log in with your Google account. You will see a form for posting the comments.
Fill out the details and click on the “Post Comment” button. The comment will be posted and
displayed in a card just below the form. Refer to the image shown below.
The comment card will display the name of the person who posted the comment along with
the date and time of posting the comment. If you are logged in as an admin user, then you can
also see the “Delete comment” button on the top-right corner of the comment card.
We will also install the icon packs using the command shown below.
Import share buttons theme in the global style in the app/src/style.scss file.
@import '~@ngx-share/button/themes/circles/circles-dark-theme';
@NgModule({
...
imports: [
...
HttpClientModule,
FontAwesomeModule,
ShareModule.withConfig(customConfig),
],
})
In the custom config, we will set the twitterAccount name of the blog Author. This will
ensure that, whenever we share the blog on Twitter, this account will be tagged in the tweet.
ng g c components\social-share
<app-social-share></app-social-share>
Create a new file inside called icons.ts inside the src folder. Open src/icons.ts file
and put the following code inside it.
Here we are importing all the icons which we are going to use in our application from the
fortawesome library. We will then export the icon pack to make it available to be used in
the component.
button{
margin: 5px;
}
Checkpoint 10
Open the browser. Navigate to any blog page and scroll to the bottom. You will see a list of
share buttons displayed there. You will get all the share options that we have configured.
Refer to the image shown below.
If you click on any of the share buttons, the corresponding application page will open asking
you to log in. Upon successful login, you will get the share link of the blog. Refer to the
image shown below to see the Twitter share option in action. You can observe that it is also
tagging the author’s twitter handle as “via @ankitsharma_007”. If you click on the “Tweet”
button, this tweet will be shared on your Twitter timeline. The tagging the author feature is
available only for Twitter.
Deploy the app on Firebase
The final step is to deploy the app on Firebase. We will follow the steps mentioned below.
Step 1: Install firebase CLI tools via npm. Run the command as shown below.
Step 2: Run the following command to build the app in the production configuration.
ng build --prod
The ‘prod’ option will set the build configuration to the production target. The production
target is set up in the workspace configuration such that all builds make use of bundling,
limited tree-shaking, and also limited dead code elimination.
Step 3: Open a command prompt window inside the /blogsite/dist folder. And run the
following command to login into the firebase.
firebase login
It will open a browser window and ask you to log in to Firebase. Login using your Google
account. Upon successful login navigate back to your CLI.
firebase init
This command will initialize a firebase project. You will be asked a set of questions. Answer
them as shown below: -
Step 4: Deploy on Firebase. Run the following command to deploy your application on
Firebase.
firebase deploy
This command will deploy your angular application on Firebase and upon success, it will
give you a hosting URL. Navigate to the hosting URL to see your deployed app in action.
Refer to the image shown below for reference.
You can also find the hosting URL on the firebase dashboard. Navigate to the "Project
Overview" page of your Firebase project. Select “Hosting” under the “Develop” menu from
the list on the left. You can see the domain names for your web app in the panel on the right.
This completes our application. We learned how to create a simple blogging application using
Angular on the frontend and cloud Firestore as a database.
References and Useful Links
● https://firebase.google.com/
● https://material.angular.io/
● https://cli.angular.io/
● https://ckeditor.com/
● https://fontawesome.com/
● https://getbootstrap.com/
● https://www.typescriptlang.org/docs/home.html
● https://angular.io/start
● https://blog.angular-university.io/tag/angular-for-beginners/
● https://www.c-sharpcorner.com/technologies/angularjs
● https://www.npmjs.com/package/ngx-pagination
● https://www.npmjs.com/package/@ngx-share/button
● https://github.com/AnkitSharma-007/blogging-app-with-Angular-CloudFirestore
● https://blogsite-30c69.firebaseapp.com/
Personal blog
You can read articles on Angular on my blog at https://ankitsharmablogs.com/
Connect with me
You can connect with me via social channels and GitHub
● LinkedIn – https://www.linkedin.com/in/ankitsharma-007/
● Twitter – https://twitter.com/ankitsharma_007
● GitHub - https://github.com/AnkitSharma-007