Your progress isn't saved!
Sign in and we'll remember exactly where you left off the next time you visit - on any of your devices.
Sign up or sign in with

AngularJS Tutorial: Learn to Rapidly Build Real-time Web Apps with Firebase


Introduction

In response to the feedback from our original tutorial, we decided to take the new version of the tutorial in an entirely new direction. Mucking around in backend code doesn't belong in an AngularJS tutorial, and we've found an elegant solution to this problem.

This tutorial will guide you through the process of creating a frontend-only application. Firebase’s firebase.js and angularFire.js libraries offer an entirely new flavor of application: an app built with no backend server or code. The only parts of this application are the AngularJS app, the Firebase data store, and the minimal backend server to deliver the assets to the browser. The tutorial features step-by-step instructions on how to build a fantasy football application, code snippets of the full application, and explanations on design decisions.

Our intention with this tutorial is to provide the AngularJS community with instructions on how to design and build an ultra-modern application. Firebase is not yet a complete backend server replacement, but it doesn’t intend to be. What it does offer is entirely new: a veritable three-way data binding solution that makes real-time data presentation and handling trivial.

The application you are building will go beyond basic use of AngularJS, and we will attempt to explore as much of the framework as possible. Additionally, we will use Firebase in a number of interesting ways in order to demonstrate its power and flexibility. We also feel strongly about maintaining modernity in a tutorial, so we will keep it congruent with AngularJS as the framework and community matures. This tutorial is built on top of AngularJS v1.2.0rc3.

The tutorial is a living thing, a work in progress. We are constantly extending the tutorial and making changes and corrections. If you find errata, think something should be changed, or would like to suggest an improvement or new section, we would love to hear from you.

Source code and PDF eBook

This tutorial is provided to you free of charge on the site. We built this in the interest of advancing the AngularJS framework and community.

We encourage you to purchase the source code and PDF of this tutorial to help fund our continued efforts in building more material and features for Thinkster. Your money literally goes towards paying our rent and food for the next few months while we make the tutorial even more awesome!

All users who purchase the tutorial will have free access to new version of the up-to-date source code and PDF as they are released. The purchase link above gives you the final source code and PDFs for both of our tutorials.

You have 100% ownership over the PDF and source code and can use them however you like.

Prerequisites

This tutorial assumes you already have a working knowledge of AngularJS. Throughout, there will be references to parts of the "A Better Way to Learn AngularJS" curriculum if you need to clarify or refresh on a certain subject. We recommend going through the entire curriculum before beginning this tutorial.

The Stack

This application is built on a stack that defies convention. The server the application is served off of exists only to deliver assets. It has no framework, and is not connected to any database. The frontend application you will write talks to Firebase directly.

We decided to use Firebase because it is more in the spirit of AngularJS and single page application frameworks in general.

What is fantasy football?

Fantasy football is centered around creating a “fantasy” team out of real life NFL players, and pitting your fantasy team against other teams. Groups of users, usually 8-12, will create their own teams as part of a “league”, and will compete against other individuals within that league.

Individuals in a fantasy football league will choose real players in the American National Football League (NFL) for their teams, and these teams will face each other weekly in a one-on-one matchup during actual NFL games. Players in the real games perform actions that score points in fantasy football, and whichever fantasy team scores more points in that matchup wins. Teams with the best win/loss records enter fantasy playoffs, and an eventual champion is selected.

Makeup of a fantasy team

There are 32 NFL teams, and your fantasy team will consist of players from some of these teams.

There are lots of different positions on a NFL team, but for fantasy football purposes, we simplify the different positions greatly: all you have to worry about is Quarterback (QB), Runningback (RB), Wide Receiver (WR), Tight End (TE), Kicker (K), and Defense/Special Teams (D/ST). Defense/Special Teams is a special position on your roster, it represents the entire Defense and Special Teams units, which are made up of many players, for one of the 32 teams. All other players you select will be individual players.

How players score points isn’t important right now, you can worry about that later. For those of you familiar with fantasy football, this application will operate under standard scoring rules.

Fantasy teams will have 16 members, but only 9 of them will actually count towards scoring in the weekly matchup against another fantasy team. The other 7 will remain on the team’s “bench”, and any points they score will not count towards your team’s total that week. Your team, and every team in the league, will select players in a “fantasy draft” - more about that later.

Your fantasy team’s 9-player active roster will have 1 Quarterback (QB), 2 Runningbacks (RB), 2 Wide Receivers (WR), 1 Tight End (TE), 1 Flex (which can either be a RB, WR, or TE), 1 Kicker (K), and 1 Defense/Special Teams (D/ST).

An example 16-man roster might look like the following:

Active Roster

QB: Aaron Rodgers (GB)

RB: Adrian Peterson (MIN)

RB: Arian Foster (HOU)

WR: Calvin Johnson (DET)

WR: A.J. Green (CIN)

TE: Jimmy Graham (NO)

FLEX: Ray Rice (BAL)

K: Stephen Gostkowski (NE)

D/ST: Seattle Seahawks

Bench

TE: Rob Gronkowski (NE)

RB: Marshawn Lynch (SEA)

WR: Brandon Marshall (CHI)

QB: Matt Ryan (ATL)

RB: C.J. Spiller (BUF)

K: Blair Walsh (MIN)

D/ST: Chicago Bears (CHI)

Recap

Hopefully some of that stuck, but if a lot of it went over your head, don’t worry. As you build the application, you will begin to understand much more clearly how fantasy football works. Let’s get started!

Getting Started With Firebase

We’ve provided the starting point for the application on github: https://github.com/msfrisbie/thinkster-firebase-tutorial 

Clone the application with git clone https://github.com/msfrisbie/thinkster-firebase-tutorial.git

The files are served off of a basic Node server, so you’ll need node.js: http://howtonode.org/how-to-install-nodejs

Next, go to https://www.firebase.com and sign up for an account

Go to https://www.firebase.com/account/ and create a Firebase. Remember the name you use for the Firebase, you’ll use it in a second.

With all this set up, have everything you need to get started! You won’t be able to create an account or login yet, as you haven’t linked it to your Firebase yet. You first need to initialize the connection to your Firebase.

At the end of app/js/config.js, change the .constant() line and add in your Firebase URL:

app/js/config.js
Stop scrolling to load code snippet
  // your Firebase URL goes here
  // should look something like: https://blahblah.firebaseio.com
  .constant('FBURL', '<<FIREBASE_URL_GOES_HERE>>')

Great! You should now be able to boot the skeleton of the application. 

Start the server from the app/ directory with node scripts/web-server.js

Navigate to 'localhost:8000/app/index.html' to see it in action!

What Am I Actually Dealing With Here?

Before you actually get your hands dirty, familiarize yourself with what is provided in the skeleton application.

App Directory

All the files we care about on the frontend will live in this folder. 

Stop scrolling to load code snippet
thinkster-firebase-tutorial/
├── app
│   ├── css
│   │   └── app.css
│   ├── img
│   ├── index.html
│   ├── js
│   │   ├── app.js
│   │   ├── config.js
│   │   ├── controllers
│   │   │   ├── headercontroller.js
│   │   │   ├── signincontroller.js
│   │   │   └── signupcontroller.js
│   │   └── services
│   │       ├── loginservice.js
│   │       └── profilecreator.js
│   ├── lib
│   │   ├── angular
│   │   ├── angular-bootstrap
│   │   ├── angular-cookies
│   │   ├── angular-fire
│   │   ├── angular-mocks
│   │   ├── angular-resource
│   │   ├── angular-route
│   │   ├── angular-scenario
│   │   ├── bootstrap
│   │   ├── firebase
│   │   ├── jquery
│   │   └── json3
│   └── views
│       ├── default.html
│       ├── header.html
│       └── users
│           ├── signin.html
│           └── signup.html
├── bower.json
├── config
│   ├── karma.conf.js
│   ├── karma-e2e.conf.js
│   └── security-rules.json
├── logs
├── README.md
├── scripts
│   ├── e2e-test.bat
│   ├── e2e-test.sh
│   ├── playerimport.html
│   ├── test.bat
│   ├── test.sh
│   ├── watchr.rb
│   └── web-server.js
└── test
    ├── e2e
    │   ├── runner.html
    │   └── scenarios.js
    ├── lib
    │   └── angular
    │       ├── angular-mocks.js
    │       ├── angular-scenario.js
    │       └── version.txt
    └── unit
        ├── configSpec.js
        ├── controllersSpec.js
        ├── directivesSpec.js
        ├── filtersSpec.js
        └── servicesSpec.js

Your entire AngularJS app will live in the ./js folder, your views will live in the /views folder, and Bower will dump all the library files in the /lib folder. The server will send index.html to the browser, and all other views that are used in the app will be requested by Angular as needed.

Bower

Bower is an incredibly robust package manager. If you’ve used Yeoman before to generate an AngularJS skeleton, Bower is included in that package. All packages that your application needs, including Angular and all its dependencies, Bootstrap, jQuery, and Firebase can be acquired and managed with this automation tool. 

The skeleton you cloned off GitHub comes with all the bower packages already in place, but if you wanted to use bower, you would run ‘bower install’ from the /app directory.

Read up on bower here. Follow the instructions listed there to install bower.

With all the packages automatically fetched, you can just refer to them where they live from index.html.

Getting Into AngularJS

All of your AngularJS application files will live in the app/js directory.  

app.js declares the application with all its dependencies

config.js defines the routes, initializes the angularFireAuth module, and declares your Firebase URL as a constant

The controllers/ directory contains all your application controllers, separated into their own files. You will continue this separate file convention as the application grows.

The services/ directory contains all your application services, following the same file convention as controllers.

The directives/ and filters/ directories are empty, but they will contain all your custom Angular directives and filters.

Authentication with Firebase

Of course, it is infeasible to entrust all of authentication to the client, as we must assume the client has control of every aspect of the execution environment. Firebase offers its own flavor of authentication which is available to us through the angularFireAuth module.

Firebase Simple Login allows for authentication through Facebook, Twitter, Github, or even custom authentication solutions. Here, though, we only need an email/password pair for each individual user, and Firebase’s Simple Login allows for this too.

app/js/config.js
Stop scrolling to load code snippet
'use strict';

// Declare app level module which depends on filters, and services
angular.module('fantasyApp.config', [])

app.config(['$routeProvider', 
    function($routeProvider) {
      $routeProvider
      .when('/',        { templateUrl: 'views/default.html' })
      .when('/signin',  { templateUrl: 'views/users/signin.html' })
      .when('/signup',  { templateUrl: 'views/users/signup.html' })
      .otherwise(       { redirectTo: '/' });
    }])
  
  // establish authentication
  .run(['angularFireAuth', 'FBURL', '$rootScope', 
    function(angularFireAuth, FBURL, $rootScope) {
      angularFireAuth.initialize(new Firebase(FBURL), {scope: $rootScope, name: 'auth', path: '/signin'});
      $rootScope.FBURL = FBURL;
    }])

In config.js, we see that when angularFireAuth is initialized, we pass it ‘auth’ as the name attribute. This means that, when the user is logged in, the user authentication object will be attached to the $scope.auth object. When the user is logged out, the $scope.auth attribute will not exist.

Bear in mind that this does not mean we can just use this for all user data. The Firebase auth object stores the email and password combination, nothing more. If we want to store additional information about the user, we will need to create a separate “model” that is keyed with the authentication ID. 

angularFireAuth will emit an event on login, logout, and on error: “angularFireAuth:login”, “angularFireAuth:logout”, and “angularFireAuth:error”, respectively. Obviously, these events will fire when the client encounters each of these angularFireAuth actions. In addition, if the user session has not expired, this event will fire every time there is a new application load - so if you perform a full reload of a page after logging in, the event will fire again when the page is loaded so you can handle authentication state functionality in a semi-synchronous manner.

The existing code for login/logout is more or less boilerplate, most of it is trivial to understand from mere examination. However, knowing the basic Firebase authentication components is essential.

The User Model

Now, as mentioned before, angularFireAuth only serves to identify the current user, and doesn’t hold any information about them (for example, their name). Returning to the signin/signup controllers, we see that the $scope.login() method invokes a login() method on the loginService, which was injected to the controller. The service:

app/js/services/loginservice.js
Stop scrolling to load code snippet
'use strict';

angular.module('fantasyApp.services.login', ['fantasyApp.services.profileCreator'])
  .factory('loginService', ['angularFireAuth', 'profileCreator', '$location', '$rootScope',
    function(angularFireAuth, profileCreator, $location, $rootScope) {
      return {
        login: function(email, pass, redirect, callback) {
          var p = angularFireAuth.login('password', {
            email: email,
            password: pass,
            rememberMe: true
          });
          p.then(function(user) {
            if( redirect ) {
              $location.path(redirect);
            }
            callback && callback(null, user);
          }, callback);
        },
        logout: function(redirectPath) {
          angularFireAuth.logout();
          if(redirectPath) {
            $location.path(redirectPath);
          }
        },
        createAccount: function(name, email, pass, callback) {
          angularFireAuth._authClient.createUser(email, pass, function(err, user) {
            if(callback) {
              callback(err, user);
              $rootScope.$apply();
            }
          });
        },
        createProfile: profileCreator
      }
    }])

Most of the code here, as mentioned before, is merely wrapping the angularFireAuth authentication methods. It exposes the login(), logout() and createAccount() methods, which all function as you would expect.

Next, let’s move to the process of account creation. This is the method in the signup controller that is executed to create a new user:

app/js/controllers/signupcontroller.js
Stop scrolling to load code snippet
      $scope.createAccount = function() {
        if( !$scope.email ) {
          $scope.err = 'Please enter an email address';
        }
        else if( !$scope.pass ) {
          $scope.err = 'Please enter a password';
        }
        else {
          loginService.createAccount($scope.name, $scope.email, $scope.pass, function(err, user) {
            if( err ) {
              $scope.err = err;
            }
            else {
              $scope.login(function(err) {
                if( !err ) {
                  loginService.createProfile(user.id, $scope.name, user.email);
                }
              });
            }
          });
        }
      };

When a user is creating an account,  the createAccount() method in signupcontroller.js will invoke the loginService.createAccount() method, which uses angularFireAuth to create a new user with an email/password pair. After angularFireAuth successfully handles the creation of a new account, the controller will pass this new user object, along with all the User model attributes from the account creation form, to the loginService.createProfile() method.

app/js/services/profilecreator.js
Stop scrolling to load code snippet
'use strict';

angular.module('fantasyApp.services.profileCreator', [])
  .factory('profileCreator', ['Firebase', 'FBURL', '$rootScope', function(Firebase, FBURL, $rootScope) {
    return function(id, name, email, callback) {
      new Firebase(FBURL).child('users/'+id).set({email: email, name: name}, function(err) {
        if( callback ) {
          callback(err);
          $rootScope.$apply();
        }
      });
    }
  }]);

Although this service is shorter, it’s more important because we are seeing an instance of Firebase binding for the first time. The returned function creates a Firebase reference to the location of the specific user (which doesn’t exist yet), and inserts the email and name data there. On success, it invokes the function(err) {} callback.

I know, there’s a lot to digest there. Let’s work through it piece by piece.

Firebase Data

In the profileCreator service, you saw the following line:

app/js/services/profilecreator.js
Stop scrolling to load code snippet
new Firebase(FBURL).child('users/'+id).set({email: email, name: name}, function(err) { … })

As described in the article above, this creates a reference to Firebase data. It could be a string, it could be the parent object of a tall tree of object attributes, it doesn't even have to exist. It serves to expose a set of methods on this location which can perform CRUD operations, traverse the data hierarchy, or any number of things.

This particular line creates a reference to the root Firebase instance. The child() method shifts the reference to the users/id location, which doesn't exist yet. It invokes the set() method to pass an object to be written to that location. 

Firebase Forge

One awesome thing about Firebase is the Forge. It is a GUI that you can use in a browser to view and edit the data in your Firebase, as well as a ton of other things. Once you’re logged in to the Firebase website, you can visit the Forge for one of your Firebases by just directing your browser to the Firebase URL you put into the config.js file: <firebasename>.firebaseio.com.

Here, you will be able to see your user data be inserted in real time. The Firebase Forge is a surprisingly powerful tool, especially when debugging. There are many features of the Forge that we will get to later.

Security and Permissions with Firebase

One other piece of functionality that AngularJS cannot feasibly handle by itself is data access permissions. For the same reasons that a client cannot assess by itself whether or not a user is who they say they are, the client also cannot be trusted to assess whether or not data can be accessed on Firebase. Enter the Firebase security API.

Read through the blog post and watch the video introducing the Firebase Security API

Inside the Firebase Forge, the “Security” section allows you to define your security rules for data access. The rules are written in quasi-JSON syntax, and make it very easy to validate data and prevent unwanted access. The default you’ll see there is a read-all, write-all permission, but we’ll be changing that later.

Getting Your Hands Dirty

Awesome! We now have everything we need to start digging into the meat of the application.

In the Firebase Forge, navigate to the ‘Auth’ section. Under the ‘Authentication Providers’ header, select the ‘Email & Password’ option, and check ‘Enable’. 

This will allow account creation in this Firebase. Under ‘Registered Users’, you’ll be able to manage the email/password pairs that represent individual accounts.

Create an account, and log in to the application. 

You can’t do anything yet besides login and logout, but let’s go ahead and start writing some code. If you poke around a bit, you might have noticed that, when you’re logged in, you can still navigate to the #/signin and #/signout routes, and see the forms. This isn't horrible, but we would prefer to prevent the user from seeing these at all when they’re logged in to avoid confusion. 

To do this, we’ll use the $scope.auth variable we mentioned before to check if the user is logged in. If they are, we’ll just redirect them to the main page. 

Do this for both the Signup controller and the Signin controller as shown:

app/js/controllers/signupcontroller.js
Stop scrolling to load code snippet
'use strict';

angular.module('fantasyApp.controllers.signup', [])
  .controller('SignupCtrl', ['$scope', 'loginService', '$location',
    function($scope, loginService, $location) {

      if (!!$scope.auth) {
        $location.path('/');
      }

      $scope.$on('angularFireAuth:login', function () {
        $location.path('/');
      })
app/js/controllers/signincontroller.js
Stop scrolling to load code snippet
'use strict';

angular.module('fantasyApp.controllers.signin', ['fantasyApp.services.login'])
  .controller('SigninCtrl', ['$scope', 'loginService', '$location',
    function($scope, loginService, $location) {

      if (!!$scope.auth) {
        $location.path('/');
      }

      $scope.$on('angularFireAuth:login', function () {
        $location.path('/');
      })

Now that we have the user data available to us, let’s put it to use. Check out headercontroller.js:

app/js/controllers/headercontroller.js
Stop scrolling to load code snippet
'use strict';

angular.module('fantasyApp.controllers.header', ['fantasyApp.services.login'])
  .controller('HeaderController', ['$scope', '$location', 'loginService', 'angularFire', 'FBURL', 
    function($scope, $location, loginService, angularFire, FBURL) {

      $scope.$on('angularFireAuth:login', function() {

      });

      $scope.logout = function() {
        loginService.logout('/signin');
      };

      $scope.navbarEntries = [
      ];
    }])

Let’s focus on the ‘angularFireAuth:login’ event, which currently invokes an empty function. We want to display the user’s name in the header when they’re logged in, and we know where to get it now - we can construct a reference to it as we did in the profileCreator service. This brings us to the angularFire library for the first time.

angularFire In The Hole!

It’s easy to get confused on how angularFire.js and firebase.js are used together, but the distinction is a critically important one. 

angularFire will bind the data in Firebase to a scope model, which in turn is bound to the view. This offers an effective 3-way data binding, where the data in the view is a real-time representation of the data that is in your Firebase. The binding listens for the Firebase data change events, and updates the models accordingly. Best of all, we don’t have to worry about any of this.

Let’s use angularFire for the first time. In header.html, you can see that when a user is logged in, they will just see the placeholder instead of their own. We’re going to define the user model on the $scope right after this, so interpolate the name attribute of the user.

app/views/header.html
Stop scrolling to load code snippet
      <a href="#" class="dropdown-toggle" data-toggle="dropdown">
        {{ user.name }}<b class="caret"></b>
      </a>

Obviously, the user model isn’t doing anything yet, so let’s change that. Inside the angularFireAuth:login event callback function, bind the user data in your Firebase to the user model on the scope:

app/js/controllers/headercontroller.js
Stop scrolling to load code snippet
      $scope.$on('angularFireAuth:login', function() {
        angularFire(new Firebase(FBURL+'/users/'+$scope.auth.id), $scope, 'user');
      });

angularFire() takes three parameters: the reference to the data that we want bound to the model, the $scope object we are binding to, and the stringified version of the model name we are binding to. Once this is invoked, the Firebase data will be updated in real time, and we can always assume that the data displayed to the user is correct.

This introduces an interesting design pattern that will persist throughout the rest of this tutorial. Unlike traditional CRUD applications, where one is involved in the tedium of handoffs of data from server to frontend, the user is always viewing and editing the data as it exists everywhere, for all users.

With the angularFire() call in place, you should now be able to see your name in the header of the application. You can test the realtime data binding by changing your name in the Firebase Forge editor, and then seeing it change in the browser.

Building Out The Application

Now that you have authentication all taken care of, let’s move on to building out the application.

The 32 NFL teams and the data about them is about as static as data gets, so instead of serving this from the server every time, it makes more sense to just provide them in a service. Players and other entities that refer to teams will just provide a team index, and the Angular service will take care of the rest.

At this point, it’s important to establish a naming convention for your application. Since ‘team’ could refer to either a NFL team or a fantasy team, you will refer to one as NFLTeam, and one as FantasyTeam in all directories, files, and code.

Writing the NFL Service

Create a new service file, nflservice.js:

app/js/services/nflservice.js
Stop scrolling to load code snippet
angular.module('fantasyApp.services.nfl', [])
  .factory('NFL', function() {
    var NFL = {};
    NFL.teams = [
      {"abbr":"ARI", "team":"Arizona",        "mascot": "Cardinals",  "conference":"NFC", "division": "West"},
      {"abbr":"ATL", "team":"Atlanta",        "mascot": "Falcons",    "conference":"NFC", "division": "South"},
      {"abbr":"BAL", "team":"Baltimore",      "mascot": "Ravens",     "conference":"AFC", "division": "North"},
      {"abbr":"BUF", "team":"Buffalo",        "mascot": "Bills",      "conference":"AFC", "division": "East"},
      {"abbr":"CAR", "team":"Carolina",       "mascot": "Panthers",   "conference":"NFC", "division": "South"},
      {"abbr":"CHI", "team":"Chicago",        "mascot": "Bears",      "conference":"NFC", "division": "North"},
      {"abbr":"CIN", "team":"Cincinnati",     "mascot": "Bengals",    "conference":"AFC", "division": "North"},
      {"abbr":"CLE", "team":"Cleveland",      "mascot": "Browns",     "conference":"AFC", "division": "North"},
      {"abbr":"DAL", "team":"Dallas",         "mascot": "Cowboys",    "conference":"NFC", "division": "East"},
      {"abbr":"DEN", "team":"Denver",         "mascot": "Broncos",    "conference":"AFC", "division": "West"},
      {"abbr":"DET", "team":"Detroit",        "mascot": "Lions",      "conference":"NFC", "division": "North"},
      {"abbr":"GB",  "team":"Green Bay",      "mascot": "Packers",    "conference":"NFC", "division": "North"},
      {"abbr":"HOU", "team":"Houston",        "mascot": "Texans",     "conference":"AFC", "division": "South"},
      {"abbr":"IND", "team":"Indianapolis",   "mascot": "Colts",      "conference":"AFC", "division": "South"},
      {"abbr":"JAC", "team":"Jacksonville",   "mascot": "Jaguars",    "conference":"AFC", "division": "South"},
      {"abbr":"KC",  "team":"Kansas City",    "mascot": "Chiefs",     "conference":"AFC", "division": "West"},
      {"abbr":"MIA", "team":"Miami",          "mascot": "Dolphins",   "conference":"AFC", "division": "East"},
      {"abbr":"MIN", "team":"Minnesota",      "mascot": "Vikings",    "conference":"NFC", "division": "North"},
      {"abbr":"NE",  "team":"New England",    "mascot": "Patriots",   "conference":"AFC", "division": "East"},
      {"abbr":"NO",  "team":"New Orleans",    "mascot": "Saints",     "conference":"NFC", "division": "South"},
      {"abbr":"NYG", "team":"New York",       "mascot": "Giants",     "conference":"NFC", "division": "East"},
      {"abbr":"NYJ", "team":"New York",       "mascot": "Jets",       "conference":"AFC", "division": "East"},
      {"abbr":"OAK", "team":"Oakland",        "mascot": "Raiders",    "conference":"AFC", "division": "West"},
      {"abbr":"PHI", "team":"Philadelphia",   "mascot": "Eagles",     "conference":"NFC", "division": "East"},
      {"abbr":"PIT", "team":"Pittsburgh",     "mascot": "Steelers",   "conference":"AFC", "division": "North"},
      {"abbr":"SD",  "team":"San Diego",      "mascot": "Chargers",   "conference":"AFC", "division": "West"},
      {"abbr":"SEA", "team":"Seattle",        "mascot": "Seahawks",   "conference":"NFC", "division": "West"},
      {"abbr":"SF",  "team":"San Francisco",  "mascot": "49ers",      "conference":"NFC", "division": "West"},
      {"abbr":"STL", "team":"St. Louis",      "mascot": "Rams",       "conference":"NFC", "division": "West"},
      {"abbr":"TB",  "team":"Tampa Bay",      "mascot": "Buccaneers", "conference":"NFC", "division": "South"},
      {"abbr":"TEN", "team":"Tennessee",      "mascot": "Titans",     "conference":"AFC", "division": "South"},
      {"abbr":"WAS", "team":"Washington",     "mascot": "Redskins",   "conference":"NFC", "division": "East"}
    ];
    NFL.positions = [
      {"abbr":"QB",  "pos":"Quarterback"},
      {"abbr":"RB",  "pos":"Runningback"},
      {"abbr":"WR",  "pos":"Wide Receiver"},
      {"abbr":"TE",  "pos":"Tight End"},
      {"abbr":"K",   "pos":"Kicker"},
      {"abbr":"D/ST","pos":"Defense/Special Teams"}
    ];
    return NFL;
  });

Obviously, this returns an object with 'teams' and 'positions' arrays, static data that the application will use often.

Writing the NFL Controller

Next, create a new controller, nflcontroller.js:

app/js/controllers/nflcontroller.js
Stop scrolling to load code snippet
'use strict';

angular.module('fantasyApp.controllers.nfl', ['fantasyApp.services.nfl'])
  .controller('NFLController', ['$scope','$routeParams','NFL',
    function($scope, $routeParams, NFL) {
      $scope.nflteams = NFL.teams;
      $scope.nflteam = NFL.teams[$routeParams['nflTeamId']];
    }]);

This controller will be used to view all the teams, as well as individual teams.

Includes and Dependencies

You still need to include these new controller and service files with the rest of the application, so add the paths to app/views/index.html

app/views/index.html
Stop scrolling to load code snippet
    <script src="http://static.firebase.com/v0/firebase.js"></script>
    <script src="https://cdn.firebase.com/v0/firebase-simple-login.js"></script>
    <script src="lib/angular/angular.js"></script>
    <script src="lib/angular-bootstrap/ui-bootstrap-tpls.min.js"></script>
    <script src="lib/angular-route/angular-route.min.js"></script>
    <script src="lib/angular-fire/angularFire.js"></script>

    <script src="js/app.js"></script>
    <script src="js/config.js"></script>

    <script src="js/controllers/headercontroller.js"></script>
    <script src="js/controllers/signincontroller.js"></script>
    <script src="js/controllers/signupcontroller.js"></script>
    <script src="js/controllers/nflcontroller.js"></script>

    <script src="js/services/loginservice.js"></script>
    <script src="js/services/profilecreator.js"></script>
    <script src="js/services/nflservice.js"></script>

Since you are also declaring these as modules, you must add them to the module dependencies in app/js/app.js:

app/js/app.js
Stop scrolling to load code snippet
'use strict';

// Declare app level module which depends on filters, and services
var app = angular.module('fantasyApp',
  [ 'fantasyApp.config'
  , 'fantasyApp.controllers.header'
  , 'fantasyApp.controllers.signin'
  , 'fantasyApp.controllers.signup'
  , 'fantasyApp.controllers.nfl'
  , 'firebase', 'ui.bootstrap', 'ngRoute']
  )

Routing

Now add routes for each of these views. The view templates will be app/views/nfl/list.html and app/views/nfl/view.html:

app/js/config.js
Stop scrolling to load code snippet
'use strict';

// Declare app level module which depends on filters, and services
angular.module('fantasyApp.config', [])

app.config(['$routeProvider', 
    function($routeProvider) {
      $routeProvider
      .when('/',                    { templateUrl: 'views/default.html' })
      .when('/signin',              { templateUrl: 'views/users/signin.html' })
      .when('/signup',              { templateUrl: 'views/users/signup.html' })
      .when('/nflteams',            { templateUrl: 'views/nfl/list.html'
                                    , authRequired: true })
      .when('/nflteams/:nflTeamId', { templateUrl: 'views/nfl/view.html'
                                    , authRequired: true })
      .otherwise(                   { redirectTo: '/' });
    }])
  
  // establish authentication
  .run(['angularFireAuth', 'FBURL', '$rootScope', 
    function(angularFireAuth, FBURL, $rootScope) {
      angularFireAuth.initialize(new Firebase(FBURL), {scope: $rootScope, name: 'auth', path: '/signin'});
      $rootScope.FBURL = FBURL;
    }])

  // your Firebase URL goes here
  // should look something like: https://blahblah.firebaseio.com
  .constant('FBURL', '<<yourfirebasename>>.firebaseio.com')

Notice that :nflTeamId exists for the individual team route, and the string value of this in the URL will be available as $routeParams[‘nflTeamId’] in the controller once $routeParams is injected.

It’s possible in Angular to define the top-level controller for a view partial in the routing. It’s generally best to avoid this, as it causes you to have to check an additional file to find the controller for a view, as opposed to being able to ascertain it directly from the template. Declaring the controller in the routing seems cleaner, but very infrequently will it benefit you. Just define the controller in your templates.

In the routes, the authRequired attribute functions exactly as you would expect: if the user has not authenticated via angularFireAuth, or their auth token has expired, they will be unable to access that route, and will instead be redirected to the path which you defined when you initialized angularFireAuth in config.js.

Views

Create a new directory app/views/nfl/, and in it, create a list.html view:

app/views/nfl/list.html
Stop scrolling to load code snippet
<div data-ng-controller="NFLController">
  <h2>NFL Teams</h2>
  <div data-ng-repeat="nflteam in nflteams">
    <a href="#/nflteams/{{ $index }}">{{ nflteam.team }} {{ nflteam.mascot }}</a>
  </div>
</div>

Here, you are introduced to the ng-repeat directive for the first time. nflteams is the collection, nflteam is the iterator, and each instance in nflteams will render the ng-repeat HTML, here an <a> tag inside a <div>. $index is simply the 0-based index of iterator, which here is the ID of the NFL team.

Also, create view.html:

app/views/nfl/view.html
Stop scrolling to load code snippet
  <div data-ng-controller="NFLController">
    <h2>{{ nflteam.team }} {{ nflteam.mascot }}</h2>
    <p><strong>Abbreviation: </strong>{{ nflteam.abbr }}</p>
    <p><strong>Conference: </strong>{{ nflteam.conference }}</p>
    <p><strong>Division: </strong>{{ nflteam.division }}</p>
    <p><a href="#/nflteams">All NFL teams</a></p>
  </div>

Hopefully nothing surprising here.

With all of this, you should have your first working pages in your application! Run the server, and navigate to localhost:8000/#/nflteams to try it out.

Building Out The Navbar

You’re going to get sick of typing in routes manually pretty quickly, so let’s add in some links to the various pages. 

Add the following in headercontroller.js:

app/js/controllers/headercontroller.js
Stop scrolling to load code snippet
      $scope.navbarEntries = [
        {
          "title": "NFL Teams",
          "link": "/nflteams"
        }
      ];

We will use this array to add in new links to the navbar as we build out the pages. 

Next, in views/header.html, add in the <li> repeater on the navbarEntries array:

app/views/header.html
Stop scrolling to load code snippet
<div class="navbar-inner ng-scope" data-ng-controller="HeaderController">
  <ul class="nav">
    <li><a class="brand" href="#/">ngFantasyFootball</a></li>
    <li data-ng-repeat="entry in navbarEntries" data-ng-show="auth" >
      <a href="#{{entry.link}}">{{entry.title}}</a>
    </li>
  </ul>

With this, we have working navbar buttons, but we’d like to take advantage of the “active” class to show if that section of the application is currently active. 

Add an ng-class entry to the <li> repeater in the header:

app/views/header.html
Stop scrolling to load code snippet
    <li data-ng-repeat="entry in navbarEntries" data-ng-show="auth" data-ng-class="{ active: entry.isActive }">

Read up more on the ng-class directive here

Here, the “active” class is added when entry.isActive evaluates to true. 

Next, you’ll add a listener to the header controller scope to set these array attributes every time the route changes:

app/js/controllers/headercontroller.js
Stop scrolling to load code snippet
      $scope.navbarEntries = [
        {
          "title": "NFL Teams",
          "link": "/nflteams"
        }
      ];

      $scope.$on('$routeChangeSuccess', function() {
        $scope.navbarEntries.forEach(
          function(data) {
            data.isActive = ($location.path().indexOf(data.link) == 0);
          }
        )
      })

Read up more on ngRoute here

The above snippet will invoke the provided function each time the Angular route is successfully changed. Each time, it will iterate through the navbarEntires entities and set the isActive attribute based on whether or not the route starts with that string.

Angular View Anti-Pattern

This implementation might seem a bit strange to you, and it should. Why, might you ask, wouldn't you just call a method from the view, which would assess if it should be active or not?

The reason is the $digest cycle re-evaluation. This cycle occurs very, very often, and every method call you use in your view to assess property will be re-invoked every single cycle, which will obviously degrade performance as the application scales. Using a variable allows angular to avoid this re-evaluation. In this context, the variable only needs to be recalculated every time the route changes, so doing it with the $routeChangeSuccess event allows us to limit the calculation to that single event.

Playing With Firebase

Now we can start getting serious with Firebase. The first thing you’ll build is the League infrastructure.

League Routing

Update the routes in config.js to match the following:

app/js/config.js
Stop scrolling to load code snippet
'use strict';

// Declare app level module which depends on filters, and services
angular.module('fantasyApp.config', [])

app.config(['$routeProvider', 
    function($routeProvider) {
      $routeProvider
      .when('/',                        { templateUrl: 'views/default.html' })
      .when('/signin',                  { templateUrl: 'views/users/signin.html' })
      .when('/signup',                  { templateUrl: 'views/users/signup.html' })
      .when('/nflteams',                { templateUrl: 'views/nfl/list.html', authRequired: true })
      .when('/nflteams/:nflTeamId',     { templateUrl: 'views/nfl/view.html', authRequired: true })
      .when('/leagues',                 { templateUrl: 'views/leagues/list.html', authRequired: true })
      .when('/leagues/create',          { templateUrl: 'views/leagues/edit.html', authRequired: true })
      .when('/leagues/:leagueId',       { templateUrl: 'views/leagues/view.html', authRequired: true })
      .when('/leagues/:leagueId/edit',  { templateUrl: 'views/leagues/edit.html', authRequired: true })
      .otherwise(                       { redirectTo: '/' });
    }])

League Services

Next, let’s build out some of the services that we’ll for fantasy league functionality.

Create the FireRef service, firerefservice.js:

app/js/services/firerefservice.js
Stop scrolling to load code snippet
'use strict';

angular.module('fantasyApp.services.firebaseRefs', [])
  .factory('FireRef', ['FBURL', 'Firebase',
    function(FBURL, Firebase) {
      return {
        leagues: function() {
          return new Firebase(FBURL+'/leagues');
        }
      , users: function() {
          return new Firebase(FBURL+'/users');
        }
      }
    }])

Basically, everywhere in the application you inject Firebase and FBURL, it will be to construct references. Since we can chain the .child() method to extend references, this service will be used for all the base references, and therefore we only need to do the injection here, and we can in turn inject this service to wherever we need Firebase refs.

Make sure and include firerefservice.js in your index.html file.

Next, create the Leagues service, leagueservice.js:

app/js/services/leagueservice.js
Stop scrolling to load code snippet
'use strict';

angular.module('fantasyApp.services.leagues', ['fantasyApp.services.firebaseRefs'])
  .factory('Leagues', ['angularFireCollection', 'FireRef',
    function(angularFireCollection, FireRef) {
      return {
        collection: function(cb) {
          return angularFireCollection(FireRef.leagues(),cb);
        }
      , find: function(leagueId) {
          return FireRef.leagues().child('/'+leagueId);
        }
      , create: function(league, commissioner) {
         return FireRef.leagues().push({
            name: league.name,
            commissionerId: commissioner.id,
            fantasyTeams: []
          }).name();
        }
      , removeLeague: function(leagueId) {
          var league = FireRef.leagues().child('/'+leagueId)
          league.remove();
        }
      }
    }])

Make sure and include leagueservice.js in your index.html file.

There’s a lot to dissect here. find() shouldn’t be surprising to you, it simply uses the Firebase reference API to reference a child. You should also be able to intuit what removeLeague() does from what you know about references and from the Firebase API documentation. The next sections will discuss collection() and create().

angularFireCollection

When first exposed to angularFireCollection, it seems a bit unwieldy until you properly understand how to use it. angularFireCollection binds the $scope to a collection of attributes, and manages the events that occur within that collection. The attributes are normally of the same type. The $scope attribute that is bound to the collection can be used in the view in the same way as an array, and as elements in the collection are added, removed, and changed, the angularFireCollection will manage the alterations seamlessly.

It's worth mentioning here that an angularFireCollection is not something you would or should iterate through in the controller. In the real-time data paradigm, the concept of 'iterating' breaks down when you consider that all data is subject to addition, removal, and alteration at any time. The angularFireCollection's purpose is to cleanly project a real-time collection onto the view.

In the Leagues service, the collection() method is merely returning the angularFireCollection, which here will exist as a collection of all leagues in the Firebase. We will later assign the returned collection to the scope in the controller.

The create() method is important to understand. When the push() method is used on a reference, the object passed to it will be inserted at that location. The ‘id’ for that object is automatically generated by Firebase, and will look something like this: -J54kg6u7BKj5FXSEdQ7. From the Firebase documentation: “The unique name generated by push( ) is prefixed with a client-generated timestamp so that the resulting list will be chronologically-sorted.”

With these methods, we can now create, read, update, and destroy leagues as we please. If you’re paying attention, you might have noticed that nothing above serves to update the league. This is intentional, and will become clear in a moment.

League Controller

Now, build out leaguescontroller.js:

app/js/controllers/leaguescontroller.js
Stop scrolling to load code snippet
'use strict';

angular.module('fantasyApp.controllers.leagues', ['fantasyApp.services.leagues'])
  .controller('LeaguesController', ['$scope','$routeParams', '$location', 'angularFire', 'Leagues', 
    function($scope, $routeParams, $location, angularFire, Leagues) {

      $scope.league = {};
      $scope.leagueId = $routeParams.leagueId;

      $scope.findLeagues = function() {
        $scope.leagues = Leagues.collection();
      }

      $scope.findOneLeague = function (leagueId) {
        if(!!$scope.leagueId) {
          angularFire(Leagues.find($routeParams.leagueId), $scope, 'league')
        }
      }

      $scope.createLeague = function() {
        var leagueId = Leagues.create($scope.league, $scope.auth);
        $scope.league = null;
        $location.path('/leagues/'+leagueId);
      }

      $scope.removeLeague = function(leagueId) {
        Leagues.removeLeague(leagueId);
      }
    }])

If you understood the service we just wrote, then everything here should be no surprise. findLeagues() attaches the angularFireCollection for the /leagues ref to $scope.leagues. findOneLeague() binds the league data for the leagueId specified in the route. For createLeague(), recall that the create() method on the Leagues service returns the unique ID that Firebase allocated for the data. This passes the league data from the form (which we have yet to create) to Firebase for a write, and it will redirect to the route of the league that was just created. removeLeague() simply invokes the service method of the same name.

Remember to include the leagues controller file in index.html, as well as add the module as a dependency in app.js.

League Views

Next, we’re going to create edit.html, list.html, and view.html. 

Create a new directory, app/views/leagues/, for the new views

view.html:

app/views/leagues/view.html
Stop scrolling to load code snippet
  <div data-ng-controller="LeaguesController" data-ng-init="findOneLeague()">
    <h2>{{ league.name }}</h2>
    <p><strong>Commissioner: </strong>{{ league.commissionerId }}</p>
    <br>
    <a href="#/leagues">View all leagues</a>
  </div>

list.html

app/views/leagues/list.html
Stop scrolling to load code snippet
 <div data-ng-controller="LeaguesController" data-ng-init="findLeagues()">
  <h2>Leagues</h2>
  <h3>All Leagues</h3>
  <ul class="unstyled">
    <li data-ng-repeat="league in leagues">
      <a href="#/leagues/{{ league.$id }}">{{league.name}}</a>
      (<a href="#/leagues/{{ league.$id }}/edit">Edit</a>)
      (<a href="" data-ng-click="removeLeague('{{ league.$id }}')" >Remove</a>)
    </li>
  </ul> 
  <br>
  <a href="#/leagues/create">Create a new league</a>
</div>

edit.html:

app/views/leagues/edit.html
Stop scrolling to load code snippet
<div data-ng-controller="LeaguesController" data-ng-init="findOneLeague()">
  <form class="form-horizontal">
    <div class="control-group">
      <label class="control-label" for="name">Name</label>
      <div class="controls">
        <input type="text" data-ng-model="league.name" id="name" placeholder="Name">
      </div>
    </div>
    <div class="control-group">
      <div class="controls">
        <button data-ng-click="createLeague()" data-ng-show="!leagueId">Create League</button>
      </div>
    </div>
    <div class="control-group">
      <div class="controls">
        <a href="#/leagues">View all leagues</a>
      </div>
    </div>
  </form>
</div>

In each view, the ng-init directive invokes the method listed there before the view renders. These view are relatively trivial, and most of what they contain should be unsurprising to you. Notice that the ng-show conditional on the ‘Create League’ button lets us reuse the edit view for creating and editing.

Feelin’ Fine About Firebase

With these views, you have all the pieces now to go and create leagues in Firebase. By now, hopefully you’ve realized why an update view isn’t necessary. The answer, of course, is that there is no ‘update’ action that we have to write. When you go to edit a league, you are looking at the real time data in the Firebase. If you watch the Firebase Forge, you’ll be able to see the data be updated there as you change it in the browser. Similarly, if you create a second user account and have it observe the list view, the new user will be able to see the list of leagues update in real time, both with additional entries and editing. 

This is the power of Firebase!

But Wait, There’s More: The Firebase Security API

We are able to define custom security rules for our data by using the Firebase Forge. 

First, go through this Firebase blog post for a good introduction to Firebase security

You’re probably starting to get an idea of why the Firebase Security API is so powerful. To gain a better understanding, let’s figure out the rules required to enforce the following conditions: 

We want the users to be logged in when they read all data, and create and edit a league; we want the league to only be editable by the person who created it (the commissioner), we want to prevent users spoofing other users onto leagues, and we want to ensure that the league has a name and commissioner.

The Firebase security rules correspond to how our data is structured in the Firebase. Firebase gives us access to certain values in the rules. ‘auth’ is the authenticated user, ‘data’ is a data snapshot of the current data object at that position, and ‘newData’ is a data snapshot of the new data object being written to that position. Additionally, we are able to use three different types of rules: .read, .write, and .validate. With these, we are able to define robust data security and validation rules for our application, and they can be edited on the fly in the Firebase Forge editor. 

Make sure you have read through the rules documentation

The following is the set of rules that will properly implement the above conditions:

Firebase Rules
Stop scrolling to load code snippet
{
  "rules": {
    ".read": "auth != null",
    "users": {
      ".write": "auth != null"
    },
    // OH SNAP A COMMENT
    "leagues": {
      "$league": {
        ".write": "auth != null && (data.child('commissionerId').val() == auth.id || data.child('commissionerId').val() == null)",
        ".validate": "newData.hasChildren(['commissionerId','name'])",
        "commissionerId": {
          ".validate": "auth.id == newData.val()"
        },
        "name": {
          ".validate": "newData.val() != ''"
        }
      }
    }
  }
}

It’s worth mentioning here that, if you are editing the security rules in the Firebase Forge editor (which I definitely recommend), you must hit ‘Save Rules’ for them to be applied.

Rules, Rules, Rules

We’re granting read access to everything in the Firebase if the user has authenticated, accomplished with the simple rule expression “auth != null”. For “users”, we simply prevent writing user data unless the user is logged in. Obviously this is not sufficient to totally protect users, but since we haven’t yet built out an interface to change user data, this will suffice for now.

With “leagues”, it starts to get interesting. The Firebase Security API allows for a catch-all security filter by prefacing the attribute name with a $ (what follows the '$' doesn't matter). This will apply the nested rules to all data in that location that is not specifically defined otherwise. Here, we grant write access to all leagues if 1) the user is authenticated, and 2) if the existing data is owned by the current user, OR there is no existing data yet. 

The Firebase Security API documentation puts the difference between write access and validation perfectly: “For a write to be allowed, only one .write rule needs to evaluate to true, but all .validate rules for the new data being written must pass.”

Cutting To The Chase

With this, we can dissect the league object and specify each piece exactly how we want it. For the whole league object, we use the hasChildren() validator to check that all fields exist. Then, for each individual piece “commissionerId” and “name”, we can specify individual rules. “commissionerId” must match the current authenticated user id, and “name” must not be an empty string.

One final thing worth mentioning that is awesome about the Firebase Security API is that, since it is a pseudo-JSON format, you can have comments, as demonstrated above. As you can imagine, this is terribly useful when authoring complex security rules.

With this, we have built an effective layer of data security for the application. You can test these security rules by either playing with the application in the client, or by using the Firebase Forge simulator.

Tying Up a Few Loose Ends

If you played around with the application, you might have noticed that when a validation fails, the way we have written the application does not handle this gracefully. We need to modify a few things:

Change the Leagues service to accept a callback:

app/js/services/leagueservice.js
Stop scrolling to load code snippet
      , create: function(league, commissioner, cb) {
         return FireRef.leagues().push({
            name: league.name,
            commissionerId: commissioner.id,
            fantasyTeams: []
          }, cb).name();
        }

Change the Leagues controller to pass a callback to the service call:

app/js/controllers/leaguescontroller.js
Stop scrolling to load code snippet
      $scope.createLeague = function() {
        var leagueId = Leagues.create($scope.league, $scope.auth, function(err) {
          if (!err) {
            $scope.league = null;
            $location.path('/leagues/'+leagueId);
            $scope.$apply();
          }
        });
      }

Simply, this is now handling the Firebase write properly: as an asynchronous event with a callback. When Firebase does not return an error, it will clear the scope league variable and redirect. The $location.path() redirect will not occur on a library callback, so we need to use $scope.$apply() to ensure this happens.

Finally, let’s add Leagues to our header, since we how have that completely working:

app/js/controllers/headercontroller.js
Stop scrolling to load code snippet
      $scope.navbarEntries = [
        {
          "title": "NFL Teams",
          "link": "/nflteams"
        }
      , {
          "title": "Leagues",
          "link": "/leagues"
        }
      ];

Awesome! You now have completely functioning functionality for Fantasy leagues, complete with security, authentication, and best of all, no backend!

Building the Player Picker

Don't Hate the Player, Hate the Game

Fantasy football is nothing without players, so let’s build this out next. Included in the repository in the scripts/ directory is playerimport.html. Our application is nothing without players, so let’s import some:

First, add a security rule that allows writing to players:

Stop scrolling to load code snippet
{
  "rules": {
    ".read": "auth != null",
    "users": {
      ".write": "auth != null"
    },
    "players": {
      ".write": true
    },

Open playerimport.html in your browser, enter your Firebase name, and click the import button to add the sample list of players to your Firebase under the /players location.

Once you make sure the player data made it into your Firebase, you should disable writing to players, since we won’t be updating this data:

Stop scrolling to load code snippet
{
  "rules": {
    ".read": "auth != null",
    "users": {
      ".write": "auth != null"
    },
    "players": {
      ".write": false
    },

Now let’s start cranking out some player infrastructure. 

Player Service

Add the players reference to the FireRef service:

app/js/scripts/firerefservice.js
Stop scrolling to load code snippet
'use strict';

angular.module('fantasyApp.services.firebaseRefs', [])
  .factory('FireRef', ['FBURL', 'Firebase',
    function(FBURL, Firebase) {
      return {
        leagues: function() {
          return new Firebase(FBURL+'/leagues');
        }
      , users: function() {
          return new Firebase(FBURL+'/users');
        }
      , players: function() {
          return new Firebase(FBURL+'/players');
        }
      }
    }])

Next, create the Player service:

app/js/services/playerservice.js
Stop scrolling to load code snippet
'use strict';

angular.module('fantasyApp.services.players', ['fantasyApp.services.firebaseRefs'])
  .factory('Players', ['FBURL', 'Firebase', 'angularFireCollection', 'FireRef',
    function(FBURL, Firebase, angularFireCollection, FireRef) {
      return {
        collection: function() {
          return angularFireCollection(FireRef.players());
        }
      , find: function(playerId) {
          return FireRef.players().child(playerId);
        }
      }
    }])

There’s nothing here you haven’t seen before, we’re simply exposing the angularFireCollection of all players, and the reference to a single player.

Add the Player service to index.html

Player Controller

Next, create the Player controller:

app/js/controllers/playerscontroller.js
Stop scrolling to load code snippet
'use strict';

angular.module('fantasyApp.controllers.players', ['fantasyApp.services.players'])
  .controller('PlayersController', ['$scope','$routeParams', 'angularFire', 'NFL', 'Players',
    function ($scope, $routeParams, angularFire, NFL, Players) {

      $scope.positions = NFL.positions;
      $scope.nflteams = NFL.teams;
      $scope.searchsize = {
        "limit": 10
      }
      $scope.strictsearch = {};

      $scope.findPlayers = function() {
        $scope.players = Players.collection();
      }

      $scope.findOnePlayer = function() {
        angularFire(Players.find($routeParams.playerId), $scope, 'player');
      }
    }]);

Again, this is more or less identical to the Leagues controller, save for that we are initializing some data that we are going to use in a bit.

Add the Players controller to index.html, and list it as a dependency in app.js

Player Routes and Views

Next, add the routes that we’re going to use:

app/js/config.js
Stop scrolling to load code snippet
      .when('/players',                 { templateUrl: 'views/players/list.html', authRequired: true })
      .when('/players/:playerId',       { templateUrl: 'views/players/view.html', authRequired: true })

Next, create the two views listed in the routes in a new players/ directory:

view.html:

app/views/players/view.html
Stop scrolling to load code snippet
<div data-ng-controller="PlayersController" data-ng-init="findOnePlayer()">
  <h2><span data-ng-show="player.num">#{{ player.num }}</span> {{ player.name }}</h2>
  <p><strong>Team: </strong>{{ nflteams[player.team].team }} {{ nflteams[player.team].mascot }}</p>
  <p><strong>Position: </strong>{{ positions[player.pos].pos }}</p>
</div>

list.html:

app/views/players/list.html
Stop scrolling to load code snippet
<div data-ng-controller="PlayersController" data-ng-init="findPlayers()">
  <table>
    <tr>
      <th>Player</th>
      <th>Position</th>
      <th>Team</th>
    </tr>
    <tr data-ng-repeat="player in players">
      <td>{{ player.name }}<span data-ng-show="player.num"> - #{{ player.num }}</span></td>
      <td>{{ positions[player.pos].abbr }}</td>
      <td>{{ nflteams[player.team].abbr }}</td>
    </tr>
  </table>
</div>

These views should be unsurprising to you. If you’ve properly implemented all the steps, you should see the player data, positions, and teams for all players in your Firebase.

Constructing the Player Filter

You surely don’t want to see all the hundreds of players at once, so let’s build a filtering mechanism into the player picker. 

Change your list.html to match the following, adding in three ng-includes and the player filter:

app/views/players/list.html
Stop scrolling to load code snippet
<div data-ng-controller="PlayersController" data-ng-init="findPlayers()">
  <ul class="navlist">
    <li><input type="text" data-ng-model="search.name" placeholder="Player name"></li>
    <li data-ng-include="'views/partials/nflteamselect.html'"></li>
    <li data-ng-include="'views/partials/positionselect.html'"></li>
    <li data-ng-include="'views/partials/limitselect.html'"></li>
  </ul>
  <table>
    <tr>
      <th>Player</th>
      <th>Position</th>
      <th>Team</th>
    </tr>
    <tr data-ng-repeat="player in players | filter:search | limitTo:searchsize.limit">
      <td>{{ player.name }}<span data-ng-show="player.num"> - #{{ player.num }}</span></td>
      <td>{{ positions[player.pos].abbr }}</td>
      <td>{{ nflteams[player.team].abbr }}</td>
    </tr>
  </table>
</div>

Create the three partials listed under ng-include directives in a new /partials directory. We’ll be reusing these later, this is the reason for the use of ng-include.

nflteamselect.html:

app/views/partials/nflteamselect.html
Stop scrolling to load code snippet
<select data-ng-model="search.team">
  <option value="">All NFL teams</option>
  <option data-ng-repeat="nflteam in nflteams" value="{{$index}}">{{nflteam.team}}</option>
</select>

positionselect.html:

app/views/partials/positionselect.html
Stop scrolling to load code snippet
<select data-ng-model="search.pos">
  <option value="">All positions</option>
  <option data-ng-repeat="position in positions" value="{{$index.toString()}}">{{position.abbr}}</option>
</select>

limitselect.html:

app/views/partials/limitselect.html
Stop scrolling to load code snippet
<select data-ng-model="searchsize.limit">
  <option value="10">10</option>
  <option value="25">25</option>
  <option value="50">50</option>
</select>

The search object will be taken from the positions and nflteams dropdowns, as well as the text input. The object’s attributes will be matched up against those in the players objects, and matching ones will be filtered out. You are now also using a limit, taken from the searchsize.limit dropdown.

If you did everything properly, you should now be able to filter players by name, position, team, and limit the number of results. Give yourself a pat on the back, this is pretty cool.

Corner Cases

If you play around with the player picker enough, you might have noticed that filtering by team doesn’t QUITE work correctly. Selecting, for example, to filter for players on Arizona returns players on Arizona, Detroit, New York, and Tennessee. This is because the indexes of these teams are 0, 10, 20, and 30, and you are searching for teams against the index ‘0’. Angular’s returns all results with a ‘0’ in their team index, as this is technically a match. You must create a custom filter that will return values with exact matches, not matching substrings.

AngularJS v1.1.5 introduced the comparator option for filters, which allows the user to use either presets, or a custom comparison function, to determine what the filter will return. For your purposes, passing in ‘true’ will require identical values for a match to be recognized. The comparator defaults to the substring match that you are using now.

Read more on ngFilter in the API documentation

Refactor

Each comparator value that is applied to a filter is applied to the entire search object. You don’t want to use exact string matches for the player names, only for the position and NFL team dropdowns. Thus, you will need to separate them out into two search objects, and filter separately.

Return to your directive templates and attach them to a new ‘strictsearch’ model:

positionselect.html:

app/views/partials/positionselect.html
Stop scrolling to load code snippet
<select data-ng-model="strictsearch.pos">
  <option value="">All positions</option>
  <option data-ng-repeat="position in positions" value="{{$index.toString()}}">{{position.abbr}}</option>
</select>

nflteamselect.html:

app/views/partials/nflteamselect.html
Stop scrolling to load code snippet
<select data-ng-model="strictsearch.team">
  <option value="">All NFL teams</option>
  <option data-ng-repeat="nflteam in nflteams" value="{{$index}}">{{nflteam.team}}</option>
</select>

Now you can chain the filters with separate objects, applying different comparators to each one. Note that the comparator argument defaults to ‘false’  

Modify list.html as follows:

app/views/players/list.html
Stop scrolling to load code snippet
    <tr data-ng-repeat="player in players | filter:search | filter:strictsearch:true | limitTo:searchsize.limit">
      <td>{{ player.name }}<span data-ng-show="player.num"> - #{{ player.num }}</span></td>
      <td>{{ positions[player.pos].abbr }}</td>
      <td>{{ nflteams[player.team].abbr }}</td>
    </tr>
  </table>
</div>

With this, you are now able to filter the players correctly by team, position, and name.

Add in a players button to the header:

app/js/controllers/headercontroller.js
Stop scrolling to load code snippet
      , {
          "title": "Players",
          "link": "/players"
        }

What's Next

You now have learned many of the basic building blocks necessary to put together an AngularJS/Firebase application. Everything beyond this point is implementation of advanced topics on top of what you have just learned.

Implementing Fantasy Teams

Next, we’re going to work on building out fantasy teams functionality. Initially, we’re going to with a very simple fantasy teams model: every fantasy team will have exactly one owner, will belong to exactly one league, and have a string name. The model will grow in complexity as we move further through the tutorial, but this is a good place to start, as it brings several critical Firebase concepts into the spotlight.

Denormalized Data

As a primer, read through this Firebase blog post 

In the context of real-time data, the concept of ‘where’ queries don’t make sense anymore. We still have collections of objects with various attributes, but there is no search mechanism to gather a subset with certain properties. As mentioned in the blog post, the correct way to implement associations is for there to be 2-way references between every associated object that will be queried against the referenced object.

So how will this work with leagues and fantasy teams? 

Recall that every fantasy team will be owned by a single league, and that a league will have many fantasy teams. This is a typical one-to-many association that would be trivial to implement in MySQL or MongoDB in their respective patterns. In Firebase, though, we need to consider which queries we will want to execute, and make the proper associations. We will want to know which fantasy teams are in a league, and we will want to know which league a fantasy team belongs to. Moreover, we will need to accomplish these operations without retrieving every single fantasy team and league on the frontend and searching there. 

This, as you might have surmised, is accomplished with denormalization. Specifically, every league will have to contain data referencing which fantasy teams are inside it, and every fantasy team will have to contain data referencing which league it belongs to. As you would expect, this is done with the unique Firebase IDs of the data. 

We will also want to provide the same model for users/fantasy teams, and this is implemented in the same way as leagues, so we will just build them out simultaneously.

We can flap our gums until we’re blue in the face, but it’s not as good as actually writing the code, so let’s get to it.

Fantasy Team Service

Add fantasy teams to the reference service:

app/js/services/firerefservice.js
Stop scrolling to load code snippet
      , fantasyTeams: function() {
          return new Firebase(FBURL+'/fantasyTeams');
        }

Create the FantasyTeam service:

app/js/services/fantasyteamservice.js
Stop scrolling to load code snippet
'use strict';

angular.module('fantasyApp.services.fantasyTeams', [])
  .factory('FantasyTeams', ['$q', 'angularFireCollection', 'FireRef',
    function($q, angularFireCollection, FireRef) {
      return {
        collection: function(cb) {
          return angularFireCollection(FireRef.fantasyTeams(),cb);
        }
      , find: function(fantasyTeamId) {
          return FireRef.fantasyTeams().child(fantasyTeamId);
        }
      , create: function(fantasyTeam, owner, cb) {
          var deferred = $q.defer();
          var name = FireRef.fantasyTeams().push({
            name: fantasyTeam.name,
            leagueId: fantasyTeam.leagueId,
            ownerId: owner.id
          }, cb).name()
          FireRef.leagues().child('/'+fantasyTeam.leagueId+'/fantasyTeams/'+name).set(true);
          FireRef.users().child('/'+owner.id+'/fantasyTeams/'+name).set(true);
          deferred.resolve(name);
          return deferred.promise;
        }
      , removeFantasyTeam: function(fantasyTeamId) {
          var fantasyTeam = this.find(fantasyTeamId);
          fantasyTeam.once('value',function(data) {
            FireRef.leagues().child('/'+data.val().leagueId).child('/fantasyTeams/'+fantasyTeamId).remove();
            FireRef.users().child('/'+data.val().ownerId).child('/fantasyTeams/'+fantasyTeamId).remove();
          })
          fantasyTeam.remove();
          return;
        }
      };
    }])

collection() and find() should look familiar to you. create() and removeFantasyTeam(), however, have new pieces. 

In create(), we’re setting up a promise for the first time. We’re going to queue up a couple Firebase writes, and we don’t want the page to redirect until the writes are done and we have the ID of the new object. After we push the fantasyTeam object to Firebase, we know the name() method returns the unique ID that was assigned to it. Since we are trying to denormalize the data, we have to also update the user and league model to reflect that each owns a new fantasy team. Thus, we simply set an attribute of the new id to true inside their respective fantasyTeams/ locations. With this, the user and league data contains which fantasy teams are owned by them, and the fantasy teams themselves contain id references to the user and league which it is owned by. With this data, we can easily find the data which answers the inquiry, “which fantasy teams does this league/user own”, and “which league/user is this fantasy team owned by?”. Since we need the ID of the fantasyTeam for the URL, we’ll have that be returned from the promise to be used in the controller.

In removeFantasyTeam(), we are simply doing the opposite of create(). Before we remove the main object from the Firebase, we first read the data of the object a single time using once(), and unset the references inside the user and league data objects. Once these have been removed, we can remove the fantasy team data object safely.

Make sure and include the service file in index.html

Fantasy Team Controller

Create the fantasy teams controller:

app/js/controllers/fantasyteamcontroller.js
Stop scrolling to load code snippet
'use strict';

angular.module('fantasyApp.controllers.fantasyTeams', ['fantasyApp.services.fantasyTeams'])
  .controller('FantasyTeamsController', ['$scope','$routeParams', '$location', 'angularFire', 'Leagues', 'FantasyTeams','FireRef',
    function($scope, $routeParams, $location, angularFire, Leagues, FantasyTeams, FireRef) {

      $scope.fantasyTeamId = $routeParams.fantasyTeamId;
      $scope.noFantasyTeam = !$routeParams.fantasyTeamId;

      $scope.findFantasyTeams = function() {
        $scope.fantasyTeams = FantasyTeams.collection();
      }

      $scope.findOneFantasyTeam = function () {
        if(!!$scope.fantasyTeamId) {
          angularFire(FantasyTeams.find($routeParams.fantasyTeamId), $scope, 'fantasyTeam');
        }
      }

      $scope.findLeagues = function () {
        $scope.leagues = Leagues.collection();
      }

      $scope.create = function() {
        FantasyTeams.create($scope.fantasyTeam, $scope.auth).then(function(fantasyTeamId) {
          $scope.fantasyTeam = null;
          $location.path('/fantasyteams/'+fantasyTeamId);
        })
      }

      $scope.removeFantasyTeam = function(fantasyTeamId) {
        FantasyTeams.removeFantasyTeam(fantasyTeamId);
      }
    }])

This is essentially identical to the leagues controller. This should make you feel good - this means we’re appropriately abstracting the dirty details of interacting with Firebase to the services. The only major changes here are the injection of Leagues, and the findLeagues() method, which simply attaches the leagues collection to the scope. Also note that, since the service returns a promise, the create() method is slightly different.

Make sure to add the controller file to index.html, and add it as a dependency in app.js

Next, add the new fantasy team routes:

app/js/config.js
Stop scrolling to load code snippet
      .when('/fantasyteams',                      { templateUrl: 'views/fantasyteams/list.html', authRequired: true })
      .when('/fantasyteams/create',               { templateUrl: 'views/fantasyteams/edit.html', authRequired: true })
      .when('/fantasyteams/:fantasyTeamId',       { templateUrl: 'views/fantasyteams/view.html', authRequired: true })
      .when('/fantasyteams/:fantasyTeamId/edit',  { templateUrl: 'views/fantasyteams/edit.html', authRequired: true })

Fantasy Team Views

Now create the three views in a new directory, /fantasyteams:

view.html:

app/views/fantasyteams/view.html
Stop scrolling to load code snippet
<div data-ng-controller="FantasyTeamsController" data-ng-init="findOneFantasyTeam()">
  <h2>{{ fantasyTeam.name }}</h2>
  <p><strong>Owner: </strong>{{ fantasyTeam.ownerId }}</p>
  <p><strong>League: </strong>{{ fantasyTeam.leagueId }}</p> 
  <br>
  <a href="#/fantasyteams">View all fantasy teams</a>
</div>

edit.html:

app/views/fantasyteams/edit.html
Stop scrolling to load code snippet
<div data-ng-controller="FantasyTeamsController" data-ng-init="findOneFantasyTeam();findLeagues()">
  <form class="form-horizontal">
    <div class="control-group">
      <label class="control-label" for="name">Name</label>
      <div class="controls">
        <input type="text" data-ng-model="fantasyTeam.name" id="name" placeholder="Name">
      </div>
    </div>
    <div class="control-group">
      <label class="control-label" for="league">League</label>
      <div class="controls">
        <select 
          data-ng-model="fantasyTeam.leagueId" 
          name="league" 
          required="required"
          data-ng-options="c.$id as c.name for c in leagues">
         <!--  <option data-ng-show="noFantasyTeam" value="">Choose a league:</option> -->
        </select>
      </div>
    </div>
    <div class="control-group">
      <div class="controls">
        <button data-ng-click="create()" data-ng-show="noFantasyTeam">Create Fantasy Team</button>
      </div>
    </div>
    <div class="control-group">
      <div class="controls">
        <a href="#/fantasyteams">View all Fantasy Teams</a>
      </div>
    </div>
  </form>
  <p data-ng-show="err" class="error">{{err}}</p>
</div>

list.html:

app/views/fantasyteams/list.html
Stop scrolling to load code snippet
<div data-ng-controller="FantasyTeamsController" data-ng-init="findFantasyTeams()">
  <h2>Fantasy Teams</h2>
  <h3>All Teams</h3>
  <ul class="unstyled">
    <li data-ng-repeat="fantasyTeam in fantasyTeams">
      <a href="#/fantasyteams/{{ fantasyTeam.$id }}">{{fantasyTeam.name}}</a>
      (<a href="#/fantasyteams/{{ fantasyTeam.$id }}/edit">Edit</a>)
      (<a href="" data-ng-click="removeFantasyTeam('{{ fantasyTeam.$id }}')" >Remove</a>)
    </li>
  </ul>
  <br>
  <a href="#/fantasyteams/create">Create a new fantasy team</a>
</div>

In edit.html, you are seeing the ng-options directive for the first time. In the ng-init directive, you are invoking the findLeagues() method to attach a collection of all leagues returned from the service to the scope. ng-options takes this enumerable object ‘leagues’, iterates through each league ‘c’, and creates an <option> with text ‘c.name’ and value ‘c._id’. 

The default <option> entry is provided commented out to demonstrate an interesting aspect of how AngularJS handles the view interacting with the model. When creating a new fantasy team, without an default <option> with an empty value provided, you will notice that Angular provides a blank one to you anyway on a new page load. However, when an option is selected, the blank one will disappear. This is because, when building out the ng-options directive, the ng-model is initialized as empty. None of the values of the <option>s in the league array match that value, so it adds a blank one. Once you select an option, the model takes on that value, and the empty <option> is no longer necessary. The ng-show directive is present to remove the option when the data already exists, and the league has already been set.

Uncomment the default <option> above in the view.

Fantasy Team Security

Next, let’s add in the security rules for fantasy teams. They are more or less identical to that of leagues:

Firebase Security File
Stop scrolling to load code snippet
    "fantasyTeams": {
      "$fantasyTeam": {
        ".write": "auth != null && (data.child('ownerId').val() == auth.id || data.child('ownerId').val() == null)", 
        ".validate": "newData.hasChildren(['ownerId','leagueId','name'])",
        "ownerId": {
           ".validate": "auth.id == newData.val()"
        },
        "leagueId": {
          ".validate": "newData.val() != ''"
        },
        "name": {
          ".validate": "newData.val() != ''"
        }
      }
    },

And finally, let’s add a fantasy teams link to the header:

app/js/controllers/headercontroller.js
Stop scrolling to load code snippet
      , {
          "title": "Fantasy Teams",
          "link": "/fantasyteams"
        }

Recap

At this point, you have learned all the building blocks of how to use Firebase, angularFire, and angularFireAuth effectively, including a primer on object associations. 

What's Next

This tutorial serves as a broad introduction to using Firebase in an AngularJS application. If you're left wanting more, good news! We're already working on Part 2, in which you will dive into more advanced uses of Firebase. This upcoming section will dive into building a robust real-time object association management infrastructure which will be used in a real-time fantasy draft board.