In our last blog post, we discussed best practices for using arrays in Firebase. If you haven't read that, please start there. This blog post will discuss advanced concepts for converting between real-time objects and sortable, filterable arrays.
Okay, so let's get some working gloves on and start handling synchronized arrays. If we want client-side sorting, filtering, and other things arrays are great at, but also need the awesome collaborative aspects of real-time objects, then we're going to be up to our elbows in code. We're going to work in JavaScript here, because that's what I'm most familiar with. All of these techniques can be translated to iOS and Android as well.
Before we get started, it's worth mentioning that there are some great frameworks that have already resolved the problems we face with synchronized arrays. In fact, we'll be drawing from the principles of their array management throughout this article.
EmberFire provides array handling out of the box, integrating Firebase neatly into the Ember.js framework. If you're already a glowing Ember dev, check out the Ember + Firebase Guide for details.
AngularFire version 0.8.0 provides utilities for using arrays in Angular. If you are an ngDev, Check out the Angular + Firebase Guide for details.
BackboneFire provides similar capabilities for those osteologists who prefer Backbone's flexible MVVM.
Drawing from the lessons learned in developing libraries like AngularFire, we can lay down a solid set of principles to keep ourselves out of trouble:
In other words, our array is essentially a one-directional loop. Changes come from the server into the array, we read them out, we push our local edits to the server, they trickle back into the array.
Okay, let's get started handling synchronized arrays in Firebase.
Let's start simple:
function getSynchronizedArray(firebaseRef) { var list = []; // put some magic here return list; }
This provides a method we can pass a firebaseRef into and get back an array. Now let's put some data into it.
function getSynchronizedArray(firebaseRef) { var list = []; syncChanges(list, firebaseRef); return list; } function syncChanges(list, ref) { ref.on('child_added', function _add(snap, prevChild) { var data = snap.val(); data.$id = snap.key(); // assumes data is always an object var pos = positionAfter(list, prevChild); list.splice(pos, 0, data); }); } // similar to indexOf, but uses id to find element function positionFor(list, key) { for(var i = 0, len = list.length; i < len; i++) { if( list[i].$id === key ) { return i; } } return -1; } // using the Firebase API's prevChild behavior, we // place each element in the list after it's prev // sibling or, if prevChild is null, at the beginning function positionAfter(list, prevChild) { if( prevChild === null ) { return 0; } else { var i = positionFor(list, prevChild); if( i === -1 ) { return list.length; } else { return i+1; } } }
Okay, now our array is getting populated from Firebase. Next, let's deal with the other CRUD operations, and move events.
function syncChanges(list, ref) { ref.on('child_added', ...); // example above ref.on('child_removed', function _remove(snap) { var i = positionFor(list, snap.key()); if( i > -1 ) { list.splice(i, 1); } }); ref.on('child_changed', function _change(snap) { var i = positionFor(list, snap.key()); if( i > -1 ) { list[i] = snap.val(); list[i].$id = snap.key(); // assumes data is always an object } }); ref.on('child_moved', function _move(snap, prevChild) { var curPos = positionFor(list, snap.key()); if( curPos > -1 ) { var data = list.splice(curPos, 1)[0]; var newPos = positionAfter(list, prevChild); list.splice(newPos, 0, data); } }); }
Great, now our array is completely synchronized with the remote data. Now we just need some local editing capabilities.
We won't modify our array directly. We can do ops like sort to re-order the data, but if we start trying todo ops like splice or pop, and have the server modifying this array as well, things will get ugly.
sort
splice
pop
Instead, we'll directly push changes to the server with some quick wrapper methods. Since Firebase triggers eventsfor local changes immediately, without waiting for a server response, this is quite performant and simplifies theprocess as well:
function getSynchronizedArray(firebaseRef) { var list = []; syncChanges(list, firebaseRef); wrapLocalCrudOps(list, firebaseRef); return list; } function syncChanges() { /* in examples above */ } function wrapLocalCrudOps(list, firebaseRef) { // we can hack directly on the array to provide some convenience methods list.$add = function(data) { return firebaseRef.push(data); }; list.$remove = function(key) { firebaseRef.child(key).remove(); }; list.$set = function(key, newData) { // make sure we don't accidentally push our $id prop if( newData.hasOwnProperty('$id') ) { delete newData.$id; } firebaseRef.child(key).set(newData); }; list.$indexOf = function(key) { return positionFor(list, key); // positionFor in examples above } }
And there we are! We can now manipulate our array by using operations like this:
var ref = new Firebase(URL); var list = getSynchronizedArray(ref); // add a record var newRecordId = list.$add({ foo: 'bar' }).key(); // remove a record list.$remove(recordKey); // update a record var data = list[5]; data.foo = 'baz'; list.$set( data.$id, data );
That's pretty much it! There are a few additional edge cases that we can cover, but we've created asynchronized array that merges local changes with server updates and keeps all of it in a tidy array.
Here's an example project utilizing these principles you can useas a template for baking your own solutions. Check out the README for installation instructions.
At Firebase, we're never satisfied with just being awesome. We are constantly working to improve all these scenarios and have several API enhancements in the pipeline. Keep an eye on the Google Group for details and announcements.
Find this article useful or have additional questions? Leave a comment below or post a message on the Google Group!