In this project, you'll get comfortable with hooking Firebase to your Angular application in order to persist your data.
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 ui-router as scripts in your html file (Google them), then the rest of your basic angular files.
- In your app.js file include
firebase
andui.router
to your module's dependencies. - Add a
.config
function and include$stateProvider
and$urlRouterProvider
to your injections. - Create a router and add
/threads
and/threads/:threadId
as the URLS. - Use
.$urlRouterProvider.otherwise
to redirect any other url to/threads
. - In your index.html file, include your ui-view attribute/element in order to tie in your router. Should look like this below.
<div class="container" ui-view></div>
Note: In today's project we'll ignore authentication, but tomorrow we'll make it so users need to log in to see certain routes/data and we'll persist that user state with Firebase. But for now, we'll just set up the basic structure for that in order to build on top of this functionality tomorrow.
Firebase is very dependent upon URLs, meaning, if you want to set data, get data, remove data, etc, you'll do that based on your Firebase URL. Because of this, it's important that we're able to access our Firebase URL from anywhere. To accomplish this, we'll add a constant
to our Angular app. A constant
is a very common thing in Software Development. It allows us to set a value that won't change.
- Head over to your
app.js
file or wherever you're initiating your new app and right above your.config
method, add a.constant
method with the first argument being "fb" (which is the name of the constant) and the second argument being an object with a key of "url" whose value is "https://rtfm-demo.firebaseio.com/YOUR-GITHUBUSERNAME-HERE".
You can think of this fb
constant as any other service. We're now able to inject fb
anywhere we need it and grab the url
property off of it in order to get our Firebase URL.
I recommend instead of using the rtfm-demo project, you go and create your own Firebase project so you can see and handle the data yourself. To do so, head to Firebase.com and sign up. Once you do that you'll have the option to create a new project. Once you do that, copy the URL it gives you and replace the rtfm-demo URL above with your new URL.
.constant('fb', {
url: 'https://your-firebase-project'
});
Now we need to actually create our Thread view and controller.
- Create a
threads.html
view and athreadsCtrl.js
controller in the appropriate folder. Add the new view and controller to the/threads
route inapp.js
.
-
Create a threadService and put it in the appropriate folder.
-
On that threadService create methods (on
this
) namedgetThreads
andgetThread
. -
Inject your
fb
constant to get your Firebase url.
In order to create a reference to your Firebase (which will allow us to get, delete, add, and update data in our firebase database), we'll need to create a new instance of Firebase passing in the URL to our app.
For example, if my base url was "https://tylers-cool-app.firebaseio.com" then I would do
var firebaseRef = new Firebase("https://tylers-cool-app.firebaseio.com");
to create that reference. Now on firebaseRef, I can do a bunch of fancy things in order to manipulate my data that lives on my Firebase.
- Inside of your
getThreads
method, return a new instance of Firebase passing in your base url you get from thefb
constants service you set up earlier + '/threads'.
Because that was wordy...that method should look like this
this.getThreads = function(){
return new Firebase(fb.url + '/threads');
}
- Now, have the other method (
getThread
) take in athreadId
as its only parameter and return a new instance of Firebase passing in base URL +/threads/
+threadId
.
Now that your threadService is set up, we're going to use Resolve in our routes in order to make sure the data in our Firebase is ready for us when our controller loads.
- Head over to your
app.js
file and in the/threads
route, add a resolve property on the object whose value is another object which has a property of theadsRef whose value is a function. That function is going to take in thethreadService
we just built and it's going to returnthreadService.getThreads()
.
Now since we're using resolve, threadsRef
will be available in our controller if we inject it in and its value will be the data which is coming from our getThreads() method.
-
Open up your
threadsCtrl.js
Add pass inthreadsRef
to the threadsCtrl controller as well as$firebaseArray
. -
Set a property on the $scope object called
threads
which is set to$firebaseArray(threadsRef)
.
Remember, threadsRef is the result of calling getThreads
which just returns us new Firebase('THE FIREBASE URL' + /thread)
and $firebaseArray
just makes it so it gives our data back to us as an Array.
angular.module('rtfmApp')
.controller('threadsCtrl', function ($scope, threadsRef, $firebaseArray) {
$scope.threads = $firebaseArray(threadsRef)
});
- 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/>
<input type="text" ng-model="username" placeholder="Username..." 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="#/threads/{{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 = $firebaseArray(threadsRef)
$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
/threads/:threadId
route inapp.js
. Also create a resolve forthread
that uses$stateParams.threadId
andthreadService.getThread()
to inject each thread's AngularFire ref into your newthreadCtrl
.
.state('thread', {
url: '/threads/:threadId',
templateUrl: 'path/to/thread.html',
controller: 'threadCtrl',
resolve: {
threadRef: function (threadService, $stateParams) {
return threadService.getThread($stateParams.threadId);
}
}
});
- Inject
threadRef
into yourthreadCtrl
and use AngularFire's$firebaseObject
and$bindTo
methods to bind the thread to$scope.thread
.
angular.module('rtfmApp')
.controller('threadCtrl', function ($scope, threadRef, $firebaseObject) {
var thread = $firebaseObject(threadRef);
thread.$bindTo($scope, 'thread');
});
Why $firebaseObject 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 your thread template 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/>
<input type="text" ng-model="username" placeholder="Username..." 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 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.
- In your
threadService
create a getComments method which takes in athreadId
and returns a new Firebase instance passing in the base url + '/threads/' + threadId + '/comments'.
getComments: function (threadId) {
return new Firebase(fb.url + '/threads/' + threadId + '/comments');
}
- In your
app.js
file under your/threads/:threadId
route under resolve, add acommentsRef
method which takes inthreadService
as well as$stateParams
and return the invocation ofthreadService.getComments($stateParams.threadId)
.
It should look like this,
commentsRef: function (threadService, $stateParams) {
return threadService.getComments($stateParams.threadId);
}
-
Now in your
threadCtrl
injectcommentsRef
as well as$firebaseArray
and on the $scope object set acomments
property equal to the invocation of $firebaseArray passing incommentsRef
. -
Now add your
createComment
method to the $scope object. This method should take in a username and a text and then invoke the $add property on$scope.comments
passing it an object with a key of username and the value being the username you passed in as well as a key of text and a value being the text you passed in. The finalthreadCtrl
should look like this,
.controller('threadCtrl', function ($scope, threadRef, commentsRef, $firebaseObject, $firebaseArray) {
var thread = $firebaseObject(threadRef);
thread.$bindTo($scope, 'thread');
$scope.comments = $firebaseArray(commentsRef);
$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 adds a comment to your AngularFire comments
"array".
===========
Day Two: Authentication We're going to add Firebase's authentication to our app. Buckle up.
Step 1: Prepare the app
You need to go to your Firebase app and enable authentication. For this app we're going to enable email/password authentication. Head over to your Firebase dashboard and configure the email/password authentication piece.
Once that's finished, add these additional routes to your app:
Login
State: login Url: '/login' controller: 'loginCtrl' templateUrl: 'login.html'
Signup
State: signup Url: '/signup' controller: 'signupCtrl' templateUrl: 'signup.html'
Step 2: Create and set up the userService
We're going to utilize a service to manage our authentication for us. Create a userService and add the following methods:
getUser (returns the $getAuth() result) register (returns the $createUser(newUser) result) login (returns the authWithPassword(user) result) Above your service methods, create an authRef (using Firebase) and an auth object (using $firebaseAuth). This should look something like this:
var authRef = new Firebase(firebaseUrl.url); var auth = $firebaseAuth(authRef); Step 3: Wire up the login and signup states
In your signup controller, create a $scope.register method that calls the userService's register method. You'll need to pass in the new user object (including email and password) into this method. Be sure you have the inputs and ng-models necessary in your view to accomplish this.
In the login controller, create a $scope.login method that calls the userService's login method. You'll need to pass in the user object (including email and password) from the view into this method. Be sure you have the inputs and ng-models necessary in your view to accomplish this. If the login succeeds, call $state.go to redirect the user to the threads page.
Step 4 (going further): Handle logging out
We're going to use a nifty ability in routing to create a logout app.
.state('logout', { url: '/logout', controller: function(UserService) { return userService.logout(); }, }) See what we're doing? The only purpose of this route state is to call the userService.logout method.
this.logout = function(user) { return auth.$unauth(); } You're also going to want to watch the $onAuth in the userService so we can send the user to the correct view when they're not logged in.
auth.$onAuth(function(authData) { if (!authData) { $state.go('login') } }); If you want another challenge, create a "logout" directive that checks the userService for the user's auth status and shows a Log out link if appropriate. This directive could be placed anywhere in your app and would allow the user to see their current state and log out.