A project start for practicing using Firebase with AngularJS.
We're going to create a multi-user, real-time forum (RTFM).
- Create the basic structure of your Angular application naming your app 'rtfmApp'.
- After you include Angular, include firebase, angularfire, and ngRoute as scripts in your html file (Google them), then the rest of your basic angular files.
- In your app.js file include
firebase
andngRoute
to your module's dependencies. - Add a
.config
function and include$routeProvider
to your injections. - Create a router and add
/login
,/threads
and/threads/:threadId
as the URLS - Use .otherwise and redirectTo '/login'
- In your index.html file include the following line to tie in your router.
<!-- Add your site or application content here -->
<div class="container" ng-view></div>
-
Create a login folder and inside that folder create a login view and a login controller
-
Include your new view and controller in your
login
route.
- Create a new file at
/app/env.js
and set it up like so...
window.env = {
"environment": "development",
"firebase": "https://rtfm-demo.firebaseio.com/chris"
};
Feel free to use my rtfm-demo
firebase, or create you own at firebase.com. If you use
my firebase, please change the base to reflect your name rather than 'chris'. For example you could use
https://rtfm-demo.firebaseio.com/supermario
. All this will do is nest your firebase data under supermario
so
that your data doesn't mix with the rest of the group's.
- Include
env.js
in yourindex.html
file so thatwindow.env
is created before the rest of your JS files load.
<!--Environment vars attached to window.env-->
<script src="env.js"></script>
<!-- included scripts -->
<script src="path/to/angular.js"></script>
<script src="path/to/firebase.js"></script>
<script src="etc/etc/etc"></script>
<!-- end scripts -->
- Create an EnvironmentService to make your environment variables injectable into any Angular module.
angular.module('rtfmApp')
.service('EnvironmentService', function EnvironmentService($window) {
return {
getEnv: function () {
return $window.env;
}
}
});
-
Inject
EnvironmentService
into yourLoginCtrl
and assign your environment to$scope.env
, then read out{{ env }}}
in yourlogin.html
view to confirm that your environment variables are injecting correctly. You should see yourwindow.env
object logged out onto your login view. -
Add
app/env.js
to your
- Open up
login.html
and create a text input bound to$scope.username
and a button that callslogMeIn(username)
when clicked.
<p>This is the login view.</p>
<div>
<input type="text" ng-model="username"/>
<button ng-click="logMeIn(username)">Log In</button>
</div>
-
Create the
logMeIn
function in yourLoginCtrl
. Have italert
the username for now. -
Create a function in
EnvironmentService
calledsaveUsername
that accepts a username and saves it to local storage using$window.localStorage.setItem('username', username);
. -
Create another function in
EnvironmentSerice
calledgetUsername
that returns the username with $window.localStorage.getItem('username'); -
Inject
$location
intoLoginCtrl
and use it to forward the user to thethreads
route after login (which is /threads as the URL, hint, look up how to use $location to redirect to a different URL). -
Create a
threads.html
view and aThreadsCtrl
controller in the appropriate folder. Add the new view and controller to thethreads
route inapp.js
. -
Test your login and make sure that it forwards you to the stubbed threads view.
A problem we're going to run into as we're setting up our routing is sometimes we only want certain authenticated users to see certain routes. What we're going to do in this step is to secure our routes so only those people who we want to see certain routes will be able to.
- Head over to your app.js file and under your .config block, add a new .run block. This .run block will be the first thing that Angular runs before your app starts to be initialized.
- Pass the .run function a callback that accepts three parameters,
$rootScope
,$location
, andEnvironmentService
. $rootScope is exactly like$scope
, but it's global in the sense that anywhere in your application you can get properties that are on$rootScope
. $location allows us to redirect to different locations if we need to. EnvironmentService is where we're going to check if our user is Authenticated. - Inside of our callback we're going to listen for the
$routeChangeStart
event. Whenever a route changes in our application, angular will emit a '$routeChangeStart' which will run our callback. The bigger picture here is that on every route change, we're going to check if that specific user should be seeing that new view.
$rootScope.$on('THEEVENT', function(){
//callback
})
is how you tell angular to listen for certain events. So in side your .run block, tell angular to listen for the '$routeChangeStart' event and pass it a callback function with a 'event', 'next', and 'current' parameter. As you can imagine, 'event' is the event that's happening, 'next' is the route the application is going to, and 'current' is the current route the application is on. 4. Inside your callback, check to see if ```EnvironmentService.getUserName()''' returns a truthy value, if it doesn't that means the user hasn't been created - which means we need to redirect the user to the login page IE $location.path('/login'). If it does, set a property on $rootScope (for now) of username with the value being what getUserName returned.
- Create a ThreadService and put it the appropriate folder.
- Create methods named
getThreads
andgetThread
to generate AngularFire references to all threads and any individual thread. You'll need to injectEnvironmentService
to get your Firebase url and you'll need to inject$firebase
to generate Firebase references (heretofore referred to as "refs").
angular.module('rtfmApp')
.service('ThreadService', function ThreadService(EnvironmentService, $firebase) {
var firebaseUrl = EnvironmentService.getEnv().firebase;
return {
getThreads: function () {
return $firebase(new Firebase(firebaseUrl + '/threads'));
},
getThread: function (threadId) {
return $firebase(new Firebase(firebaseUrl + '/threads/' + threadId));
}
}
});
- Inject the
threadsRef
into theThreadsCtrl
using aresolve
attribute in your router.
.when('/threads', {
templateUrl: 'views/threads.html',
controller: 'ThreadsCtrl',
resolve: {
threadsRef: function (ThreadService) {
return ThreadService.getThreads();
}
}
})
- Open up your
ThreadsCtrl
located inthreads.js
. AddthreadsRef
to its injections and bindthreads.$asArray()
to scope.
// app/scripts/controllers/threads.js
'use strict';
angular.module('rtfmApp')
.controller('ThreadsCtrl', function ($scope, threadsRef) {
$scope.threads = threadsRef.$asArray();
});
Why $asArray()???
If you read the docs, you'll see
that AngularFire refs generated with $firebase
are meant for certain kinds of low-level Firebase transactions.
You don't want to use raw AngularFire refs very often... you want to use $asArray()
or $asObject()
to
convert the ref into an AngularFire array or an AngularFire object. These "arrays" and objects are designed very
specifically to work with Angular views.
AngularFire "arrays" are not true JavaScript arrays (hence the quotation marks), but they are as close as you'll get to an array with Firebase. Firebase doesn't support JavaScript arrays for some very fundamental reasons related to data integrity... but AngularFire "arrays" provide functionality that is very similar to the JS arrays with which you are familiar.
You'll use $asObject()
when you want to interact with the individual keys of the Firebase ref like you would with
a JS object. For instance, a single thread would be treated as an object so that you could do things like this:
var thread = threadRef.$asObject();
thread.title = "This is a new thread";
thread.$save();
Notice that we you could set the object property thread.title
just as you would any JS object.
- Let's set up
threads.html
with a list of threads, an input and a button to create a new thread, and links to each thread's unique page.
<div>
<p>Threads</p>
<form name="newThreadForm">
<input type="text" ng-model="newThreadTitle" placeholder="New thread title..." required/>
<button ng-disabled="newThreadForm.$invalid" ng-click="createThread(username, newThreadTitle)">Add Thread</button>
</form>
<ul>
<li ng-repeat="thread in threads">
<a ng-href="#/secure.thread/{{thread.$id}}">
<span>{{ thread.title }}</span>
<span>(by {{ thread.username }})</span>
</a>
</li>
</ul>
</div>
- You'll need to create a function in your
ThreadsCtrl
namedcreateThread
. This function must be attached to$scope
and should accept a username and a thread title as arguments. It will then use the AngularFire "array"$add
function to add the new thread to thethreads
array. Once you get this working, you'll be able to add threads in your view and watch them automatically add themselves to the threads list.
angular.module('rtfmApp')
.controller('ThreadsCtrl', function ($scope, threadsRef) {
$scope.threads = threadsRef.$asArray();
$scope.threads.$loaded().then(function (threads) {
console.log(threads);
});
$scope.createThread = function (username, title) {
$scope.threads.$add({
username: username,
title: title
});
}
});
- Create a
ThreadCtrl
and athread.html
- Add the new controller and view to the
thread
route inapp.js
. Also create a resolve forthread
that uses$route.current.params.threadId
andThreadService.getThread()
to inject each thread's AngularFire ref into your newThreadCtrl
.
.when('thread/:threadId', {
templateUrl: 'views/thread.html',
controller: 'ThreadCtrl',
resolve: {
threadRef: function (ThreadService, $route) {
return ThreadService.getThread($route.current.params.threadId);
}
}
});
- Inject
threadRef
into yourThreadCtrl
and use AngularFire's$asObject
and$bindTo
methods to bind the thread to$scope.thread
.
angular.module('rtfmApp')
.controller('ThreadCtrl', function ($scope, threadRef) {
var thread = threadRef.$asObject();
thread.$bindTo($scope, 'thread');
});
Why $asObject and $bindTo???
AngularFire refs can get converted into AngularFire "objects". These "objects" can be bound to $scope
using
AngularFire's
$bindTo
function. This sets up 3-way binding from your view, through $scope
and all the way back to your Firebase
data store. You can edit these AngularFire "objects" in place in your view and watch the changes propagate throughout
your entire app.
- Edit
app/views/thread.html
to create a inputs to add comments under the thread as well as read out all existing comments.
<div>
<h1>{{ thread.title }} (by {{ thread.username }})</h1>
<form name="newCommentForm">
<input type="text" ng-model="newCommentText" placeholder="Write a comment..." required/>
<button ng-disabled="newCommentForm.$invalid" ng-click="createComment(username, newCommentText)">Add Comment</button>
</form>
<ul>
<li ng-repeat="comment in comments">{{ comment.username }}: {{ comment.text }}</li>
</ul>
</div>
Notice how we're looping through comment in comments
? We're going to want each thread to have an "array" of
comments in its Firebase data structure. We haven't created the comments
"array" yet, but we can create an
AngularFire ref to it anyway. Firebase will treat that ref as if it already exists, so we can loop through it and add
to it seamlessly. This will require creating a new getComments
method in ThreadService
and injecting this
new commentsRef
into ThreadCtrl
using a resolve
in your secure.thread
route.
This may seem like a lot of steps, but you've already gone through these steps twice with threadsRef
and
threadRef
. The new commentsRef
follows the same pattern.
angular.module('rtfmApp')
.service('ThreadService', function ThreadService(EnvironmentService, $firebase) {
var firebaseUrl = EnvironmentService.getEnv().firebase;
return {
getThreads: function () {
return $firebase(new Firebase(firebaseUrl + '/threads'));
},
getThread: function (threadId) {
return $firebase(new Firebase(firebaseUrl + '/threads/' + threadId));
},
getComments: function (threadId) {
return $firebase(new Firebase(firebaseUrl + '/threads/' + threadId + '/comments'));
}
}
});
.when('/thread', {
templateUrl: 'views/thread.html',
controller: 'ThreadCtrl',
resolve: {
threadRef: function (ThreadService, $route) {
return ThreadService.getThread($route.current.params.threadId);
},
commentsRef: function (ThreadService, $route) {
return ThreadService.getComments($route.current.params.threadId);
}
}
})
angular.module('rtfmApp')
.controller('ThreadCtrl', function ($scope, threadRef, commentsRef) {
var thread = threadRef.$asObject();
thread.$bindTo($scope, 'thread');
$scope.comments = commentsRef.$asArray();
$scope.createComment = function (username, text) {
$scope.comments.$add({
username: username,
text: text
});
};
});
Notice that we've added a new $scope.createComment
function. This will get called from the thread.html
view
and add a comment to your AngularFire comments
"array".
This is the seed of a functioning Angular + Firebase application. You could really take it anywhere, but a great first
step would be to use
FirebaseSimpleLogin
to create a real login system rather than the localStorage
hack that we've used here.
You'll want to create users, get the logged in user and offer a "log out" button.
Check out this example user-service if you get stuck. It's got some more advanced code that may look confusing at first, but read through each function and try to understand what it's doing. If you can't understand the function, skip it and circle back later. The important functions in this example are the simple ones.