Building a Designer News Clone with AngularJS and Firebase
I recently discovered Firebase, and it's changed everything.
If you haven't heard of it, Firebase is a platform that allows you to store and sync data in realtime. It essentially gives you the functionality of a backend database, but presented in a much simpler format, as it is NoSQL, and only using front-end code.
You can create some surprisingly complex applications with it, so I decided to try and make something myself. I am relatively a beginner with both AngularJS and Firebase, so I settled on making something simple, a clone of Designer News.
You can actually see and use the application here, and view the code on github.
Project Overview #
Although I can't go through the entire application, I will share how I organised the data, and how some specific actions were handled.
The Data Structure #
The structure of the data is incredibly important, and I spent a while trying to figure out the best way to do it so that I would have access to all the relevant data from certain controllers. In the end, I grouped the data into two main arrays - stories and users.
Each user in the user array has the following data -
- first_name
- last_name
- title
- email
- id (uid from Firebase login via email)
- karma
- posts
- post
- title
- date
- id (Firebase key for post under "stories" array)
- comments
- comment
- title (post title)
- id (Firebase key for post under "stories" array)
- date
- comment (the text)
Each story in the stories array has the following data -
- title
- date
- url (null if description post)
- description (null if url post)
- category
- voteCount
- commentCount
- voters (an array with uid of each person who has voted on the post)
- comments
- comment
- date
- text
- voteCount
- user
- first_name
- last_name
- title
- id (Firebase key for the user)
- uid (uid from Firebase login via email)
- voters (an array with uid of each person who has voted on the comment)
Creating a user #
Creating a user is relatively simple with Firebase. There is a built in email and password authentication service, which handles everything for you. Creating a new user is as simple as writing -
var ref = new Firebase(FIREBASE_URL);
// Passing a "user" object
ref.createUser({
email: user.email,
password: user.password
})
When the user is created, they are given a token, the "uid". Along with the other information given by the user, I pass in the "uid" to a new user in the users array.
var ref = new Firebase(FIREBASE_URL + '/users');
var users = $firebaseArray(ref);
// Passing a "user" object
users.$add({
uid: uid,
first_name: user.first_name,
last_name: user.last_name,
title: user.title,
email: user.email,
karma: 0
});
This creates a connection between the user created by the Firebase authentication service, which is referenced by the uid, and the user in the users array, which is referenced by the array key.
When a user is created, we can check for the current user by looping through the users array and finding the user with the same uid as the current authorised user -
auth.$onAuth(function(authUser) {
if (authUser) {
users.$loaded().then(function(){
angular.forEach(users, function(user) {
if (user.uid == authUser.uid) {
$rootScope.currentUser = user;
}
}); // end loop
}); // end users.$loaded
} // end if
});
Adding a new story #
When adding a new story, I had to make sure that I added the details to the "stories" array, but also to the story author's posts in the "users" array.
Adding a new url story looked roughly like this -
var ref = new Firebase(FIREBASE_URL + '/stories');
var stories = $firebaseArray(ref);
var userRef = new Firebase(FIREBASE_URL + '/users/' + $rootScope.currentUser.$id + '/posts');
var thisUser = $firebaseArray(userRef);
$scope.addStory = function(story) {
// Checks for errors go here
// If no errors
stories.$add({
title: story.title,
url: story.url,
category: storyCategory, // custom function used to get category
description: null,
date: Firebase.ServerValue.TIMESTAMP,
user: {
first_name: $rootScope.currentUser.first_name,
last_name: $rootScope.currentUser.last_name,
title: $rootScope.currentUser.title,
uid: $rootScope.currentUser.uid,
id: $rootScope.currentUser.$id
},
commentCount: 0,
voteCount: 0
}).then(function(ref){
thisUser.$add({
title: story.title,
date: Firebase.ServerValue.TIMESTAMP,
id: ref.key()
})
});
}
Voting #
The voting system, although a relatively small part of the application, had a lot of moving parts. It worked like this -
- Check if user has voted by looping through the array of voters
- If has voted, stop here
- If not, allow voting
- Add 1 to current voteCount
- Add the current user to the list of voters on the post
- Add 1 to the story author's karma
$scope.upvote = function(story) {
var thisStoryRef = new Firebase(FIREBASE_URL + '/stories/' + story.$id);
var thisStory = $firebaseObject(thisStoryRef);
var votersRef = new Firebase(FIREBASE_URL + '/stories/' + story.$id + '/voters');
var voters = $firebaseArray(votersRef);
var hasVoted = false;
// Check if user has voted by looping through the array of voters
voters.$loaded().then(function() {
angular.forEach(voters, function(object, id) {
if (object.$value == $rootScope.currentUser.uid) {
hasVoted = true;
}
})
}).then(function() {
// If has voted, stop here
if (hasVoted) {
$scope.alertMessage = {
message: 'You have already voted on this post!',
type: 'warning'
};
// If not, allow voting
} else {
// Add 1 to current voteCount
thisStory.voteCount++;
thisStory.$save();
// Add the current user to the list of voters on the post
voters.$add($rootScope.currentUser.uid);
var storyAuthorRef = new Firebase(FIREBASE_URL + '/users/' + story.user.id);
var storyAuthor = $firebaseObject(storyAuthorRef);
// Add 1 to the story author's karma
storyAuthor.$loaded().then(function() {
storyAuthor.karma++;
storyAuthor.$save();
})
}
}) // end .then from voters.loaded
};
More to do #
This was just an insight into how I handled some of the functions of the application. If you want to check out the full code, you can view it on my github. I was playing around with the idea of making a screencast where I go through how I did everything. If that's something you would be interested in, let me know.
Although the core functionality it works, it is definitely not production ready. I still have a lot of messages being logged to the console. In the future I want to work more on handling the error messages more effectively, especially related to authentication.
As always, I would like any feedback I can get on this, so leave a comment below!
Part 2 #
Read part 2 of this series, 'Implementing Firebase Security'