Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Lindsey Redinger
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Lindsey Redinger

My First Look At Using The Firebase Backend-As-A-Service (BaaS) In AngularJS

By
Published in Comments (3)

Over the last couple of days, I've started to dig into Firebase, a data storage and data synchronization Backend-as-a-Service (Baas). Like Pusher and PubNub, it can push events over WebSockets. But, it doesn't [necessarily] deal with arbitrary events; rather, it deals with synchronization events that automatically keep local client caches in-sync with the backend data collections.

Run this demo in my JavaScript Demos project on GitHub.

To get my feet wet, I wanted to start out with a List-Detail style experiment, using Firebase to power the data persistence tier of the demo. But, I didn't want the Firebase implementation to "leak" into the rest of the AngularJS application. As such, the entirety of Firebase is encapsulated within my "FriendService", which exposes data through Promises and scope-driven events.

Getting this all to work was a non-trivial task. It took about 3 days and I'm fairly certain that I'm not using Firebase in the way it was intended to be used. I have tried, more or less, to shoehorn Firebase into a RESTful mindset; meaning, I'm using Firebase to replace the $http service in AngularJS. And, while this works, I get the feeling that it misses the "point" of Firebase and its data synchronization magic.

The biggest hurdle in this experiment was understanding the timing of various events and callbacks. Unlike Deferred values in the Q library ($q in AngularJS), not everything in Firebase is asynchronous. And, those things that are asynchronous aren't necessarily asynchronous all of the time. A lot of the timing depends on the state of the local cache and type of request being made (ex, transactional).

This made it very hard to determine when and if an AngularJS digest needed to be triggered. Since Firebase is not part of the "AngularJS lifecycle", it's up to the developer to tell AngularJS when the View-Model has been (or may have been) updated via Firebase. Of course, since Firebase events are not consistently asynchronous, this leads to all sorts of "digest already in progress" errors. To deal with this, I had to lean heavily on the Scope.$evalAsync() method to safely trigger digests.

NOTE: This actually had a silver lining because $evalAsync() ended up triggering fewer digests than would have been triggered using $apply(); it allowed digests to be chunked around pre-value, "child_added" events.

The inconsistent timing of events also made it hard to understand why JavaScript variable references seemed to be defined in one context and then not in the next. Though, I will admit that this was aggravated by my "use" of JavaScript variable hoisting. And, when I stopped hoisting variables, these errors went away.

NOTE: You can't "stop hoisting" variables in JavaScript. In the above statement, what I mean is that I stopped depending on hoisting behavior.

Once I was able to wrap my head around the Firebase interactions and get those events encapsulated behind AngularJS promises, I was able to get the demo to work:

<!doctype html>
<html ng-app="Demo">
<head>
	<meta charset="utf-8" />

	<title>
		My First Look At Firebase In AngularJS
	</title>

	<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body ng-controller="AppController">

	<h1>
		My First Look At Firebase In AngularJS
	</h1>


	<!-- Show this if we're looking at the list of friends. -->
	<!-- BEGIN: List. -->
	<div
		ng-if="! id"
		ng-controller="ListController"
		ng-hide="isLoading">

		<h2>
			Friends
		</h2>

		<form ng-submit="submitFriend()">

			<input type="text" ng-model="form.name" size="30" />

			<input type="submit" value="Add Friend" />

		</form>

		<!-- If we have friends. -->
		<ul ng-if="friends.length">
			<li ng-repeat="friend in friends">

				<a ng-click="viewFriend( friend.id )">{{ friend.name }}</a>

			</li>
		</ul>

		<!-- Else-if we do not have friends. -->
		<p ng-if="! friends.length">
			<em>You have not added any friends; this saddens me.</em>
		</p>

	</div>
	<!-- END: List. -->


	<!-- Show this if we're looking at a given friend's detail. -->
	<!-- BEGIN: Detail. -->
	<div
		ng-if="id"
		ng-controller="DetailController"
		ng-hide="isLoading">

		<h2>
			Detail
		</h2>

		<h3>
			{{ friend.name }} &mdash; <em>( ID: {{ friend.id }} )</em>
		</h3>

		<p>
			Favorite number: {{ friend.favoriteNumber }}
		</p>

		<p>
			Added, with love, on {{ friend.createdAtLabel }}
		</p>

		<hr />

		<p>
			&laquo; <a ng-click="viewList()">Back to List</a>
		</p>

		<br />

		<p>
			<a ng-click="deleteFriend()">Delete {{ friend.name }}</a>
		</p>

	</div>
	<!-- END: Detail. -->


	<!-- Load scripts. -->
	<script type="text/javascript" src="../../vendor/jquery/jquery-2.1.0.min.js"></script>
	<script type="text/javascript" src="../../vendor/angularjs/angular-1.2.22.min.js"></script>
	<script type="text/javascript" src="../../vendor/firebase/firebase-1.1.0.min.js"></script>
	<script type="text/javascript">

		// Create an application module for our demo.
		var app = angular.module( "Demo", [] );


		// -------------------------------------------------- //
		// -------------------------------------------------- //


		// I control the root of the application.
		app.controller(
			"AppController",
			function( $scope, friendService ) {

				// I hold the unique ID of the friend that has been selected. The
				// selection of an ID will indicate the use of the Detail page, as
				// opposed to the list page. We're using this since we're not using a
				// full-fledged "route" approach for this simple demo.
				$scope.id = null;


				// ---
				// PUBLIC METHODS.
				// ---


				// I switch the Detail view for the given friend ID.
				$scope.viewFriend = function( newID ) {

					$scope.id = newID;

				};


				// I switch to the list view (of all the friends).
				$scope.viewList = function() {

					$scope.id = null;

				};

			}
		);


		// -------------------------------------------------- //
		// -------------------------------------------------- //


		// I control the list of friends.
		app.controller(
			"ListController",
			function( $scope, friendService ) {

				// I hold the model data for the form inputs.
				$scope.form = {};

				// I determine if the data is currently being loaded from the
				// remote resource.
				$scope.isLoading = false;

				// I hold the collection of friends.
				$scope.friends = [];

				// Since we want to try to keep our view synchronized, update the current
				// view whenever friends are added by other clients.
				// --
				// NOTE: These events will also be triggered by actions taken by _this_
				// client; as such, we need to take special care to not accidentally
				// duplicate our local response to said events.
				$scope.$on(
					"friendAdded",
					function( event, friend ) {

						if ( $scope.isLoading ) {

							return;

						}

						addFriendToList( friend );

					}
				);

				// Since we want to try to keep our view synchronized, update the current
				// view whenever friends are removed by other clients.
				// --
				// NOTE: These events will also be triggered by actions taken by _this_
				// client; as such, we need to take special care to not accidentally
				// duplicate our local response to said events.
				$scope.$on(
					"friendRemoved",
					function( event, friend ) {

						if ( $scope.isLoading ) {

							return;

						}

						removeFriendFromList( friend );

					}
				);

				// Initialize the data, pulling from the remote resource.
				loadRemoteData();


				// ---
				// PUBLIC METHODS.
				// ---


				// I save a new friend to the repository.
				$scope.submitFriend = function() {

					// Ignore empty form submissions.
					if ( ! $scope.form.name ) {

						return;

					}

					// To make the demo a bit more interesting, generate a favorite
					// number (in lieu of any meaningful form data).
					var name = $scope.form.name;
					var favoriteNumber = Math.floor( 100 * Math.random() );

					friendService.addFriend( name, favoriteNumber )
						.then(
							function handleAddFriendResolve( friend ) {

								addFriendToList( friend );

							},
							function handleAddFriendReject( error ) {

								alert( "Oops! We couldn't save your friend." );

							}
						)
					;

					// Clear form data.
					$scope.form.name = "";

				};


				// ---
				// PRIVATE METHODS.
				// ---


				// I add the given friend to the list, ensuring not to add the same
				// friend twice.
				function addFriendToList( friend ) {

					// Due to the order of events, we cannot be sure that our "save"
					// request will actually resolve before our "add" event is triggered;
					// as such, we will quickly run into handlers being invoked in
					// unexpected ways. To avoid list item duplication, we have to check
					// the state of the list before adding the new entity.
					if ( isAlreadyInList( friend ) ) {

						return;

					}

					$scope.friends.push( friend );

					sortFriends( $scope.friends );

				}


				// I apply the remote data to the local scope.
				function applyRemoteData( friends ) {

					$scope.friends = sortFriends( friends );

				}


				// I check to see if the given friend is already represented in the list,
				// as determined by unique IDs.
				function isAlreadyInList( friend ) {

					for ( var i = 0 ; i < $scope.friends.length ; i++ ) {

						if ( $scope.friends[ i ].id === friend.id ) {

							return( true );

						}

					}

					return( false );

				}


				// I load the remote data and then apply it to the local scope.
				function loadRemoteData() {

					$scope.isLoading = true;

					friendService.getFriends()
						.then(
							function handleGetFriendsResolve( friends ) {

								$scope.isLoading = false;

								applyRemoteData( friends );

							}
						)
					;

				}


				// I remove the given friend from the list, as determined by unique IDs.
				function removeFriendFromList( friend ) {

					for ( var i = 0 ; i < $scope.friends.length ; i++ ) {

						if ( $scope.friends[ i ].id === friend.id ) {

							return( $scope.friends.splice( i, 1 ) );

						}

					}

				}


				// I sort the given collection of friends based on case-insensitive name.
				// The collection is sorted in-place and a reference to the collection is
				// returned for convenience.
				function sortFriends( friends ) {

					friends.sort(
						function comparisonOperator( a, b ) {

							return(
								( a.name.toLowerCase() <= b.name.toLowerCase() )
									? -1
									: 1
							);

						}
					);

					return( friends );

				}

			}
		);


		// -------------------------------------------------- //
		// -------------------------------------------------- //


		// I control the friend detail.
		app.controller(
			"DetailController",
			function( $scope, friendService ) {

				// I determine if the data is currently being loaded from the
				// remote resource.
				$scope.isLoading = false;

				// I hold the friend being viewed.
				$scope.friend = null;

				// Initialize the data, pulling from the remote resource.
				loadRemoteData();


				// ---
				// PUBLIC METHODS.
				// ---


				// I delete the current friend from the friend repository.
				$scope.deleteFriend = function() {

					if ( $scope.isLoading ) {

						return;

					}

					// Delete the friend.
					friendService.deleteFriend( $scope.friend.id )
						.then(
							null,
							function handleDeleteFriendReject( error ) {

								alert( "Oops! For some reason, we couldn't delete your friend." );

							}
						)
					;

					// No need to wait for the request to be confirmed. We're going to
					// assume the "happy path" in the vast majority of cases and just
					// go back to the list, assuming the delete worked.
					$scope.viewList();

				};


				// ---
				// PRIVATE METHODS.
				// ---


				// I apply the remote data to the local scope.
				function applyRemoteData( friend ) {

					$scope.friend = augmentFriend( friend );

				}


				// I augment the friend for use in the local view model.
				function augmentFriend( friend ) {

					friend.createdAtLabel = new Date( friend.createdAt ).toString();

					return( friend );

				}


				// I load the remote data and then apply it to the local scope.
				// --
				// NOTE: The friend ID is being inherited from the top level controller.
				function loadRemoteData() {

					$scope.isLoading = true;

					friendService.getFriend( $scope.id )
						.then(
							function handleGetFriendResolve( friend ) {

								$scope.isLoading = false;

								applyRemoteData( friend );

							}
						)
					;

				}

			}
		);


		// -------------------------------------------------- //
		// -------------------------------------------------- //


		// I am the repository for friends (currently implemented with Firebase). But,
		// all public methods return a Promise so as to not let the Firebase
		// implementation and the mixed-synchronicity "leak" outside of the service
		// layer. All promises will be resolved or rejected asynchronously (in relation
		// to the calling context), thanks to $q.
		app.service(
			"friendService",
			function( $q, $rootScope, firebase ) {

				// NOTE: This is my "developer" Firebase, so there is no security
				// being applied to it.
				var root = "https://popping-torch-33.firebaseio.com/2014-10-09/";

				// I hold the firebase reference to the collection of friends.
				// --
				// NOTE: Once this reference is established, the data will start to sync
				// down to the local cache immediately (from what I can see, though it
				// may only synchronize when events are added?).
				var collectionResource = firebase( root + "friends" );

				// I hold the firebase reference to the primary key that will mimic our
				// auto-incrementing primary-key value. This will be updated using a
				// cross-client transaction (below).
				var pkeyResource = firebase( root + "friends-pkey" );

				// When a friend is added to the collection, announce the event to the
				// application (using scope inheritance for pub/sub).
				collectionResource.on(
					"child_added",
					function handleChildAdded( snapshot ) {

						// We can't simply turn around and broadcast this event inside
						// an $apply() for two reasons, due to the way Firebase works:
						//
						// 1) It would end up triggering a digest for EACH update in the
						// initial data for the local cache. When you get the resource,
						// it triggers "child_added" for each item as the data is
						// synchronized, which would trigger an unfortunate and
						// unnecessary number of digests.
						//
						// 2) We can't trigger an $apply() since we may already be inside
						// a digest. Due to the sometimes ASYCHNRONOUS, sometimes
						// SYNCHRONOUS nature of the way Firebase announces events, we
						// have to use an async evaluation in order to normalize the
						// approach and not raise digest errors
						// --
						// NOTE: Thankfully, this will only trigger one digest for the
						// initial data Get, not N-digests for N-friends.
						$rootScope.$evalAsync(
							function handleEvalAsync() {

								$rootScope.$broadcast( "friendAdded", snapshot.val() );

							}
						);

					}
				);

				// When a friend is removed from the collection, announce the event to
				// the application (using scope inheritance for pub/sub).
				collectionResource.on(
					"child_removed",
					function handleChildRemoved( snapshot ) {

						// NOTE: We have to use evalAsync. See "child_added" above.
						$rootScope.$evalAsync(
							function handleEvalAsync() {

								$rootScope.$broadcast( "friendRemoved", snapshot.val() );

							}
						);

					}
				);

				// Return the public API.
				return({
					addFriend: addFriend,
					deleteFriend: deleteFriend,
					getFriend: getFriend,
					getFriends: getFriends
				});


				// ---
				// PUBLIC METHODS.
				// ---


				// I add a friend with the given name and favorite number. Returns a
				// promise that will resolve to the newly created friend object.
				function addFriend( name, favoriteNumber ) {

					// Get the next primary key (enforced uniqueness via transaction),
					// before saving the friend.
					var promise = getNextPrimaryKey()
						.then(
							function handlePrimaryKeyResolve( nextAvailableID ) {

								var friend = {
									id: nextAvailableID,
									name: name,
									favoriteNumber: favoriteNumber,
									createdAt: ( new Date() ).getTime()
								};

								var deferred = $q.defer();

								// Save the friend to the path at the recently transacted
								// ID. This way, we are using our own "auto-incrementing"
								// value instead of the Firebase UUID.
								// --
								// NOTE: The interesting part here is that the
								// "child_added" event will execute SYNCHRONOUSLY but the
								// success callback will execute ASYNCHRONOUSLY. I believe
								// this is because the event relates to the local cache
								// while the callback is for the data synchronization?
								collectionResource.child( nextAvailableID ).set(
									friend,
									function handleSynchronizationComplete( error ) {

										if ( error ) {

											deferred.reject( error );

										} else {

											deferred.resolve( friend );

										}

									}
								);

								return( deferred.promise );

							}
						)
					;

					return( promise );

				}


				// I delete the friend with the given ID. Returns a promise.
				function deleteFriend( id ) {

					var deferred = $q.defer();

					// NOTE: This will trigger the "child_removed" event. Now, unlike
					// the "child_added" event, this will trigger when working in the
					// browser's offline mode, even if the data synchronization callback
					// does not complete. That said, if you are working in online mode,
					// then the "child_removed" event will be triggered SYNCHRONOUSLY
					// while the data synchronization callback will be invoked
					// ASYNCHRONOUSLY.
					collectionResource.child( id ).remove(
						function handleSynchronizationComplete( error ) {

							if ( error ) {

								deferred.reject( error );

							} else {

								deferred.resolve();

							}

						}
					);

					return( deferred.promise );

				}


				// I get the friend with the given ID. Returns a promise.
				function getFriend( id ) {

					var deferred = $q.defer();

					// Get the friend with the given ID (as it translates to a path in
					// the parent resource). Unlike the SET/REMOVE methods, the success
					// callback may be invoked SYNCHRONOUSLY. If the data is available in
					// the local cache, then it will be invoked immediately. If, however,
					// the data is not in the local cache, the callback will be invoked
					// ASYNCHRONOUSLY when the resource has been synchronized.
					collectionResource.child( id ).once(
						"value",
						function handleSuccess( snapshot ) {

							deferred.resolve( snapshot.val() );

						},
						function handleError( error ) {

							// Client does not have permission to read this data.
							deferred.reject( error );

						}
					);

					return( deferred.promise );

				}


				// I get all the friends in the collection. Returns a promise.
				function getFriends() {

					var deferred = $q.defer();

					// Get all the children in the resource. As with the getFriend()
					// method above, the success callback may be invoked ASYNCHRONOUSLY
					// or SYNCRHONOUSLY, depending on the state of local cache.
					collectionResource.once(
						"value",
						function handleSuccess( snapshots ) {

							var friends = [];

							snapshots.forEach(
								function handleIteration( snapshot ) {

									friends.push( snapshot.val() );

								}
							);

							deferred.resolve( friends );

						},
						function handleError( error ) {

							deferred.reject( error );

						}
					);

					return( deferred.promise );

				}


				// ---
				// PRIVATE METHODS.
				// ---


				// I get the next unique ID in the resource. This acts as our auto-
				// incrementing primary key that we're used to seeing in a relational
				// database backend.
				function getNextPrimaryKey() {

					var deferred = $q.defer();

					// Get the next available incremented value. Because the transaction
					// has to ensure an uncompromised value, the success handler will
					// always have to be invoked asynchronously (my theory).
					// --
					// NOTE: The "update" method may be invoked several times before the
					// complete handler is called.
					pkeyResource.transaction(
						function handleUpdate( currentValue ) {

							return( ( currentValue || 0 ) + 1 );

						},
						function handleComplete( error, isCommitted, snapshot ) {

							// The transaction has two ways of failing - error and failure
							// to commit. Failure to commit won't really be a problem in
							// this scenario since our update method never returns an
							// undefined value. But, leaving the logic in here since I am
							// still learning about Firebase.
							var transactionAborted = ! isCommitted;

							if ( error || transactionAborted ) {

								deferred.reject( error || "Could not increment primary key." );

							} else {

								deferred.resolve( snapshot.val() );

							}

						}
					);

					return( deferred.promise );

				}

			}
		);


		// -------------------------------------------------- //
		// -------------------------------------------------- //


		// Define our Firebase gateway so that we can inject it into other services
		// for synchonization with remote data stores.
		app.factory(
			"firebase",
			function( $window ) {

				// Create our factory which will create a new instance of the Firebase
				// reference for the given path.
				function firebase( resourcePath ) {

					return( new firebase.Firebase( resourcePath ) )

				}

				// Keep a reference to the original object in case we need to reference
				// it later (ex, in the factory method).
				firebase.Firebase = $window.Firebase;

				// Delete from the global scope to make sure no one cheats in our
				// separation of concerns. The "angular way" is to not use the global
				// scope and to inject all needed dependencies.
				delete( $window.Firebase );

				// Return our factory method.
				return( firebase );

			}
		);

	</script>

</body>
</html>

You may notice that I am explicitly generating the unique ID to be used with each Friend record. To do this, I am using a secondary Firebase reference to a "primary key" value, which my code increments before saving a new Friend. At first, I tried to use the UUID-style IDs that Firebase auto-generates when you push a new value onto a Firebase collection. But, those were hard to use since I had to copy the Firebase ID into the model every time I went to pass data out of the service tier. This worked, but was a hassle and didn't look very attractive. The explicit ID was more upfront work (using a Firebase transaction), but made the rest of the demo feel more natural.

This was a challenging but fun dive into Firebase. So far, I have heard nothing but good things about the service, so I was eager to give it a try. Getting up and running had quite a bit of a learning curve. And, now that I've gotten my feet wet, I'll try to check out other people's code for architectural insights. I think I may have missed the mark on how Firebase was intended to be integrated; but, at least I'm more familiar with the API now.

Want to use code from this post? Check out the license.

Reader Comments

2 Comments

Nice write up, Firebase gets pretty fun and the security rules are like solving logic puzzles, lol. Have you integrated their JAR with Coldfusion yet?

I am thinking of having Coldfusion push to Firebase and then have our clients get websocket updates from fire base but still hit our REST API for any
Creates, Updates or Deletes. (With fall back to our rest api of course)

---if you have not already---
Take a look at these resources:
https://gist.github.com/katowulf/6158392
https://github.com/firebase/angularfire-seed

2 Comments

Thank you for sharing this information mate... This will help a lot since I am facing some troubles with firebase due to the fact that the view gets a bit of lag when loading information from firebase.

2 Comments

Hello again Ben,

I know this post is kind of old, but I am using your example for a personal project. I have a problem with it, and its about the id you use to retrieve an instance of a "friend" object from firebase. Is that field an internal field that firebase uses internally or is a field you created for this purpose? I am trying to do the same with my "products" table and I am unable to retrieve any data using the id attribute nor a custom field (productId).

Thank you

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel