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:
- 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
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 ui-router as
our router. We'll also be using the "controller as" syntax for referencing our
controllers.
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/');
Creating the Auth Service
angular.module('angularfireSlackApp')
.factory('Auth', function(){
});
Here we'll inject $firebaseAuth, which is a service that AngularFire provides
us with, along with our constant, FirebaseUrl. Then we'll be able to create a
reference to Firebase using the Firebase constructor and our FirebaseUrl,
which we'll be passing to the $firebaseAuth service. See the
angularFire API docs
for a list of available methods $firebaseAuth provides. Our factory will
return the $firebaseAuth 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);
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 service is provided by ui-router for us to control the state
of our application. We can use the go() function on $state to redirect
our application to a specific state. We also create a 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. $firebaseAuth provides us with two functions:
$authWithPassword for logging in users and $createUser 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 (){
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 function works very similarly to our login function. We want
to set error on the controller if $createUser fails, however, when
$createUser succeeds, it doesn't automatically log in 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.
src="app.js">
src="auth/auth.controller.js">
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'
})
The resulting form should look like this:
ng-submit="authCtrl.register()">
class="input-group">
type="email" class="form-control" placeholder="Email" ng-model="authCtrl.user.email">
class="input-group">
type="password" class="form-control" placeholder="Password" ng-model="authCtrl.user.password">
type="submit" class="btn btn-default" value="Register">
ng-show="authCtrl.error">
{{ 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.
ng-submit="authCtrl.login()">
class="input-group">
type="email" class="form-control" placeholder="Email" ng-model="authCtrl.user.email">
class="input-group">
type="password" class="form-control" placeholder="Password" ng-model="authCtrl.user.password">
type="submit" class="btn btn-default" value="Log In">
ng-show="authCtrl.error">
{{ 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. 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 $requireAuth function which
returns a promise. This promise will get resolved with an auth object if the
user is logged in. The Firebase Documentation
provides a table of what information is available to us within auth. If the
user is not authenticated, the promise gets rejected. In our requireNoAuth
dependency, if the User is logged in we want to send them back to the home
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.
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 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;
});
It's also good to know that while $firebaseArray will return pseudo array,
meaning it will act a lot like an array in javascript, however, methods like
splice(), push(), pop() will only affect data locally and not on the
Firebase. Instead, $firebaseArray 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) allows us to get a $firebaseObject of a specific user's
profile, while all returns a $firebaseArray of all the users.
getDisplayName(uid) is a helper function that returns a user's displayName
when given a uid. 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 app/app.js to
resolve a couple dependencies. We want to have the user's auth data
and their profile available to us before our controller is instantiated.
.state('profile', {
url: '/profile',
resolve: {
auth: function($state, Users, Auth){
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 properties out of the state
configuration temporarily because we haven't created them yet. The auth
dependency is similar to the requireNoAuth dependency we created for
login and register, except it does the inverse, where the user is
redirected to the home state if they're not authenticated. The .catch
function is a shorthand for handling promises if we don't want to provide
a success handler. The profile dependency also ensures authentication,
but resolves to the user's profile using the getProfile function we
created in our Users service. $loaded is a function provided by
both $firebaseObject and $firebaseArray that returns a promise that
gets resolved when the data from Firebase is available locally.
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();
};
Here we're getting the current user's email from the auth data that was
resolved from our router, hashing it and setting to emailHash on profile.
displayName will be set from the template we'll be creating next using
ng-model.
getGravatar: function(uid){
return '//www.gravatar.com/avatar/' + users.$getRecord(uid).emailHash;
},
src="auth/auth.service.js">
src="users/users.service.js">
src="users/profile.controller.js">
url: '/profile',
controller: 'ProfileCtrl as profileCtrl',
templateUrl: 'users/profile.html',
class="page-wrapper">
class="page-header">
Edit Profile
ng-submit="profileCtrl.updateProfile()">
ng-hide="profileCtrl.profile.displayName">
You'll need a display name before you can start chatting.
class="input-group">
required type="text" class="form-control" placeholder="Display Name" ng-model="profileCtrl.profile.displayName">
type="submit" class="btn btn-default" value="Set Display Name">
We should now be able to navigate to http://localhost:4000/#/profile, specify
a display name for our user, submit the form and it should persist when we
refresh the page.
Adding Messaging Functionality
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 function on our service returns a $firebaseArray of messages
when provided a channelId. Later in this tutorial, we'll create a forUsers
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){
return '#'+channels.$getRecord($stateParams.channelId).name;
}
}
})
This state will again be a child state of channels. Our url will have a
channelId parameter. We can access this parameter with $stateParams,
provided by ui-router. We're resolving messages, which is using the
forChannel function from our Messages service, and 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 channels since child states
inherit their parent's dependencies. We'll come back and add the controller
and templateUrl properties once we create our controller and template.
angular.module('angularfireSlackApp')
.controller('MessagesCtrl', function(profile, channelName, messages){
var messagesCtrl = this;
});
Again, the profile dependency that we're injecting will actually come from
the parent state channels 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 = '';
});
}
};
A message object will need to contain uid, which will be how we identify
who sent the message. body 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.
src="channels/channels.service.js">
src="channels/messages.service.js">
src="channels/messages.controller.js">
class="header">
{{ messagesCtrl.channelName }}
class="message-wrap" ng-repeat="message in messagesCtrl.messages">
class="user-pic" ng-src="{{ channelsCtrl.getGravatar(message.uid) }}" />
class="message-info">
class="user-name">
{{ channelsCtrl.getDisplayName(message.uid) }}
class="timestamp">{{ message.timestamp | date:'short' }}
class="message">
{{ message.body }}
class="message-form" ng-submit="messagesCtrl.sendMessage()">
class="input-group">
type="text" class="form-control" ng-model="messagesCtrl.message" placeholder="Type a message...">
class="input-group-btn">
class="btn btn-default" type="submit">Send
Here we're creating a header to display the channelName from our controller.
Then we're ng-repeating over messages and using message.uid and the
helper functions from channelsCtrl to get the user's display name and
Gravatar. We're also using Angular's date 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 sendMessage function from our
controller.
url: '/{channelId}/messages',
templateUrl: 'channels/messages.html',
controller: 'MessagesCtrl as messagesCtrl',
ui-sref="channels.messages({channelId: channel.$id})" ui-sref-active="selected"># {{ channel.name }}
We're specifying the parameters for the channels.messages state within the
ui-sref directive. The ui-sref-active directive will add the specified
class (selected in our case) to the element when a state specified in a
sibling or child ui-sref 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()});
});
};
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){
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
}
}
}
}
}
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);
});
}
}
});
This state is almost identical to channels.messages, using the same
templateUrl and controller. We're using a different url, and the
messages dependency is using the Messages.forUsers function that we just
created. The channelName dependency also looks up the other user's display
name, and prefixes it with @.
channelsCtrl.users = Users.all;
class="channel create">
ui-sref="channels.create">+ create channel
class="list-head">Direct Messages
class="channel" ng-repeat="user in channelsCtrl.users">
ng-if="user.$id !== channelsCtrl.profile.$id" ui-sref="channels.direct({uid: user.$id})" ui-sref-active="selected">
{{ user.displayName }}
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);
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 .info/connected node and will $add
any open connections to a $firebaseArray keyed under online 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(){
channelsCtrl.profile.online = null;
channelsCtrl.profile.$save().then(function(){
Auth.$unauth();
$state.go('home');
});
};
class="user-name">
class="presence" ng-class="{online: channelsCtrl.profile.online}">
{{ channelsCtrl.profile.displayName }}
We're also dynamically adding the online class to the span tag using
ng-class, based on if the $firebaseArray containing connections in the
profile is present.
ng-if="user.$id !== channelsCtrl.profile.$id" ui-sref="channels.direct({uid: user.$id})" ui-sref-active="selected">
class="presence" ng-class="{online: user.online}">
{{ user.displayName }}
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.
{
"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"
}
}
}
},
"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"
}
}
}
}
}
}
}
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!