Nowadays, most applications have a commenting system of sorts, whether it’s commenting on a status update or a story or even a live feed. With status updates also comes the ‘like’ feature which was made popular by companies like Facebook. In this tutorial, we will add reactions to a comment just like Facebook allows you do on status updates.
What we will be building
We will be building a simple application where you can publish a status and after publishing a status, you can add reactions to it. You can like, dislike and love any status on the application.
The application will be powered by Angular 4 in the front-end and Firebase in the backend. In following articles, I will demonstrate how to add other real-time features to the app using a tool like Pusher, but for this one, we will be focusing on the update and reactions part. It will look something like this after we are done.
Requirements for building our status update reactions app
To successfully follow along in this tutorial, you need to have a few things in place. Here is a list of those things:
- NodeJS & npm installed on your local machine.
angular-cli
installed on your machine. Run $ npm install -g @angular/cli
to install it.
- Visual Studio Code or any text editor of your choice.
- Basic knowledge of Sass, TypeScript and JavaScript.
- Firebase account to create a database for our application.
Let us get started with building the application. To start we will need to create our Angular application. You can create a new application by running the ng new
command as will be explained below.
Creating our new Angular application
To create our new application, we can use the Angular CLI and just run the command below in the directory we want to create our project in:
$ ng new reactions --style scss --skipTests
The command above will create a new directory called reactions, and will set our default style engine to SCSS, and will skip creating tests for the application. Tests are important but we won’t be needing them for this tutorial.
Once this is complete, open your project in a text editor of your choice. Visual Studio Code is recommended solely because it has inbuilt support for TypeScript, but you can use whichever you want to use.
Creating the Angular Application UI
Let us focus on building the front-end or UI of our application. We will need FontAwesome and Bootstrap for the application so let’s pull those in:
npm install --save bootstrap font-awesome
Now we need to import Bootstrap and FontAwesome into our stylesheets build process. Open up your src/styles.scss
file and add the following to the file:
// import bootstrap and font awesome into our scss file
@import "~bootstrap/dist/css/bootstrap.min.css";
@import "~font-awesome/css/font-awesome.css";
body {
background-color: #e6ecf0;
}
For this tutorial, we do not need the JavaScript part of Bootstrap but if you want to add this to your application, you can import it in the scripts section of .angular.cli.json
. You will need to add the path relative to the file. For example, to add Bootstrap, the path will be: ../node_modules/bootstrap/dist/js/bootstrap.js
Now we are ready to start building our application. To serve our application, run the command below to serve your application:
ng serve --open
Once the build is complete, it will automatically open the application on your default browser. If you did everything correctly, you should see the default Angular page on your screen. Perfect.
Creating Angular components for our page
Next, let's create some components to make up the page. We will create a navigation component where we will add our navigation items. It is unimportant to the tutorial and it is just going to be there for aesthetic reasons. Generate the component using the ng g component
command as seen below:
Creating the Angular navigation component
$ ng g component nav --spec=false
The --spec
flag is basically a boolean that specifies if a spec file is generated. We do not need this for this tutorial.
This should create a new nav
directory in the ./src/app
folder. Open the nav.component.html
file and replace the contents with the code below:
<!-- Bootstrap navigation bar -->
<nav class="navbar navbar-fixed navbar-default">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li class="active"><a href="#"><i class="fa fa-home"></i> Home <span class="sr-only">(current)</span></a></li>
<li><a href="#"><i class="fa fa-bell-o"></i> Notifications</a></li>
<li><a href="#"><i class="fa fa-commenting-o"></i> Messages</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li class="profile-photo">
<a href="#" alt="John Doe"><img src="https://randomuser.me/api/portraits/men/84.jpg" alt=""></a>
</li>
</ul>
<form class="navbar-form navbar-right">
<div class="form-group">
<input type="text" class="form-control" placeholder="Search" disabled>
</div>
</form>
</div>
</div>
</nav>
This is just a basic Bootstrap navigation bar. Now let’s add some custom styling to the navigation bar component. Open the nav.component.scss
file and add the following:
// Style the navigation bar icon
.nav i.fa {
font-size: 18px;
margin-right: 5px;
top: 3px;
}
// Style the navigation bar profile photo
.nav > li.profile-photo > a {
padding: 8px 0;
img {
border-radius: 100%;
max-width: 34px;
}
}
Finally, to see our navigation bar, we will need to clear the contents of our app.component.html
file and replace it with the component selector, as shown in the code below:
<!-- Show the angular navigation component -->
<app-nav></app-nav>
<div class="container">
<div class="row">
<div class="col-xs-12 col-md-3">
<!--<app-sidebar></app-sidebar>-->
</div>
<div class="col-xs-12 col-md-9">
<div class="statuses">
<!--<app-statuses></app-statuses>-->
</div>
</div>
</div>
</div>
We commented out the app-sidebar
and app-statuses
component selectors because they do not exist yet. We will create them later in the article and uncomment the component selectors to show them.
If you are still running the ng serve
command, refresh the page and you should see your changes on the browser.
Creating the Angular sidebar component
The next thing we want to add after the navigation is the sidebar. As we did before, we will run the ng g component
command to create the sidebar Angular component:
$ ng g component sidebar --spec=false
Open the sidebar.component.html
file and replace the contents with the HTML code below:
<aside class="main">
<div class="profile-block">
<div class="photo">
<div class="banner-photo"></div>
</div>
<div class="profile">
<div class="row">
<div class="col-xs-4">
<div class="profile-photo">
<img src="https://randomuser.me/api/portraits/men/84.jpg" alt="">
</div>
</div>
<div class="col-xs-8">
<span class="name">John Doe</span>
<span class="handle">@johndoe</span>
</div>
</div>
<div class="row assets">
<div class="col-xs-6">
<span class="assets-title">Following</span>
<span class="assets-count">10</span>
</div>
<div class="col-xs-6">
<span class="assets-title no-left-pad">Followers</span>
<span class="assets-count no-left-pad">103K</span>
</div>
</div>
</div>
</div>
<div class="followers">
<h3>Followers</h3>
<div class="row">
<div class="col-xs-12">
<ul class="list-inline">
<li><a href="#"><img src="https://randomuser.me/api/portraits/women/25.jpg" alt=""></a></li>
<li><a href="#"><img src="https://randomuser.me/api/portraits/women/26.jpg" alt=""></a></li>
<li><a href="#"><img src="https://randomuser.me/api/portraits/men/25.jpg" alt=""></a></li>
<li><a href="#"><img src="https://randomuser.me/api/portraits/men/89.jpg" alt=""></a></li>
<li><a href="#"><img src="https://randomuser.me/api/portraits/men/39.jpg" alt=""></a></li>
<li><a href="#"><img src="https://randomuser.me/api/portraits/men/59.jpg" alt=""></a></li>
<li><a href="#"><img src="https://randomuser.me/api/portraits/women/39.jpg" alt=""></a></li>
<li><a href="#"><img src="https://randomuser.me/api/portraits/women/79.jpg" alt=""></a></li>
</ul>
</div>
</div>
</div>
</aside>
This should add a make-believe sidebar. Next, open the sidebar.component.scss
file and add the accompanying style for the sidebar component:
aside.main {
.photo {
height: 95px;
width: 100%;
background-color: #d9d9d9;
}
.banner-photo {
background-image: url(https://pbs.twimg.com/profile_banners/843496574332354560/1497099556/600x200);
height: 100%;
width: 100%;
background-repeat: no-repeat;
background-size: cover;
background-position: center;
}
.profile {
background-color: #fff;
min-height: 50px;
.profile-photo {
img {
max-width: 100%;
width: 72px;
float: right;
left: 25px;
position: absolute;
border-radius: 100%;
top: -26px;
border: 2px solid #fff;
}
}
.col-xs-8 {
padding-left: 10px;
}
.name {
font-weight: 600;
font-size: 18px;
margin-top: 5px;
display: block;
line-height: 1.2;
}
.handle {
display: block;
margin-bottom: 10px;
color: #657786;
}
}
.assets {
padding: 10px 0;
.assets-count {
padding: 0 16px;
display: block;
font-size: 13px;
color: #657786;
}
.assets-title {
font-weight: 600;
padding: 0 16px;
color: #657786;
display: block;
}
.no-left-pad {
padding-left: 0;
}
}
.followers {
margin-top: 10px;
padding: 10px 16px;
background-color: #fff;
h3 {
margin: 0 0 10px;
font-size: 17px;
font-weight: 600;
color: #14171a;
}
.list-inline {
margin-bottom: 0;
}
.list-inline li {
padding: 10px 5px;
a {
display: block;
}
}
img {
border-radius: 100%;
max-width: 45px;
}
}
}
Now go to the app.component.html
file and uncomment the <app-statuses></app-statuses>
tag. This should make the sidebar instantly visible on the browser. So far you should have something similar to the screenshot below:
Connecting our application to Firebase
Now, let’s create the statuses component and the statuses service. The statuses service will be the place where all the calls to Firebase will happen from. It will be the proxy between our application and Firebase. It is important to use services for such interfacing because it becomes reusable as opposed to using Firebase directly.
Before we create the component let us import Firebase and the AngularFire library. You will need to stop the ng serve
command if it is currently running. Now run the command below to install the required Firebase libraries:
$ npm install angularfire2 firebase --save
After it is done, we need to configure firebase. Open the app.module.ts
file and add the following into the file:
// Import the AngularFireModule and the AngularFireDatabaseModule
import { AngularFireModule } from 'angularfire2';
import { AngularFireDatabaseModule } from 'angularfire2/database';
// Define the firebase database configuration keys, get it from your Firebase application console
export const firebaseConfig = {
apiKey: "",
authDomain: "",
databaseURL: "",
projectId: "",
storageBucket: "",
messagingSenderId: ""
};
Scroll down to the imports section of the same file and add the AngularFireModule
and AngularFireDatabaseModule
to the @NgModule
imports as shown below:
@NgModule({
// ...
imports: [
// ...
// Import as an NgModule...
AngularFireModule.initializeApp(firebaseConfig),
AngularFireDatabaseModule
]
// ...
})
Next, go to the Firebase website and create a new Firebase application.
When the application has been created, navigate to “Add Firebase to your web app” and then use the credentials to replace the empty firebaseConfig
constant in your app.module.ts
file.
One last thing, in your Firebase application dashboard, navigate to “Database” and then click on “Rules”, in the field, paste the following:
{
"rules": {
"statuses": {
".read": "true",
".write": "true",
".indexOn": "createdAt"
}
}
}
IMPORTANT: This will turn off authentication for your Firebase and should only be used for the purposes of this application ALONE. This is NOT recommended for a production application!
Creating the Status Angular Component & Service
Now that we have Firebase plugged in, we can create the statuses component and the service.
$ ng g component statuses --spec=false
$ ng g service statuses/statuses --spec=false
The first command basically creates a status component, while the second command creates a statuses.service.ts
file in the ./app/statuses
directory.
Open the ./app/statuses/status.service.ts
file so we can add the Firebase functionality. Replace the existing code with the code below:
// Import the required packages to the service
import { Injectable } from '@angular/core';
import { AngularFireDatabase, FirebaseListObservable } from 'angularfire2/database';
import "rxjs/add/operator/map";
@Injectable()
export class StatusesService {
// Flag to see if status update is in progress
private inProgress: boolean = false
// Possible available reactions
private reactions: string[] = ['like', 'love', 'dislike']
// All the statuses available
public statuses: FirebaseListObservable<any[]>
// The maimum length and minimum length of a status
public maxLength:number = 500
public minLength:number = 0
// Flag that determines if the status text is valid or nah
public statusTextValid: boolean = false
// Class constructor, injects the angular fire database as this.af
constructor(private af: AngularFireDatabase) { }
// ----------------------------------------------------------------------
// Method to post the status to Firebase
// ----------------------------------------------------------------------
post(status: string) {
if ( ! this.updating()) {
this.inProgress = true
let payload = {text: status, like:0, dislike:0, love:0, createdAt: {".sv": "timestamp"}};
this.statuses.push(payload).then( snapshot => {
this.inProgress = false
})
}
}
// ----------------------------------------------------------------------
// Method to send a reaction to a status to Firebase
// ----------------------------------------------------------------------
react(reaction: string, status) {
if (~this.reactions.indexOf(reaction)) {
let reactions: any = {}
let count: number = isNaN(parseInt(status[reaction])) ? 0 : parseInt(status[reaction])
reactions[reaction] = count+1
this.statuses.update(status.$key, reactions)
}
}
// ----------------------------------------------------------------------
// Method to get the recent statuses from Firebase
// ----------------------------------------------------------------------
recent(amount: number): FirebaseListObservable<any[]> {
return this.statuses = this.af.list('/statuses').map(arr => arr.reverse()) as FirebaseListObservable<any[]>;
}
// ----------------------------------------------------------------------
// Method to check the validity of a status update
// ----------------------------------------------------------------------
valid(status: string) : boolean {
return status.length >= this.minLength && status.length <= this.maxLength
}
// ----------------------------------------------------------------------
// Method to check the in progress flag
// ----------------------------------------------------------------------
updating() : boolean {
return this.inProgress
}
}
Whoa! That is a lot! The first 3 lines simply import node modules that we will be needing for the service to work properly. We define a couple of private
and public
properties for the class that will be used both internally and externally in the class.
In the constructor, we initialize the af
property which is for working with Firebase. We also create a post
method, and in there, we define the logic for posting status updates to Firebase.
The react
method checks if the reaction is valid and if it is, it posts a reaction to the Firebase. The recent
method fetches the most recent statuses from Firebase. The valid
method just checks if the status text is valid and the updating
checks to see if there is currently a post to Firebase pending completion.
Next, we have to register the service so our application can see and use it. In the app.module.ts
file, add the following:
// Import the status service and forms module
import { FormsModule } from '@angular/forms';
import { StatusesService } from './statuses/statuses.service';
@NgModule({
imports: [
// .... other modules
FormsModule,
// ...other modules
],
providers: [
// Add the status service as a provider
StatusesService
],
// ...
})
Great, now we have registered the service. Moving on.
The next thing we will do is update our statuses.component.ts
to interact with our application using the statuses.service.ts
file. Open the statuses.component.ts
file and replace it with the following:
import { Component, OnInit } from '@angular/core';
import { StatusesService } from './statuses.service';
@Component({
selector: 'app-statuses',
templateUrl: './statuses.component.html',
styleUrls: ['./statuses.component.scss']
})
export class StatusesComponent implements OnInit {
// Status text
public statusText: string
// The status available
public statuses: any[]
// Flag to see if a new status can be added or nah
public canPostStatus: boolean = false
// ---------------------------------------------------------
// Class constructor, injects StatusService as this.status
// ---------------------------------------------------------
constructor(public status: StatusesService) { }
// ---------------------------------------------------------
// ngOnInit is automatically fired on intialisation
// ---------------------------------------------------------
ngOnInit() {
// Get 50 of the most recent statuses
this.status.recent(50)
}
// ---------------------------------------------------------
// Get the status of the text if it's valid or nah
// ---------------------------------------------------------
typingStatus() {
this.canPostStatus = this.status.valid(this.statusText) && this.status.updating() == false;
}
// ---------------------------------------------------------
// Post a status if it is valid
// ---------------------------------------------------------
postStatus() {
this.status.valid(this.statusText) && this.status.post(this.statusText)
}
// ---------------------------------------------------------
// React to an existing post
// ---------------------------------------------------------
react(reaction: string, status) {
this.status.react(reaction, status)
}
}
Above, we defined some properties and methods to use in the lifetime of the component. The ngOnInit
method is called automatically when the component is initialized. In the constructor
, we inject the status service into the component so the methods will be available to this component.
The typingStatus
method will be called every time someone updates the status text from the front-end. We use this to determine if the post button should be enabled or not. The postStatus
posts the status to Firebase using the status service, and finally, the react
method posts to Firebase using the service.
Adding our Status Component to the application
Once we are done with the backend of our Status Component, we will now add the UI to interact with the component.
Open the statuses.component.html
and add replace the content with the HTML below:
<div class="create-status">
<form class="clearfix">
<div class="form-group">
<textarea class="form-control" rows="3" placeholder="Whats happening?" [(ngModel)]="statusText" (ngModelChange)="typingStatus()" name="statusText"></textarea>
</div>
<div class="pull-right">
<span class="countdown">
<!-- Show the characters remaining for the status to remain valid -->
<span *ngIf="statusText">{{this.status.maxLength - statusText.length}}</span>
<span *ngIf="!statusText">{{this.status.maxLength}}</span>
</span>
<!-- On click of this button, post status -->
<button class="btn btn-primary btn-md" [disabled]="canPostStatus != true" (click)="postStatus()">Post</button>
</div>
</form>
</div>
<div class="list-statuses clearfix">
<ul>
<!-- Loop through the statuses and display them -->
<li *ngFor="let status of status.statuses | async">
<div class="photo">
<img src="https://randomuser.me/api/portraits/men/84.jpg" alt="">
</div>
<div class="status">
<p>{{status.text}}</p>
<div class="reaction">
<ul class="list-inline pull-left">
<!-- On click of any of the reactions, call the react method -->
<li><a href="#" class="btn btn-sm btn-default" (click)="react('like', status)">Like</a></li>
<li><a href="#" class="btn btn-sm btn-default" (click)="react('dislike', status)">Dislike</a></li>
<li><a href="#" class="btn btn-sm btn-default" (click)="react('love', status)">Love</a></li>
</ul>
<div class="counts pull-right">
<!-- Show the reaction counts -->
<span class="thumbs reaction" [hidden]="!status.like || status.like <= 0"><span class="count">{{status.like}}</span> Like</span>
<span class="thumbs reaction" [hidden]="!status.dislike || status.dislike <= 0"><span class="count">{{status.dislike}}</span> Dislike</span>
<span class="heart reaction" [hidden]="!status.love || status.love <= 0"><span class="count">{{status.love}}</span> Love</span>
</div>
</div>
</div>
</li>
</ul>
</div>
This is basically HTML that has some parts where Angular is injected. On line 3, we add a text area and we use the ngModel
and ngModelChange
attributes to bind a model and listen for model changes respectively.
On line 8 and 9, we just calculate the available amount of characters left as the user types. On line 11, we have the post button that is disabled
when the canPostStatus
variable is false. We also attached a click event callback postStatus
. Finally, we loop through all the statuses available on Firebase.
Finally, we will update the statuses component's SCSS file. Paste the following in the statuses.component.scss
file:
.create-status {
padding: 10px;
background-color: #fcfcfc;
border-bottom: 1px solid #f0f0f0;
form {
.form-group {
margin-bottom: 5px;
}
.countdown {
color: #657786;
margin-right: 5px;
display: inline-block;
font-size: 13px;
}
textarea {
border-radius: 15px;
padding: 10px 15px;
border-color: #f0f0f0;
resize: none;
margin: 0;
&:focus {
border-color: #00baba;
}
}
button {
border-radius: 15px;
background-color: #00baba;
border: none;
outline: none;
min-width: 80px;
&:focus, &:hover {
background-color: darken(#00baba, 5%);
&[disabled] {
background-color: #00baba;
}
}
}
}
}
.list-statuses {
background-color: #fff;
ul {
list-style: none;
padding: 0;
li {
padding: 20px;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border: none;
}
}
}
.photo {
float:left;
height:40px;
width:40px;
margin-right: 10px;
img {
max-width: 100%;
border-radius: 100%;
}
}
.status {
overflow: hidden;
color: #474747;
p:last-child {
margin-bottom: 0;
}
ul {
list-style: none;
float: left;
padding-left: 5px;
margin-top: 10px;
li {
padding: 0;
}
}
}
.counts {
line-height: 40px;
.reaction {
display: inline-block;
font-weight: 600;
color: #140044;
margin-left: 5px;
&[hidden] {
display: none;
}
}
}
}
Alright great! Now go to the app.component.html
file and uncomment the app-statuses
tag to show your changes. If all goes well and you have ng serve
still active, you should see something like this:
Conclusion
We have successfully created a simple application that has reactions built-in with Firebase and Angular 4. You can use this as the backbone to your application and you can extend the functionality of this application by adding some more reactions and animations. For practice, I would suggest you do just that.
If you have any feedback or have any questions, feel free to leave a comment below. You can see the source code to this application on Github.