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.
// 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:
// 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:
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