function runExample(demoUrl) { var fbRef = new Firebase(demoUrl); var groupsRef = fbRef.child('groups'); var messagesRef = fbRef.child('messages'); var indexRef = fbRef.child('/users/chuck/groups'); var userCache = new UsersCache(fbRef.child('users')); var groups = {}; /****************** Monitor Mary's list of groups (an index) for add/remove events ******************/ indexRef.on('child_added', addedGroup); indexRef.on('child_removed', removedGroup); /********************** When a group is added or removed, start listening on that group. **********************/ function addedGroup(snapshot) { // Create a new Group object var group = new Group(snapshot, addMessageToDom); // Store the group for easy reference by it's key groups[group.key] = group; // Add it to the dom after initial data loads group.ready().then(addGroupToDom); } function removedGroup(snapshot) { // Removed deleted groups from the DOM removeGroupFromDom(snapshot.key()); // Tell the group it is no longer needed groups[snapshot.key()].remove(); } /********************** Group Class *********************/ function Group(snapshot, messageHandler) { this.key = snapshot.key(); this.messageHandler = messageHandler; // a promise resolved when initial data is fetched this.deferred = $.Deferred(); // populated by this._update() this.name = null; this.members = {}; // monitor the group meta data for updates groupsRef.child(this.key).on('value', this._update, this); // Listen for incoming messages and invoke the _newMessage function // when one arrives messagesRef.child(this.key) .on('child_added', this._newMessage, this); } Group.prototype.remove = function() { // If a group is deleted from the user's profile, we don't need // to listen for changes anymore, so we turn off the child_added listener messagesRef.child(this.key) .off('child_added', this._newMessage, this); groupsRef.child(this.key) .off('value', this._update, this); } Group.prototype._newMessage = function(snapshot) { console.log('new message'); // Fetch the data out of the Firebase snapshot var messageData = snapshot.val(); // Localize some variables for use in the callback var handler = this.messageHandler; var groupKey = this.key; // Fetch the user's profile from our local user cache userCache.getUser(messageData.user).then(function(user) { console.log('loaded user'); // add the user's name to the message data and // send it to the message handler messageData.name = user.name; handler( groupKey, messageData ); }); }; Group.prototype._update = function(snapshot) { var data = snapshot.val()||{}; this.name = data.name; this.members = data.members; // the first time this method is called, resolve // the ready promise this.deferred.resolve(this); }; Group.prototype.ready = function() { return this.deferred.promise(); }; /********************** UsersCache Used to fetch data about specific users and cache it locally, since we assume the names appearing in chat will have a lot of duplication. Updates the user records whenever data changes. **********************/ function UsersCache( refToUsersPath ) { this.ref = refToUsersPath; this.userPromises = {}; } UsersCache.prototype.getUser = function(userId) { if( !this.userPromises.hasOwnProperty(userId) ) { this.userPromises[userId] = this._monitor(userId); } return this.userPromises[userId]; }; UsersCache.prototype._monitor = function(userId) { var def = $.Deferred(); this.ref.child(userId).on('value', function(snapshot) { def.resolve(snapshot.val()); }); return def.promise(); }; /********************** Capture UI events and modify the index ***********************/ // catch button clicks and trigger add/remove watchForButtonClicks(addToIndex, dropFromIndex); function addToIndex(id) { // Create the index entry, there's nothing else to do here // because listeners just attach themselves to the data // so we don't need coupling or information about who cares // that we updated this index; they are already listening to // that path! indexRef.child(id).set(true); } function dropFromIndex(id) { // Remove the index entry. Like addToIndex, there's nobody // to notify, because they just attach themselves to the Firebase // path to get events indexRef.child(id).remove(); } /************************************************** DEMO FLUFF Most of the stuff from here on is used to make the UI shiny and print the results to the DOM. It's probably not interesting for learning Firebase ***************************************************/ // updates the checkboxes for Chuck's Groups indexRef.on('child_added', addIndexEntry); indexRef.on('child_removed', removeIndexEntry); // this just prints the groups data to the screen indexRef.on('value', function(snap) { log( 'jsonGroups', snap.val() ); }); // print out the users who are being cached setInterval(function() { log( 'jsonUsers', Object.keys(userCache.userPromises) ); }, 250); function addGroupToDom(group) { console.log('add group', group.key, group.name); $('<li></li>') .attr('data-id', group.key) .append( $('<b></b>').text(group.name) ) .append('<ul></ul>') .appendTo('#messages'); $('#index li[data-id="' + group.key + '"]').addClass('yes'); } function removeGroupFromDom(groupKey) { $('#messages li[data-id="' + groupKey + '"]').remove(); $('#index li[data-id="' + groupKey + '"]').removeClass('yes'); } function addMessageToDom(groupKey, message) { console.log('add message to dom', groupKey, message); $('<li></li>') .text(message.message) .addClass('message') .prepend( $('<span>').text(message.name) ) .appendTo('#messages li[data-id="' + groupKey + '"] ul'); } function watchForButtonClicks(add, remove) { $('#index input[type="checkbox"]') .click(function () { console.log('clicked'); var myId = $(this).attr('name'); if( $(this).prop('checked') ) { add(myId); } else { remove(myId); } }); } // updates the /users/chuck/groups data // and the checkboxes for Chuck's groups function addIndexEntry(snap) { $('ul#index input[name="' + snap.key() + '"]') .prop('checked', true); } function removeIndexEntry(snap) { $('ul#index input[name="' + snap.key() + '"]') .prop('checked', false); } function unwrapPromises(data) { var results = {}; Object.keys(data).forEach(function(key) { data[key].then(function(val) { results[key] = val; }); }); return $.when(data).then(function() { return results; }); } function log(id, data) { $('#'+id).text(JSON.stringify(data, null, 2)); } } // Dependencies used in this fiddle: // code.jquery.com/jquery-2.1.0.min.js // cdn-gh.firebase.com/demo-utils-script/demo-utils.js // // This line creates a unique, private Firebase URL // you can hack in! Have fun! $.loadDemo('web/struct/join-example', 'web/struct/join-example').then(runExample);