Documente Academic
Documente Profesional
Documente Cultură
pdfcrowd.com
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
pdfcrowd.com
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.
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
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
pdfcrowd.com
.constant('FirebaseUrl', 'https://firebase-name-here.firebaseio.com/');
pdfcrowd.com
$firebaseAuth
FirebaseUrl
FirebaseUrl
$firebaseAuth
$firebaseAuth
$firebaseAuth
Firebase
angular.module('angularfireSlackApp')
.factory('Auth', function($firebaseAuth, FirebaseUrl){
var ref = new Firebase(FirebaseUrl);
pdfcrowd.com
Auth
service ready for our application to use, let's create a controller to use
The
$state
use the
go()
service is provided by
function on
$state
ui-router
pdfcrowd.com
reference to the
this
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: ''
};
});
ng-model
on our controller, one for registering users and one for logging in users.
with two functions:
$authWithPassword
$firebaseAuth
$createUser
provides us
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.
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.
Our
register
controller if
$createUser
login
$createUser
error
on the
the user that was just created so we'll need to call the
login
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>
<script src="auth/auth.controller.js"></script>
<script src="auth/auth.service.js"></script>
.state('login', {
url: '/login',
controller: 'AuthCtrl as authCtrl',
templateUrl: 'auth/login.html'
})
.state('register', {
url: '/register',
controller: 'AuthCtrl as authCtrl',
templateUrl: 'auth/register.html'
})
pdfcrowd.com
<form ng-submit="authCtrl.register()">
<div class="input-group">
<input type="email" class="form-control" placeholder="Email" ng-model="authCtrl.user.email">
</div>
<div class="input-group">
<input type="password" class="form-control" placeholder="Password" ng-model="authCtrl.user.password">
</div>
<input type="submit" class="btn btn-default" value="Register">
</form>
<div ng-show="authCtrl.error">
<span>{{ authCtrl.error.message }}</span>
</div>
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
pdfcrowd.com
<form ng-submit="authCtrl.login()">
<div class="input-group">
<input type="email" class="form-control" placeholder="Email" ng-model="authCtrl.user.email">
</div>
<div class="input-group">
<input type="password" class="form-control" placeholder="Password" ng-model="authCtrl.user.password">
</div>
<input type="submit" class="btn btn-default" value="Log In">
</form>
<div ng-show="authCtrl.error">
<span>{{ authCtrl.error.message }}</span>
</div>
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
resolve
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
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
auth
$requireAuth
home
requireNoAuth
auth
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.
pdfcrowd.com
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
pdfcrowd.com
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;
pdfcrowd.com
});
$firebaseArray
splice()
$firebaseArray
push()
pop()
$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
all
returns a
pdfcrowd.com
$firebaseArray
when given a
displayName
uid
getDisplayName(uid)
uid
Firebase auth data, so data in our Firebase will look similar to:
{
"users": {
"simplelogin:1":{
"displayName": "Blake Jackson"
}
}
}
Users
service is created, let's create a controller for updating a user's profile. First we'll
auth
app/app.js
to
resolve
.state('profile', {
url: '/profile',
resolve: {
auth: function($state, Users, Auth){
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
home
login
and
register
.catch
Users
service.
$firebaseArray
$loaded
requireNoAuth
dependency also
getProfile
$firebaseObject
function we created
and
that returns a promise that gets resolved when the data from Firebase is available
locally.
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();
};
pdfcrowd.com
emailHash
ng-model
on
profile
auth
displayName
getGravatar: function(uid){
return '//www.gravatar.com/avatar/' + users.$getRecord(uid).emailHash;
},
<script src="auth/auth.service.js"></script>
<script src="users/users.service.js"></script>
<script src="users/profile.controller.js"></script>
url: '/profile',
controller: 'ProfileCtrl as profileCtrl',
templateUrl: 'users/profile.html',
pdfcrowd.com
<div class="page-wrapper">
<div class="page-header">
<h1>Edit Profile</h1>
</div>
<form ng-submit="profileCtrl.updateProfile()">
<p ng-hide="profileCtrl.profile.displayName">
You'll need a display name before you can start chatting.
</p>
<div class="input-group">
http://localhost:4000/#/profile
our user, submit the form and it should persist when we refresh the page.
open in browser PRO version
pdfcrowd.com
angular.module('angularfireSlackApp')
.factory('Channels', function($firebaseArray, FirebaseUrl){
var ref = new Firebase(FirebaseUrl+'channels');
var channels = $firebaseArray(ref);
return channels;
});
.state('channels', {
open in browser PRO version Are you a developer? Try out the HTML to PDF API
pdfcrowd.com
url: '/channels',
resolve: {
channels: function (Channels){
return Channels.$loaded();
},
profile: function ($state, Auth, Users){
return Auth.$requireAuth().then(function(auth){
return Users.getProfile(auth.uid).$loaded().then(function (profile){
if(profile.displayName){
return profile;
} else {
$state.go('profile');
}
});
}, function(error){
$state.go('home');
});
}
}
})
templateUrl
and
controller
profile
channels
profile
$firebaseArray
profile
that the user already a displayName set, otherwise they're taken to the
open in browser PRO version
of channels,
profile
home
state.
angular.module('angularfireSlackApp')
.controller('ChannelsCtrl', function($state, Auth, Users, profile, channels){
var channelsCtrl = this;
});
channelsCtrl.profile = profile;
channelsCtrl.channels = channels;
channelsCtrl.getDisplayName = Users.getDisplayName;
channelsCtrl.getGravatar = Users.getGravatar;
channelsCtrl.logout = function(){
pdfcrowd.com
Auth.$unauth();
$state.go('home');
};
<script src="users/profile.controller.js"></script>
<script src="channels/channels.controller.js"></script>
<script src="channels/channels.service.js"></script>
<div class="main">
<div class="sidebar">
<div class="slack-name">
<h2>FireSlack</h2>
</div>
<div class="channel-list">
<div class="list-head">Channels</div>
</div>
<div class="my-info">
<img class="user-pic" ng-src="{{ channelsCtrl.getGravatar(channelsCtrl.profile.$id) }}" />
pdfcrowd.com
<div class="user-info">
<div class="user-name">
{{ channelsCtrl.profile.displayName }}
</div>
<div class="options">
<a ui-sref="profile">edit profile</a>
/
<a href="#" ng-click="channelsCtrl.logout()">logout</a>
</div>
</div>
</div>
</div>
</div>
url: '/channels',
controller: 'ChannelsCtrl as channelsCtrl',
templateUrl: 'channels/index.html',
resolve: {
pdfcrowd.com
profileCtrl.updateProfile = function(){
profileCtrl.profile.emailHash = md5.createHash(auth.password.email);
profileCtrl.profile.$save().then(function(){
$state.go('channels');
});
};
This
the
requireNoAuth
channels
register
http://localhost:4000
open in browser PRO version
channels
login
and
requireNoAuth
register
dependency on
login
and
sidebar we just created for our application. There should be the logged in user's name and Gravatar
at the bottom of the sidebar, and an edit profile and logout link next to it. We're using the
directive that comes with
ui-router
ui-sref
displayName
channels
state when we submit the form. The logout link should log us out and send us back to the
home
state.
.state('channels.create', {
url: '/create',
templateUrl: 'channels/create.html',
controller: 'ChannelsCtrl as channelsCtrl'
})
channels
ChannelsCtrl
ui-router
ui-view
pdfcrowd.com
<div class="message-pane">
<ui-view></ui-view>
</div>
channelsCtrl.newChannel = {
name: ''
};
channelsCtrl.createChannel = function(){
channelsCtrl.channels.$add(channelsCtrl.newChannel).then(function(){
channelsCtrl.newChannel = {
name: ''
};
});
};
The
$add
$firebaseArray
.push()
pdfcrowd.com
function on a Javascript
Array
, but keeps our data in sync with Firebase, while returning a promise.
Once the new channel is created we'll need to clear out the
newChannel
object.
<div class="header">
<h1>Create a channel</h1>
</div>
<form ng-submit="channelsCtrl.createChannel()">
<div class="input-group">
<input type="text" class="form-control" placeholder="Channel Name" ng-model="channelsCtrl.newChannel.name">
</div>
<input type="submit" class="btn btn-default" value="Create Channel">
</form>
<div class="channel-list">
<div class="list-head">Channels</div>
<div class="channel" ng-repeat="channel in channelsCtrl.channels">
<a># {{ channel.name }}</a>
</div>
</div>
pdfcrowd.com
ng-repeat
channels
<div class="channel-list">
<div class="list-head">Channels</div>
<div class="channel" ng-repeat="channel in channelsCtrl.channels">
<a># {{ channel.name }}</a>
</div>
<div class="channel create">
<a ui-sref="channels.create">+ create channel</a>
</div>
</div>
We're now able to click on the create channel link and start creating channels!
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
$firebaseArray
forUsers
.state('channels.messages', {
url: '/{channelId}/messages',
resolve: {
messages: function($stateParams, Messages){
return Messages.forChannel($stateParams.channelId).$loaded();
},
channelName: function($stateParams, channels){
pdfcrowd.com
return '#'+channels.$getRecord($stateParams.channelId).name;
}
}
})
channels
$stateParams
forChannel
, provided by
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
states inherit their parent's dependencies. We'll come back and add the
templateUrl
channels
controller
since child
and
angular.module('angularfireSlackApp')
.controller('MessagesCtrl', function(profile, channelName, messages){
var messagesCtrl = this;
});
pdfcrowd.com
Again, the
channels
profile
dependency that we're injecting will actually come from the parent state
messagesCtrl.messages = messages;
messagesCtrl.channelName = channelName;
messagesCtrl.message = '';
pdfcrowd.com
};
uid
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
<script src="channels/channels.service.js"></script>
<script src="channels/messages.service.js"></script>
<script src="channels/messages.controller.js"></script>
<div class="header">
<h1>{{ messagesCtrl.channelName }}</h1>
</div>
<div class="message-wrap" ng-repeat="message in messagesCtrl.messages">
<img class="user-pic" ng-src="{{ channelsCtrl.getGravatar(message.uid) }}" />
<div class="message-info">
<div class="user-name">
pdfcrowd.com
{{ channelsCtrl.getDisplayName(message.uid) }}
<span class="timestamp">{{ message.timestamp | date:'short' }}</span>
</div>
<div class="message">
{{ message.body }}
</div>
</div>
</div>
<form class="message-form" ng-submit="messagesCtrl.sendMessage()">
<div class="input-group">
<input type="text" class="form-control" ng-model="messagesCtrl.message" placeholder="Type a message...">
<span class="input-group-btn">
<button class="btn btn-default" type="submit">Send</button>
</span>
</div>
</form>
ing over
messages
and using
channelName
message.uid
to get the user's display name and Gravatar. We're also using Angular's
date
channelsCtrl
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
url: '/{channelId}/messages',
templateUrl: 'channels/messages.html',
controller: 'MessagesCtrl as messagesCtrl',
channels.messages
ui-sref
selected
ui-sref
directive. The
channelsCtrl.createChannel = function(){
channelsCtrl.channels.$add(channelsCtrl.newChannel).then(function(ref){
$state.go('channels.messages', {channelId: ref.key()});
});
};
pdfcrowd.com
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
{
"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
}
}
}
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);
});
}
}
});
pdfcrowd.com
url
channels.messages
, and the
messages
channelName
templateUrl
and
controller
Messages.forUsers
channelsCtrl.users = Users.all;
pdfcrowd.com
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
$firebaseArray
.info/connected
keyed under
online
$add
any open
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');
});
};
<div class="user-name">
<span class="presence" ng-class="{online: channelsCtrl.profile.online}"></span>
{{ channelsCtrl.profile.displayName }}
</div>
online
ng-class
, based on if the
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.
.read
.write
, and
.validate
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"
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"
}
}
}
}
}
}
}
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
pdfcrowd.com
Home
About
Topics
Pro
Contact Us
2015 Thinkster
pdfcrowd.com