Friday 5 December 2014

Sorting a Firebase Object full of Objects in AngularJS


I’m really, really enjoying working with Firebase and I’ve taken to heart their encouragement that everything should be an Object (interesting couple of read are Queries, Part 1: Common SQL Queries Converted for Firebase and Queries, Part 2: Advanced Searches with Firebase, made Plug-and-Play Simple - they’ve been somewhat superseded now but they’re still a good read). I’ve perhaps gone a wee bit too far though, as I’ve now got Objects that are just jam packed full of other Objects, and that’s left me needing a method to sort them within an ngRepeat in AngularJS.

I am, of course, using the excellent AngularFire library but for this approach to work properly I also need underscore.js, moment.js and Google’s JavaScript implementation of AES. The encryption/decryption was only required because I store my data encrypted - your use case might not require this level of paranoia! Actually, the moment.js was only required as I’m based in the UK and - apart from having to grit my teeth each and every time I have to write color in CSS - I like my clients to not have to try and parse YYYY-MM-DD into a date that follows a DD/MM/YYYY format.

The scenario starts just after binding my Firebase data to my $scope so that I am left with a data structure which looks a little like this one:

$scope.usersObject = {
   "4ra7lhTMkhhf2v3Gg3zZ": {
       "Checked" : false,
       "Id" : 5,
       "Name" : "First User",
       "DOB": "U2FsdGVkX1+0pnL9uwjrmSG0kuDcMyp3iIbI2PC+5Pw="
   },
   "IfNiv4Hokow1fNf7BdJT": {
       "Checked" : true,
       "Id" : 6,
       "Name" : "Second User",
       "DOB": "U2FsdGVkX18xW33oKiY0Q+UhIMozmekeDjoDNifp3ww="
   },
   "5YHYJ801pByWfPuo8xji": {
       "Checked" : false,
       "Id" : 7,
       "Name" : "Third User",
       "DOB": "U2FsdGVkX19IZvKe7MzntOVfB//6VfEQRWM/Xa/YUE0="
   },
   "mfizsp7lUwAaT0ywp89O": {
       "Checked" : false,
       "Id" : 8,
       "Name" : "Forth User",
       "DOB": "U2FsdGVkX18ZNtTcztQcjat+Z5CqqzqUthqDsszRKW0="
   },
   "aJKPGKY0aLbhUgrmbXKC": {
       "Checked" : false,
       "Id" : 9,
       "Name" : "Fifth User",
       "DOB": "U2FsdGVkX1+QKWaBCkm4ZNSBqyiEzJ2yZ7ysOpNfWhc="
   },
   "hiE4tmkz0lMIPgFZEGBY": {
       "Checked" : false,
       "Id" : 10,
       "Name" : "Sixth User",
       "DOB": "U2FsdGVkX19M2xrW3tnJykge/yOsF15WeAWfgjlMGyo="
   }
};

Now ngRepeat will only sort arrays but I need to keep hold of the key of the Object somewhere in order to be able to access the specific Object when I click on it later. After all, there’s not much point keeping hold of the Objects if there’s no reference to the Object later on is there?

So I was left wanting an Array of Objects but having an Object of Objects - I went about changing things so I had an Array of Objects but it left a bad taste in my mouth as it just didn't seem the correct thing to do… so changed back when I then found this fantastic approach by Scott Lundberg, which seems almost designed for use with Firebase, on github:

app.filter('toArray', function() { return function(obj) {
   /*
    * https://github.com/angular/angular.js/issues/1286#issuecomment-14303275
    */
   if (!(obj instanceof Object)) return obj;
   return _.map(obj, function(val, key) {
       return Object.defineProperty(val, '$key', {__proto__: null, value: key});
   });
}});

This then allows me to temporarily convert my Object of Objects into an Array of Objects but doesn’t alter the underlying data in Firebase as it’s just used within the ngRepeat structure. Letting me do something like this:

<div ng-controller="myCtrl">
   <div ng-repeat="usr in usersObject | toArray | orderBy:myDecyptedDateFunction:true ">
       <input type="checkbox" id="chkUsr{{usr.Id}}" ng-model="usr.Checked">
       <label for="chkUsr{{usr.Id}}" ng-click="getKey(usr);">
           {{usr.Name}}
           :
           {{usr.DOB | decrypt:pin}}
       </label>
   </div>
</div>

I use a function to do my ordering as my dates are encrypted:

$scope.myDecyptedDateFunction = function(item) {
   var decyptedDate = CryptoJS.AES.decrypt(item.DOB, $scope.pin);
   return moment(decyptedDate.toString(CryptoJS.enc.Utf8), "DD/MM/YYYY").format("X");
};

I’ve used this approach a couple of times now and it pleases me no end as I get to keep my Object Oriented data-structure (which exports so well as json) but also lets me use it like an Array. It works by interfering with the internal [[Prototype]] of the Objects in my Object, We get access to the internal Objects using the _.map function from underscore.js which returns an Array. There may well be trade-offs involved in this approach as the MDN article points out:

Warning: Mutating the [[Prototype]] of an object is, by the nature of how modern JavaScript engines optimize property accesses, a very slow operation.

But for my use case, where I have about two dozen Objects in my Object, it’s fine and dandy. Should you be as paranoid as me then please have a look at these two filters:

app.filter("encrypt", function () {
   return function (item) {
       return CryptoJS.AES.encrypt(item, "1234").toString();
   }
});
app.filter("decrypt", function () {
   return function (item, pin) {
       var decrypted = CryptoJS.AES.decrypt(item, pin);
       return decrypted.toString(CryptoJS.enc.Utf8);
   }
});

In fact - as I was debugging my application - I ended up creating a jsFiddle in order to test things, the external resources I used are all there so please look and use what you think will be useful - you will notice that I’ve got a ngClick which makes use of the Objects name when you click on the checkbox as it’s saved in it’s [[prototype]] - if you inspect the $scope you’ll see it as greyed out, but we can still access it with our getKey function:

$scope.getKey = function(user) {
   console.log(user.$key);
};

I hope that this has helped you in some small way, please forgive any glaring mistakes; this represented my first use of both AngularJS and Firebase in anger - I can only hope I get a chance to play with them much more in the future.