An AngularJS CRUD App with Firebase

Someone recently mentioned AngularJS in passing to me and for some reason, I thought I’d check it out. Codecademy has a course on it and includes a couple projects to try. It just made sense to me. This is a post on how to make a simple CRUD application using Firebase as the backend. We’ll be using some basic bootstrap to make it look tolerable.

First off, let’s set up our file structure like so, using empty files for now:

CRUD Project
|
+-- css
|  \-- bootstrap.min.css
|  \-- starter-template.css
+-- js
|  \-- app.js
|  +-- controllers
|    \-- ListController.js
|    \-- AddController.js
|    \-- EditController.js
+-- views
|   \-- add.html
|   \-- edit.html
|   \-- list.html
+-- index.html

I like to be able to see my work as I progress. I usually have my text editor and browser open right next to each other and after each save, I refresh my browser and check the developer console for any errors. If it looks right, we move on. If it doesn’t look as we expected or we get an error, we fix it. I’ve arranged this post with that in mind so let’s start out by filling in the <head> of our index.html:

<!doctype html>
<html>
  <head>
      <meta charset="utf-8">
      <title>CRUD</title>

  <!-- Bootstrap core CSS -->
  <link href="css/bootstrap.min.css" rel="stylesheet">
  <link href="css/starter-template.css" rel="stylesheet">

  <!-- AngularJS and ng-Route -->

  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min.js"></script>
  <script src="https://code.angularjs.org/1.4.4/angular-route.min.js"></script>

      <!-- Firebase -->
      <script src="https://cdn.firebase.com/js/client/2.2.4/firebase.js"></script>

  <!-- AngularFire -->
      <script src="https://cdn.firebase.com/libs/angularfire/1.1.2/angularfire.min.js"></script>

  </head>

Now let’s bring in all those files we just referenced. First off, I’m using the starter template from Bootstrap. Save these files as shown above in the file structure: bootstrap.min.css and starter-template.css. Remove the line text-align: center; from the starter-template class.

Next, notice the CDN references to AngularJS, ng-Route, Firebase and AngularFire. The routing part of AngularJS ships separately, and we’ll be using some routes for the the CRU part of our CRUD application. Firebase is a BaaS or Backend as a Service. AngularJS is a front-end web development framework which plugs in nicely to Firebase. Firebase holds your data and gives you ways to access it. AngularJS let’s you play with it and present it to your users. At this point, we’ll go over to Firebase and open an account.

After you’ve signed up, you’ll need to create an application. Call it whatever you like.

Ang-CRUD

Then you’ll set up hosting.

Hosting

In short, you’ll need npm to install firebase-tools, then do firebase init in the same directory as your index.html. You’ll need to follow the prompts to enter your firebase credentials and select the app you created for this project. Finally, firebase deploy puts all our hard work on the web. For now, that’s a whole lot of nothing but this is the command you’ll be running once your project is complete.

Let’s continue with the rest of our index.html.

<body ng-app="myApp">

        <!-- Navbar -->
        <nav class="navbar navbar-inverse navbar-fixed-top">
          <div class="container">
            <div class="navbar-header">
              <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
              </button>
              <a class="navbar-brand" href="#/">Ang-CRUD</a>
            </div>
            <div id="navbar" class="collapse navbar-collapse">
              <ul class="nav navbar-nav">
                <li><a href="#/">Home</a></li>
              </ul>
            </div><!--/.nav-collapse -->
          </div>
        </nav>

        <!-- Container -->
        <div class="container starter-template">

      <div ng-view></div>

        </div>

        <!-- Module -->
        <script src="js/app.js"></script>

        <!-- Controllers -->
        <script src="js/controllers/ListController.js"></script>    
        <script src="js/controllers/AddController.js"></script>     
        <script src="js/controllers/EditController.js"></script> 

    </body>
</html>

Let’s go through this section by section, starting with <body ng-app="myApp">. If you open index.html in Chrome, you’ll see a navigation header, no content and this error in your developer console: Uncaught Error: [$injector:modulerr] .... (If you see any other errors, check to make sure you have all your files in place, even if they’re empty.) We have that error because ng-app tells AngularJS we have an app called “myApp” and we don’t yet. Let’s take care of that now.

Open your app.js and put this in it:

var app = angular.module('myApp', ['ngRoute', 'firebase']);

app.config(function($routeProvider){
  $routeProvider
  .when('/', {
    controller: 'ListController',
    templateUrl: 'views/list.html'
  })
  .when('/add', {
    controller: 'AddController',
    templateUrl: 'views/add.html'
  })
  .when('/edit/:id', {
    controller: 'EditController',
    templateUrl: 'views/edit.html'

  })
  .otherwise({
    redirectTo: '/'
  });
});

app.constant('FBURL', 
  'https://ang-crud.firebaseio.com/products/' 
  //Use the URL of your project here with the trailing slash                                                   
);

The first line of code gives us an angular app called ‘myApp’ which has the dependencies ‘ngRoute’ and ‘firebase’. We have also set up our routing and a constant (scroll down a bit to constant) for our Firebase URL. After you’ve saved and refreshed, your page should look the same, but you won’t have any errors.

Back on our index.html, we have our navbar section. This is just copied from the bootstrap starter template. Then we have a container <div> and another message for AngularJS: ng-view. This is where our views will be displayed.

We’re going to be CRUDing products, just by way of example. Our products have a SKU, a description and a price. Let’s put some sample data in our Firebase App. Sign in to Firebase and click on “Manage App”, then from your dashboard, click “Data”, then in the upper right corner, you’ll see a button to “Import Data.”

Save this in a file called sample-data.json, then import it.

{
  "products" : {
    "0" : {
      "description" : "Totally Rad Product",
      "sku" : "C4R5U8D4",
      "price" : 9.99
    },
    "1" : {
      "description" : "Coolest Gadget Ever",
      "sku" : "C4R5GD34",
      "price" : 19.95
    },
    "2" : {
      "description" : "A Thing You Need",
      "sku" : "OMG4201234",
      "price" : 99.99
    }
  }
}

Our first view will show a list of our products. For that, we’ll need a ListController and a list.html.

Here’s our list.html:

<table class="table" >
  <caption class="lead">Products</caption>
  <thead>
    <tr>
      <th>SKU</th>
      <th>Description</th>
      <th>Price</th>
    <th> Actions </th>
    </tr>
  </thead>
  <tbody>
    <tr ng-repeat="product in products">
      <td>{{product.sku}}</td>
      <td>{{product.description}}</td>
      <td>{{product.price | currency}}</td>
    <td><a href="#/edit/{{product.$id}}" class="btn btn-small btn-primary">Edit</a> <a class="btn btn-small btn-danger">Delete</a></td>

    </tr>
  </tbody>
</table>
<p>
    <a href="#/add" class="btn btn-success" role="button">Add New Product</a>
</p>

This is just a Bootstrap table, with some buttons for edit, delete and add. These buttons won’t work just yet. There are a few extras in there for AngularJS. The ng-repeat tells AngularJS to iterate through each product in our list of products. The parts in curly braces, e.g. {{product.sku}}, are called expressions. Notice for the price we use | currency which is a filter telling AngularJS to format that expression like so: “$19.99”.

Let’s take a look at our ListController.js so it makes a little more sense.

app.controller('ListController', ['$scope', '$firebaseArray', 'FBURL', function($scope,$firebaseArray, FBURL){
  var products = new Firebase(FBURL);
  $scope.products = $firebaseArray(products);
}]);

There’s a lot going on here in just these few lines of code. First, we’re defining our controller as ListController, then adding all our dependencies. $scope is where we’ll be putting all our data that we’re showing in our view list.html. $firebaseArray is how we talk to our backend, Firebase. FBURL is that constant we defined in our app.js. We start out with a Firebase reference in our var products. The we turn that into an array and put it in our $scope.

Now, if you refresh your index.html you should see a list of your products:

list-of-products

Moving on to the create part of our CRUD application.

Here’s our add.html.

<h1>Add New Product</h1>

<form class="form-horizontal" role="form">
  <div class="form-group">
    <label for="sku class="col-sm-2 control-label">SKU:</label>
    <div class="col-sm-4">
      <input class="form-control" id="sku"
             ng-model="product.sku" placeholder="SKU"/>
    </div>
  </div>

  <div class="form-group">
    <label for="description" class="col-sm-2 control-label">Description:</label>
    <div class="col-sm-4">
      <input class="form-control" id="description"
             ng-model="product.description" placeholder="DESCRIPTION"/>
    </div>
  </div>

  <div class="form-group">
    <label for="Price" class="col-sm-2 control-label">Price:</label>
    <div class="col-sm-4">
      <input class="form-control" id="price" type="number" min=”0? step="any"
             ng-model="product.price" placeholder="9.99"/>
    </div>
  </div>

  <div class="form-group">

    <div class="col-sm-offset-2 col-sm-10">
      <a ng-click="addProduct()" class="btn btn-small btn-primary">Add</a>
    </div>
  </div>
</form>

Again, this is just your standard Bootstrap form with one important thing added: ng-model. This is referencing the $scope of our AddController which doesn’t yet exist. ng-click calls a function addProduct() when the “Add” button is clicked. Let’s fix that.

app.controller('AddController', ['$scope', '$firebaseArray', '$location', 'FBURL', function($scope, $firebaseArray, $location, FBURL){

  $scope.addProduct = function() {
    var ref = new Firebase(FBURL);
    var product = $firebaseArray(ref);
    product.$add({
      sku: $scope.product.sku,
      description: $scope.product.description,
      price: $scope.product.price
    });
    $location.path('/');
  };

}]);

Similar to our ListController, we name our controller and bring in our dependencies. $location is used to redirect us back our list of products. $scope.addProduct() is a function that takes all the information from our form and adds it in our Firebase app.

Now if you click “Add New Product” from your index.html, you should be sent here:

add-product

Go ahead and add a product and see if it you are redirected to your list view with your new product in the list.

Next, we’ll add edit functionality. Here’s our edit.html.

<h1>Edit Product</h1>

<form name="edit_form" class="form-horizontal" role="form">
  <div class="form-group">
    <label for="sku" class="col-sm-2 control-label">SKU:</label>
    <div class="col-sm-4">
      <input class="form-control" id="sku"
             ng-model="product.sku" placeholder="SKU"/>
    </div>
  </div>

  <div class="form-group">
    <label for="description" class="col-sm-2 control-label">Description:</label>
    <div class="col-sm-4">
      <input class="form-control" id="description"
             ng-model="product.description" placeholder="DESCRIPTION"/>
    </div>
  </div>

  <div class="form-group">
    <label for="Price" class="col-sm-2 control-label">Price:</label>
    <div class="col-sm-4">
      <input class="form-control" id="price" type="number" min=”0? step="any"
             ng-model="product.price" placeholder="9.99"/>
    </div>
  </div>

  <div class="form-group">

    <div class="col-sm-offset-2 col-sm-10">
      <input type="submit" id="submit" class="btn btn-small btn-primary" ng-click="editProduct()" class="btn btn-small btn-primary"></button> <a href="#/products" class="btn btn-small btn-danger">Cancel</a>
    </div>
  </div>
</form>

It’s practically identical to our add.html, but ng-click is used on editProduct(). Let’s take a look at that function in our EditController.js.

app.controller('EditController', ['$scope','$location', '$routeParams', '$firebaseObject', 'FBURL',   
    function($scope, $location, $routeParams, $firebaseObject, FBURL){

    var ref = new Firebase(FBURL + $routeParams.id);
    $scope.product = $firebaseObject(ref);

    $scope.editProduct = function() {
        $scope.product.$save({
            sku: $scope.product.sku,
            description: $scope.product.description,
            price: $scope.product.price
        });
        $scope.edit_form.$setPristine();
        $scope.product = {};
        $location.path('/products');

    };

}]);

This is very similar to our AddController.js but with a few key differences. Here we use $routeProvider to get the exact $id of our FirebaseObject. We use $firebaseObject to manipulate this particular record. In our editProduct() we use $setPristine() and set $scope.product to an empty object to clean up our form and $scope after we’re done editing the product.

Lastly, let’s go back and add delete functionality. We’ll delete straight from our list view so open up your list.html and adjust the delete button like so:

<a class="btn btn-small btn-danger" ng-click="removeProduct(product.$id)">Delete</a>

We’re adding ng-click similar to how we did with edit.html. Now open up your ListController.js and add removeProduct(). You’ll also need to add $firebaseObject, just like in EditController.js.

$scope.removeProduct = function(id) {
    var ref = new Firebase(FBURL + id);
    var product = $firebaseObject(ref)
    product.$remove();
   };

Now if you go back to your index.html and click on the delete button, that product will disappear from your database.

There you have it: a fully functional CRUD application written in AngularJS using Firebase as your backend. There is plenty of stuff that you can do to improve this app like validation, pagination, or a modal to confirm deletions. Check here for more info on that.

You can clone a working copy from my github and play with a live demo. If you see any mistakes, please let me know in the comments or on twitter.