Learn to build a Real-Time Slack clone with AngularFire
open in browser PRO version
Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
Stay on the bleeding edge of your favorite technologies.
Albert Pai AngularJS Firebase
Introduction The goal of this tutorial is to guide you through the creation of a Slack clone called fireSlack. Upon completion, you will learn how to build a real time collaborative chat application using angularFire to integrate Firebase with AngularJS. Your application will be able to provide the following features: open in browser PRO version
Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
Sign up for an account Join/create channels to chat in Have a user profile Direct message other users See who's online
Prerequisites This course assumes knowledge of programming and at least basic knowledge of JavaScript and AngularJS. We recommend going through A Better Way to Learn AngularJS if you're not familiar with AngularJS. We've created a seed repo based off of the Yeoman AngularJS Generator to help you get started faster. Before you begin, you will need to have Node.js, npm, and Git installed. We'll need Node.js and npm in order to install Grunt and Bower for managing dependencies. Follow these instructions for installing Node.js and npm, and these instructions for installing Git. Additionally, you'll need to have a free Firebase account and create a Firebase for this tutorial.
Final Notes About the Tutorial open in browser PRO version
Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
You should never copy and paste code from this text unless we tell you to, as we've found that the skills being taught will stick better if you write out all of the code yourself. If you need more clarification on a certain part of the tutorial, we recommend that viewing the supplementary screencast series as we go into far more detail in each of the videos. It's also significantly easier to learn from the screencast series than the text, as you can actually see how a skilled developer manipulates the concepts in AngularJS to build a working application.
Getting Started Once the initial codebase is cloned locally, we'll need to run a few commands to install dependencies and get our application up and running. Within our codebase, run the following commands: After running
grunt serve
, open up
http://localhost:4000
and you should see a splash page for
our application, with a non-functional login and register page ready for us to build off of. In this tutorial, our directory structure will be grouped by feature (see #1 in this list) and we will be using uirouter as our router. We'll also be using the "controller as" syntax for referencing our controllers. open in browser PRO version
Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
Authenticating Users using angularFire Creating a registration and login system for your app can be tedious, but is often one of the most important and required features, and usually requires you to create your own backend. Thankfully, Firebase makes this really easy for us by providing us with a hosted solution. While you have your Firebase pulled up, keep your Firebase URL handy. It should look something like: https://firebase-name-here.firebaseio.com/
.constant('FirebaseUrl', 'https://firebase-name-here.firebaseio.com/');
open in browser PRO version
Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
Creating the Auth Service angular.module('angularfireSlackApp') .factory('Auth', function(){ });
Here we'll inject constant,
$firebaseAuth
FirebaseUrl
constructor and our
, which is a service that AngularFire provides us with, along with our
. Then we'll be able to create a reference to Firebase using the
FirebaseUrl
, which we'll be passing to the
angularFire API docs for a list of available methods the
$firebaseAuth
$firebaseAuth
$firebaseAuth
Firebase
service. See the
provides. Our factory will return
service associated with our Firebase.
The resulting factory should look like this:
angular.module('angularfireSlackApp') .factory('Auth', function($firebaseAuth, FirebaseUrl){ var ref = new Firebase(FirebaseUrl);
open in browser PRO version
Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
var auth = $firebaseAuth(ref); return auth; });
Now that we have an
Auth
service ready for our application to use, let's create a controller to use
with our login and registration forms.
Creating the Auth Controller angular.module('angularfireSlackApp') .controller('AuthCtrl', function(Auth, $state){ var authCtrl = this; });
The
$state
use the
go()
service is provided by function on
open in browser PRO version
$state
ui-router
for us to control the state of our application. We can
to redirect our application to a specific state. We also create a
Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
reference to the
this
keyword within our controller because we're using the
controller as
syntax. For more information about this syntax, see this lesson.
angular.module('angularfireSlackApp') .controller('AuthCtrl', function(Auth, $state){ var authCtrl = this; authCtrl.user = { email: '', password: '' }; });
This user object will be used with the
ng-model
directive in our form. Next, we'll need two functions
on our controller, one for registering users and one for logging in users. with two functions:
$authWithPassword
for logging in users and
$firebaseAuth
$createUser
provides us
for registering users.
Both of these functions take a user object like the one we initialized on our controller, and return a promise. If you're not familiar with how promises work, read this to learn more about promises.
authCtrl.login = function (){
open in browser PRO version
Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
Auth.$authWithPassword(authCtrl.user).then(function (auth){ $state.go('home'); }, function (error){ authCtrl.error = error; }); };
When authentication is successful, we want to send the user to the home state. When it fails, we want to set the error on our controller so we can display the error message to our user.
authCtrl.register = function (){ Auth.$createUser(authCtrl.user).then(function (user){ authCtrl.login(); }, function (error){ authCtrl.error = error; }); };
Our
register
controller if
function works very similarly to our
$createUser
open in browser PRO version
fails, however, when
Are you a developer? Try out the HTML to PDF API
login
function. We want to set
$createUser
error
on the
succeeds, it doesn't automatically log in pdfcrowd.com
the user that was just created so we'll need to call the
login
function we just created to log the user
in. Now that we have our authentication service and controller created, let's update our templates and put them to use.
<script src="app.js"> <script src="auth/auth.controller.js"> <script src="auth/auth.service.js">
.state('login', { url: '/login', controller: 'AuthCtrl as authCtrl', templateUrl: 'auth/login.html' }) .state('register', { url: '/register', controller: 'AuthCtrl as authCtrl', templateUrl: 'auth/register.html' })
open in browser PRO version
Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
The resulting form should look like this:
{{ authCtrl.error.message }}
This div will remain hidden until our authentication controller reaches an error, in which case the error message it will get displayed to our user. Next, let's update our login template in a similar fashion. open in browser PRO version
Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
{{ authCtrl.error.message }}
Now we should have a working register and login system, but we have no way of telling if the user is logged in or not. The login and registration pages are still accessible if we are authenticated. We can resolve this by using the
resolve
property on our states.
resolve
allows us to create dependencies
that can be injected into controllers or child states. These dependencies can depend on services in our app that return promises, and the promises will get resolved before our controller gets instantiated. open in browser PRO version
Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
Read the ui-router Github Wiki if you're not familiar with how
resolve
works with
ui-router
.
resolve: { requireNoAuth: function($state, Auth){ return Auth.$requireAuth().then(function(auth){ $state.go('home'); }, function(error){ return; }); } }
The
$firebaseAuth
service provides us with a
promise will get resolved with an
auth
$requireAuth
object if the user is logged in. The Firebase Documentation
provides a table of what information is available to us within the promise gets rejected. In our them back to the
home
function which returns a promise. This
requireNoAuth
auth
. If the user is not authenticated,
dependency, if the User is logged in we want to send
state, otherwise, we need to catch the error that gets thrown and handle it
gracefully by returning nothing, allowing the promise to be resolved instead of rejected. Now, we should no longer be able to access the login or register states if we're authenticated.
open in browser PRO version
Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
Storing User Profiles in Firebase Now that we're able to authenticate users, let's create the ability for users to have custom display names to use in our app (rather than showing the user's email or uid)
angular.module('angularfireSlackApp') .factory('Users', function($firebaseArray, $firebaseObject, FirebaseUrl){ var Users = {}; return Users; });
The purpose of this factory is to provide us with the ability to get either a specific user's data, or to get a list of all of our users. Note that while Firebase provides us with a means of authentication, all of the authentication data is separate from our Firebase data and can't be queried. It is up to us to store any open in browser PRO version
Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
custom user data within Firebase manually.
angular.module('angularfireSlackApp') .factory('Users', function($firebaseArray, $firebaseObject, FirebaseUrl){ var usersRef = new Firebase(FirebaseUrl+'users'); var Users = {}; return Users; });
Data in Firebase is stored in a tree structure and child nodes can be referenced by adding a path to our
FirebaseUrl
, so
https://firebase-name-here.firebase.io.com/users
refers to the
users
node.
angular.module('angularfireSlackApp') .factory('Users', function($firebaseArray, $firebaseObject, FirebaseUrl){ var usersRef = new Firebase(FirebaseUrl+'users'); var users = $firebaseArray(usersRef); var Users = {}; return Users;
open in browser PRO version
Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
});
It's also good to know that while
$firebaseArray
an array in javascript, however, methods like and not on the Firebase. Instead,
will return pseudo array, meaning it will act a lot like
splice()
$firebaseArray
,
push()
,
pop()
will only affect data locally
provides methods named
$add
and
$remove
to
provide similar functionality while keeping your data in sync. Read the $firebaseArray Documentation For a complete understanding of how
$firebaseArray
should be used.
var Users = { getProfile: function(uid){ return $firebaseObject(usersRef.child(uid)); }, getDisplayName: function(uid){ return users.$getRecord(uid).displayName; }, all: users };
getProfile(uid) open in browser PRO version
allows us to get a
$firebaseObject
Are you a developer? Try out the HTML to PDF API
of a specific user's profile, while
all
returns a pdfcrowd.com
$firebaseArray
of all the users.
when given a
displayName
uid
getDisplayName(uid)
is a helper function that returns a user's
. We will be keying our data by the
uid
that comes back from our
Firebase auth data, so data in our Firebase will look similar to:
{ "users": { "simplelogin:1":{ "displayName": "Blake Jackson" } } }
Now that our
Users
service is created, let's create a controller for updating a user's profile. First we'll
need to create a new state in user's
auth
app/app.js
to
resolve
a couple dependencies. We want to have the
data and their profile available to us before our controller is instantiated.
.state('profile', { url: '/profile', resolve: { auth: function($state, Users, Auth){
open in browser PRO version
Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
return Auth.$requireAuth().catch(function(){ $state.go('home'); }); }, profile: function(Users, Auth){ return Auth.$requireAuth().then(function(auth){ return Users.getProfile(auth.uid).$loaded(); }); } } })
We left the
controller
and
templateUrl
because we haven't created them yet. The dependency we created for redirected to the
home
login
and
properties out of the state configuration temporarily auth
dependency is similar to the
register
, except it does the inverse, where the user is
state if they're not authenticated. The
.catch
handling promises if we don't want to provide a success handler. The ensures authentication, but resolves to the user's profile using the in our
Users
service.
$firebaseArray
$loaded
requireNoAuth
is a function provided by both
function is a shorthand for profile
dependency also
getProfile
$firebaseObject
function we created and
that returns a promise that gets resolved when the data from Firebase is available
locally.
open in browser PRO version
Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
angular.module('angularfireSlackApp') .controller('ProfileCtrl', function($state, md5, auth, profile){ var profileCtrl = this; });
We'll be using Gravatar to get profile picture functionality in our application. Gravatar is a service that provides us with a user's profile picture when given an email, however the email needs to be md5 hashed. Luckily, there are many modules available that can do this for us, and angular-md5 was already included in our seed codebase.
profileCtrl.profile = profile;
profileCtrl.updateProfile = function(){ profileCtrl.profile.emailHash = md5.createHash(auth.password.email); profileCtrl.profile.$save(); };
open in browser PRO version
Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
Here we're getting the current user's email from the hashing it and setting to creating next using
emailHash
ng-model
on
profile
.
auth
data that was resolved from our router,
displayName
will be set from the template we'll be
.
getGravatar: function(uid){ return '//www.gravatar.com/avatar/' + users.$getRecord(uid).emailHash; },
<script src="auth/auth.service.js"> <script src="users/users.service.js"> <script src="users/profile.controller.js">
url: '/profile', controller: 'ProfileCtrl as profileCtrl', templateUrl: 'users/profile.html',
open in browser PRO version
Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
open in browser PRO version
Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
We're using the
ng-repeat
directive to iterate over our array of
channels
.
We're now able to click on the create channel link and start creating channels!
Adding Messaging Functionality open in browser PRO version
Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
angular.module('angularfireSlackApp') .factory('Messages', function($firebaseArray, FirebaseUrl){ var channelMessagesRef = new Firebase(FirebaseUrl+'channelMessages'); return { forChannel: function(channelId){ return $firebaseArray(channelMessagesRef.child(channelId)); } }; });
The
forChannel
channelId
function on our service returns a
. Later in this tutorial, we'll create a
$firebaseArray
forUsers
of messages when provided a
function for retrieving direct messages.
.state('channels.messages', { url: '/{channelId}/messages', resolve: { messages: function($stateParams, Messages){ return Messages.forChannel($stateParams.channelId).$loaded(); }, channelName: function($stateParams, channels){
open in browser PRO version
Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
return '#'+channels.$getRecord($stateParams.channelId).name; } } })
This state will again be a child state of access this parameter with which is using the
channels
$stateParams
forChannel
. Our url will have a
, provided by
function from our
ui-router
Messages
channelId
parameter. We can
. We're resolving
service, and
messages
channelName
,
which we'll be
using to display the channel's name in our messages pane. Channel names will be prefixed with a The
channels
dependency we're injecting is coming from the parent state
states inherit their parent's dependencies. We'll come back and add the templateUrl
channels
controller
#
.
since child and
properties once we create our controller and template.
angular.module('angularfireSlackApp') .controller('MessagesCtrl', function(profile, channelName, messages){ var messagesCtrl = this; });
open in browser PRO version
Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
Again, the channels
profile
dependency that we're injecting will actually come from the parent state
that resolves to the current user's profile.
messagesCtrl.messages = messages; messagesCtrl.channelName = channelName;
messagesCtrl.message = '';
messagesCtrl.sendMessage = function (){ if(messagesCtrl.message.length > 0){ messagesCtrl.messages.$add({ uid: profile.$id, body: messagesCtrl.message, timestamp: Firebase.ServerValue.TIMESTAMP }).then(function (){ messagesCtrl.message = ''; }); }
open in browser PRO version
Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
};
A message object will need to contain body
uid
, which will be how we identify who sent the message.
contains the message our user input, and
timestamp
is a constant from
Firebase
that tells
the Firebase servers to use the their clock for the timestamp. When a message sends successfully, we'll want to clear out
messagesCtrl.message
so the user can type a new message.
<script src="channels/channels.service.js"> <script src="channels/messages.service.js"> <script src="channels/messages.controller.js">
open in browser PRO version
Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
{{ channelsCtrl.getDisplayName(message.uid) }} {{ message.timestamp | date:'short' }}
{{ message.body }}
Here we're creating a header to display the ng-repeat
ing over
messages
and using
channelName
message.uid
from our controller. Then we're
and the helper functions from
to get the user's display name and Gravatar. We're also using Angular's
date
channelsCtrl
filter on the timestamp
to display a short timestamp. Finally, at the bottom of our view we have the form for sending messages which submits to the open in browser PRO version
sendMessage
Are you a developer? Try out the HTML to PDF API
function from our controller. pdfcrowd.com
url: '/{channelId}/messages', templateUrl: 'channels/messages.html', controller: 'MessagesCtrl as messagesCtrl',
# {{ channel.name }}
We're specifying the parameters for the ui-sref-active
channels.messages
directive will add the specified class (
state specified in a sibling or child
ui-sref
state within the
selected
ui-sref
directive. The
in our case) to the element when a
directive. Now we should be able to navigate between
channels and start chatting!
channelsCtrl.createChannel = function(){ channelsCtrl.channels.$add(channelsCtrl.newChannel).then(function(ref){ $state.go('channels.messages', {channelId: ref.key()}); }); };
open in browser PRO version
Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
Creating Direct Messages Now that we have working channels with messaging, adding direct messages will be easier since we can reuse a lot of the existing functionality we have.
var userMessagesRef = new Firebase(FirebaseUrl+'userMessages')
return { forChannel: function(channelId){ return $firebaseArray(channelMessagesRef.child(channelId)); }, forUsers: function(uid1, uid2){ open in browser PRO version Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
var path = uid1 < uid2 ? uid1+'/'+uid2 : uid2+'/'+uid1; return $firebaseArray(userMessagesRef.child(path)); } };
We'll be storing our direct messages in Firebase like so:
{ "userMessages": { "simplelogin:1": { "simplelogin:2": { "messageId1": { "uid": "simplelogin:1", "body": "Hello!", "timestamp": Firebase.ServerValue.TIMESTAMP }, "messageId2": { "uid": "simplelogin:2", "body": "Hey!", "timestamp": Firebase.ServerValue.TIMESTAMP } } }
open in browser PRO version
Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
} }
Since we always want to reference the same path in our Firebase regardless of which id was passed first, we'll need to sort our ids before referencing the direct messages.
.state('channels.direct', { url: '/{uid}/messages/direct', templateUrl: 'channels/messages.html', controller: 'MessagesCtrl as messagesCtrl', resolve: { messages: function($stateParams, Messages, profile){ return Messages.forUsers($stateParams.uid, profile.$id).$loaded(); }, channelName: function($stateParams, Users){ return Users.all.$loaded().then(function(){ return '@'+Users.getDisplayName($stateParams.uid); }); } } });
open in browser PRO version
Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
This state is almost identical to We're using a different
url
channels.messages
, and the
function that we just created. The name, and prefixes it with
@
messages
channelName
, using the same
templateUrl
dependency is using the
and
controller
.
Messages.forUsers
dependency also looks up the other user's display
.
channelsCtrl.users = Users.all;
Direct Messages
open in browser PRO version
Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
We're now able to chat directly to other users in our application!
Adding Presence to Users Having direct messaging is an important feature to any chat application, but it's also very useful to know what users are online. Firebase makes this very easy for us. Read the Firebase Documentation to see some example code using presence. While this code is written using the core Firebase library, we're going to replicate the same functionality using AngularFire.
var usersRef = new Firebase(FirebaseUrl+'users'); var connectedRef = new Firebase(FirebaseUrl+'.info/connected');
setOnline: function(uid){ var connected = $firebaseObject(connectedRef); open in browser PRO version Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
var online = $firebaseArray(usersRef.child(uid+'/online')); connected.$watch(function (){ if(connected.$value === true){ online.$add(true).then(function(connectedRef){ connectedRef.onDisconnect().remove(); }); } }); }
This function watches for changes at the connections to a
$firebaseArray
.info/connected
keyed under
online
node and will
$add
any open
within the user's profile. This allows us to
track multiple connections (in case the user has multiple browser windows open), which will get removed when the client disconnects.
Users.setOnline(profile.$id);
channelsCtrl.logout = function(){ open in browser PRO version Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
channelsCtrl.profile.online = null; channelsCtrl.profile.$save().then(function(){ Auth.$unauth(); $state.go('home'); }); };
{{ channelsCtrl.profile.displayName }}
We're also dynamically adding the $firebaseArray
online
class to the span tag using
ng-class
, based on if the
containing connections in the profile is present.
{{ user.displayName }}
open in browser PRO version
Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
We're now able to see when our users are online! Our application is almost ready for production. In the next sections we will go over securing our data and deploying our application live.
Securing Your Data with Security Rules When you first create a Firebase, the default security rules allow full read and write access. While this makes it a lot easier to get started developing, it's always strongly recommended that you create security rules to make sure that your data stays consistent and secured. There are three kinds of rules,
.read
,
.write
, and
.validate
for controlling access and validating your data.
{
open in browser PRO version
Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
"rules":{ ".read": true, "users":{ "$uid":{ ".write": "auth !== null && $uid === auth.uid", "displayName":{ ".validate": "newData.exists() && newData.val().length > 0" }, "online":{ "$connectionId":{ ".validate": "newData.isBoolean()" } } } }, "channels":{ "$channelId":{ ".write": "auth !== null", "name":{ ".validate": "newData.exists() && newData.isString() && newData.val().length > 0" } } }, "channelMessages":{ "$channelId":{ "$messageId":{ ".write": "auth !== null && newData.child('uid').val() === auth.uid", ".validate": "newData.child('timestamp').exists()", "body":{ ".validate": "newData.exists() && newData.val().length > 0"
open in browser PRO version
Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
} } } }, "userMessages":{ "$uid1":{ "$uid2":{ "$messageId":{ ".read": "auth !== null && ($uid1 === auth.uid || $uid2 === auth.uid)", ".write": "auth !== null && newData.child('uid').val() === auth.uid", ".validate": "$uid1 < $uid2 && newData.child('timestamp').exists()", "body":{ ".validate": "newData.exists() && newData.val().length > 0" } } } } } } }
open in browser PRO version
Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
Deploying to Firebase { "firebase": "firebase-name-here", "public": "dist" }
firebase deploy
may prompt you to log in, but afterwards it should push your application to
Firebase's hosting service. Now if you visit https://firebase-name-here.firebaseapp.com/ you should see our completed app, ready for the world to use!
Stay on the bleeding edge of your favorite technologies. Enter your email to receive courses on AngularJS, Node, Android, Swift and more. 1-2 emails per open in browser PRO version
Are you a developer? Try out the HTML to PDF API
Did you like this course? Share it!
pdfcrowd.com
Node, Android, Swift and more. 1-2 emails per week, no spam.
There's more to learn! Check out our latest Firebase courses
Help us educate millions of people around the world for free
Home About
Topics
Pro
Contact Us © 2015 Thinkster
Privacy Policy | Terms of Use
open in browser PRO version
Are you a developer? Try out the HTML to PDF API
pdfcrowd.com