Tagstackmob

AngularJS SignIt! – Interchangeable Parse, StackMob and Backbone Services

Note: This is a companion post to Example CRUD App – Starring AngularJS, Backbone, Parse, StackMob and Yeoman. If you haven’t read that yet, please do so, otherwise this might not make much sense.

The AngularJS SignIt! application basically has three different interactions with a web service – fetch petitions, save a signature, and fetch a list of signatures based on the selected petition. That’s it – a get, a save, and a query. Initially, I was only using Parse.com to store data, so it was possible to include Parse specific objects and methods in my controller to save and get data.

But then I remembered I have a StackMob account just sitting around doing nothing, and thought I should put it to good use. So now I have two (slightly) different options to store my signatures. Rather than jumbling up my controller with code specific to StackMob and Parse, I created a module to abstract the Parse and StackMob APIs into their own services. These services could then hide any code specific to Parse or StacMob behind a common interface used by the controller.

With the back-end(s) abstracted, all the controller needs to worry about is calling saveSignature, getPetitions, and getSignatures. Below is a severely truncated version of the Main Controller that shows the three methods in use. Notice there is no mention of Parse or StackMob.

var MainCtrl = ngSignItApp.controller('MainCtrl', function($scope,DataService) {

  // GET A LIST OF SIGNATURES FOR A PETITION
  $scope.getSignatures = function getSignatures (petitionId) {
    DataService.getSignatures(petitionId,function (results) {
      $scope.$apply(function() {
        $scope.signatureList = results;
      });
    });
  };

  // SAVE A SIGNATURE
  $scope.saveSignature = function saveSignature() {  
    DataService.saveSignature($scope.user, function() { //user is an object with firstName, lastName, email and signature attributes.
      $scope.getSignatures($scope.select2); //select2 is the value from the petition dropdown
    });  
  };

  // GET ALL PETITIONS
  DataService.getPetitions(function (results) {
    $scope.$apply(function() {
      $scope.petitionCollection = results;
      $scope.petitions = results.models;
    });
  });

});

If you look closely, you’ll see that each service method is prefixed with DataService. This is the injectable that provides either the StackMob service, or Parse service to the controller. Each of those services has an implementation of the getSignatures, saveSignature, and getPetitions. Take a look:

angular.module('DataServices', [])
// PARSE SERVICE
.factory('ParseService', function(){
    Parse.initialize("<PLEASE USE YOUR OWN APP KEY>", "<PLEASE USE YOUR OWN API KEY>");
    var Signature = Parse.Object.extend("signature");
    var SignatureCollection = Parse.Collection.extend({ model: Signature });
    var Petition = Parse.Object.extend("petition");
    var PetitionCollection = Parse.Collection.extend({ model: Petition });

    var ParseService = {

      // GET ALL PETITIONS
      getPetitions : function getPetitions(callback) {
        var petitions = new PetitionCollection();
        petitions.fetch({
          success: function (results) {
              callback(petitions);
          }
        });
      },

      // SAVE A SIGNATURE
      saveSignature : function saveSignature(data, callback){
        var sig = new Signature();
        sig.save( data, {
                  success: function (obj) {callback(obj);}
        });
      },

      // GET A LIST OF SIGNATURES FOR A PETITION
      getSignatures : function getSignatures(petitionId, callback) {
        var query = new Parse.Query(Signature);
        query.equalTo("petitionId", petitionId);
        query.find({
          success: function (results) {
            callback(results);
          }
        });
      }
   
    };

    return ParseService;
})
// STACKMOB SERVICE
.factory('StackMobService', function(){
    // Init the StackMob API. This information is provided by the StackMob app dashboard
    StackMob.init({
      appName: "ngsignit",
      clientSubdomain: "<PLEASE USE YOUR OWN SUBDOMAIN>",
      publicKey: "<PLEASE USE YOUR OWN PUBLICKEY>",
      apiVersion: 0
    });

    var Signature = StackMob.Model.extend( {schemaName:"signature"} );
    var SignatureCollection = StackMob.Collection.extend( { model: Signature } );
    var Petition = StackMob.Model.extend( {schemaName:"petition"} );
    var PetitionCollection = StackMob.Collection.extend( { model: Petition } );

    var StackMobService = {
     
      getPetitions : function getPetitions(callback) {
        var petitions = new PetitionCollection();
        var q = new StackMob.Collection.Query();
        petitions.query(q, {
          success: function (results) {
              callback(petitions.add(results));
          },
          error: function ( results,error) {
              alert("Collection Error: " + error.message);
          }
        });        
      },    

      saveSignature : function saveSignature(data, callback){
        var sigToSave = new Signature();
        sigToSave.set({
          firstname: data.firstName,
          lastname: data.lastName,
          petitionid: data.petitionId,
          email: data.email,
          signature: JSON.stringify(data.signature) //Also, StackMob does not allow arrays of objects, so we need to stringify the signature data and save it to a 'String' data field.
        });

        // Then save, as usual.
        sigToSave.save({},{
          success: function(result) {
            callback(result);
          },
          error: function(obj, error) {
            alert("Error: " + error.message);
          }
        });
      },

      getSignatures : function getSignatures(petitionId, callback) {
        var signatures = new SignatureCollection();
        var q = new StackMob.Collection.Query();
        var signatureArray = [];

        q.equals('petitionid',petitionId);

        signatures.query(q,{
          success: function(collection) {
            collection.each(function(item) {
              item.set({
                signature: JSON.parse(item.get('signature')),
                firstName: item.get('firstname'),
                lastName: item.get('lastname')
              });
              signatureArray.push(item);
            });
            callback(signatureArray);
          }
        });
      }
   
    };
    // The factory function returns StackMobService, which is injected into controllers.
    return StackMobService;
})

This is an abridged version of the DataServices module. To see the full code, as well as many more comments explaining the code, head over to GitHub. The main point to observe here is that each service has slightly different code for getSignatures, getPetitions, and saveSignature. Also, each service has its own initialization code for its respective back-end service. The controller could care less though, because as long as the service methods accept and provide data in the right format, it’s happy.

But how does the controller know which service to use? Well, if you paid attention to the controller code, you’ll see that ‘DataService’ is injected, which is not defined yet. In the full code, there is a service defined all the way at the bottom of the file. It looks like this:

.factory('DataService', function (ParseService,StackMobService,BackboneService,$location) {
  var serviceToUse = BackboneService;
  if ( $location.absUrl().indexOf("stackmob") > 0 || $location.absUrl().indexOf("4567") > 0 ) serviceToUse = StackMobService;
  if ( $location.path() === '/parse' ) serviceToUse = ParseService;

  return serviceToUse;
});

All the other services (ParseService, StackMobService, and BackboneService) are injected into this service. In case you are wondering, BackboneService is yet another back-end service that can be used in place of the others – see the full code for details. The code above simply examines the URL and decides which service get injected via DataService. If ‘parse’ appears as the url path (e.g. www.example.com/app/#/parse), then ParseService is returned. StackMob requires that HTML apps be hosted on their servers, so just check the domain name for ‘stackmob’ and return the StackMob service. If neither of these conditions occur, then the BackboneService is returned, and no data is saved.

In retrospect, I think what I’ve got here is the beginnings of an OO-like interface – where a set of functions are defined to form a contract with the client ensuring their existence. And the implementations of that interface are a set of adapters (or is it proxies?). If I had to do this over, I would use a proper inheritance pattern with DataService as the abstract parent, and the other services as the implementations or subclasses. One of these days I’ll get around to refactoring. One day…

Full Source: https://github.com/ericterpstra/ngSignIt

Example CRUD App – Starring AngularJS, Backbone, Parse, StackMob and Yeoman

AngularJS SignIt!

After finishing my first experimental project in AngularJS, I wanted to try saving data, rather than just retrieving it. Angular has some great mechanisms for creating forms, so I put together a small application to test out more of what Angular has to offer. Oh, and I also wanted to build this using Yeoman. More on that in a minute.

The app I ended up with is a petition signer. A user chooses a petition from the select-box, enters a first name, last name and email into the form, and then signs the signature pad either by drawing with the mouse, or using a finger on a touchscreen. Hitting the Save button will capture the information, and add it to the list of signatories for the selected petition. Pretty simple, no?

The really fun part comes when the data is saved. Depending on the url, the application will choose a place to save the data. If ‘parse’ shows up in the url path, then Parse.com is used as the back-end. If the application is hosted on StackMob’s server, then data is stored with StackMob.com. If neither of these things occur, then data is just saved to a temporary Backbone collection and cleared out when you refresh the page.

Try it out!

Please note that a modern browser is needed. This will not work on IE8 or lower. I have only tested it in the latest Chrome, Firefox 15, and Safari on iOS 5. YMMV with other browsers. If I find the time, I’ll add in the fixes for IE later on.

Get the Code: https://github.com/ericterpstra/ngSignIt

A few more things…

Aside from AngularJS, Parse, and StackMob, I threw in plenty of other nifty libraries and snippets to get this thing working and looking (kinda) nice.

  • The select2 component, wrapped in a directive provided by Angular-UI. Angular-UI is a great add-on library for AngularJS that provides some neat widgets and additional functionality to directives.
  • HTML5 SignaturePad jQuery plugin from Thomas Bradley.
  • responsive stylesheet (try it on mobile!)
  • A font-face generated from fontsquirrel.com using Estrya’s Handwriting font.
  • Stack of paper CSS3 snippet from CSS-Tricks.com

And, of course, Yeoman was involved. There is tons of functionality built into Yeoman for handling unit-tests, live compilation of SASS and CoffeeScript, dependency management, and other stuff. I basically just used it for it’s AngularJS scaffolding, and to download some libraries via Bower. It went something like this:

yeoman init angular ngSignIt
cd ngSignIt
yeoman install jquery
yeoman install json2
... etc ...

Then I went about building my application as usual. Upon completion, I ran

yeoman build

to package everything that is needed into the ‘dist’ folder, and plopped that on my web server. Done.

// TODO

In upcoming blog posts, I’ll go over some of the more interesting bits of code – namely the services module and custom directives. There are a fair number of comments in the code now, but I think a higher-level overview would add some value. Stay tuned.

UPDATE!

Here are the links to the follow-up articles:

© 2018 Eric Terpstra

Theme by Anders NorénUp ↑