diff --git a/README.md b/README.md index b5fe83d..6254d40 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ In order to use ReactFire in your project, you need to include the following fil - + ``` Use the URL above to download both the minified and non-minified versions of ReactFire from the diff --git a/bower.json b/bower.json index 36f1e42..121c227 100644 --- a/bower.json +++ b/bower.json @@ -1,7 +1,7 @@ { "name": "reactfire", "description": "Firebase mixin for ReactJS", - "version": "0.0.0", + "version": "0.5.1", "authors": [ "Firebase " ], diff --git a/dist/reactfire.js b/dist/reactfire.js new file mode 100644 index 0000000..d5099c5 --- /dev/null +++ b/dist/reactfire.js @@ -0,0 +1,365 @@ +/*! + * ReactFire is an open-source JavaScript library that allows you to add a + * realtime data source to your React apps by providing an easy way to let + * Firebase populate the state of React components. + * + * ReactFire 0.5.1 + * https://github.com/firebase/reactfire/ + * License: MIT + */ +/* eslint "strict": [2, "function"] */ +(function(root, factory) { + 'use strict'; + + /* istanbul ignore next */ + if (typeof define === 'function' && define.amd) { + // AMD + define([], function() { + return (root.ReactFireMixin = factory()); + }); + } else if (typeof exports === 'object') { + // CommonJS + module.exports = factory(); + } else { + // Global variables + root.ReactFireMixin = factory(); + } +}(this, function() { + 'use strict'; + + /*************/ + /* HELPERS */ + /*************/ + /** + * Returns the index of the key in the list. If an item with the key is not in the list, -1 is + * returned. + * + * @param {Array} list A list of items. + * @param {string} key The key for which to search. + * @return {number} The index of the item which has the provided key or -1 if no items have the + * provided key. + */ + function _indexForKey(list, key) { + for (var i = 0, length = list.length; i < length; ++i) { + if (list[i]['.key'] === key) { + return i; + } + } + + /* istanbul ignore next */ + return -1; + } + + /** + * Throws a formatted error message. + * + * @param {string} message The error message to throw. + */ + function _throwError(message) { + throw new Error('ReactFire: ' + message); + } + + /** + * Validates the name of the variable which is being bound. + * + * @param {string} bindVar The variable which is being bound. + */ + function _validateBindVar(bindVar) { + var errorMessage; + + if (typeof bindVar !== 'string') { + errorMessage = 'Bind variable must be a string. Got: ' + bindVar; + } else if (bindVar.length === 0) { + errorMessage = 'Bind variable must be a non-empty string. Got: ""'; + } else if (bindVar.length > 768) { + // Firebase can only stored child paths up to 768 characters + errorMessage = 'Bind variable is too long to be stored in Firebase. Got: ' + bindVar; + } else if (/[\[\].#$\/\u0000-\u001F\u007F]/.test(bindVar)) { + // Firebase does not allow node keys to contain the following characters + errorMessage = 'Bind variable cannot contain any of the following characters: . # $ ] [ /. Got: ' + bindVar; + } + + if (typeof errorMessage !== 'undefined') { + _throwError(errorMessage); + } + } + + /** + * Creates a new record given a key-value pair. + * + * @param {string} key The new record's key. + * @param {any} value The new record's value. + * @return {Object} The new record. + */ + function _createRecord(key, value) { + var record = {}; + if (typeof value === 'object' && value !== null) { + record = value; + } else { + record['.value'] = value; + } + record['.key'] = key; + + return record; + } + + + /******************************/ + /* BIND AS OBJECT LISTENERS */ + /******************************/ + /** + * 'value' listener which updates the value of the bound state variable. + * + * @param {string} bindVar The state variable to which the data is being bound. + * @param {Firebase.DataSnapshot} snapshot A snapshot of the data being bound. + */ + function _objectValue(bindVar, snapshot) { + var key = snapshot.key(); + var value = snapshot.val(); + + this.data[bindVar] = _createRecord(key, value); + + this.setState(this.data); + } + + + /*****************************/ + /* BIND AS ARRAY LISTENERS */ + /*****************************/ + /** + * 'child_added' listener which adds a new record to the bound array. + * + * @param {string} bindVar The state variable to which the data is being bound. + * @param {Firebase.DataSnapshot} snapshot A snapshot of the data being bound. + * @param {string|null} previousChildKey The key of the child after which the provided snapshot + * is positioned; null if the provided snapshot is in the first position. + */ + function _arrayChildAdded(bindVar, snapshot, previousChildKey) { + var key = snapshot.key(); + var value = snapshot.val(); + var array = this.data[bindVar]; + + // Determine where to insert the new record + var insertionIndex; + if (previousChildKey === null) { + insertionIndex = 0; + } else { + var previousChildIndex = _indexForKey(array, previousChildKey); + insertionIndex = previousChildIndex + 1; + } + + // Add the new record to the array + array.splice(insertionIndex, 0, _createRecord(key, value)); + + // Update state + this.setState(this.data); + } + + /** + * 'child_removed' listener which removes a record from the bound array. + * + * @param {string} bindVar The state variable to which the data is bound. + * @param {Firebase.DataSnapshot} snapshot A snapshot of the bound data. + */ + function _arrayChildRemoved(bindVar, snapshot) { + var array = this.data[bindVar]; + + // Look up the record's index in the array + var index = _indexForKey(array, snapshot.key()); + + // Splice out the record from the array + array.splice(index, 1); + + // Update state + this.setState(this.data); + } + + /** + * 'child_changed' listener which updates a record's value in the bound array. + * + * @param {string} bindVar The state variable to which the data is bound. + * @param {Firebase.DataSnapshot} snapshot A snapshot of the data to bind. + */ + function _arrayChildChanged(bindVar, snapshot) { + var key = snapshot.key(); + var value = snapshot.val(); + var array = this.data[bindVar]; + + // Look up the record's index in the array + var index = _indexForKey(array, key); + + // Update the record's value in the array + array[index] = _createRecord(key, value); + + // Update state + this.setState(this.data); + } + + /** + * 'child_moved' listener which updates a record's position in the bound array. + * + * @param {string} bindVar The state variable to which the data is bound. + * @param {Firebase.DataSnapshot} snapshot A snapshot of the bound data. + * @param {string|null} previousChildKey The key of the child after which the provided snapshot + * is positioned; null if the provided snapshot is in the first position. + */ + function _arrayChildMoved(bindVar, snapshot, previousChildKey) { + var key = snapshot.key(); + var array = this.data[bindVar]; + + // Look up the record's index in the array + var currentIndex = _indexForKey(array, key); + + // Splice out the record from the array + var record = array.splice(currentIndex, 1)[0]; + + // Determine where to re-insert the record + var insertionIndex; + if (previousChildKey === null) { + insertionIndex = 0; + } else { + var previousChildIndex = _indexForKey(array, previousChildKey); + insertionIndex = previousChildIndex + 1; + } + + // Re-insert the record into the array + array.splice(insertionIndex, 0, record); + + // Update state + this.setState(this.data); + } + + + /*************/ + /* BINDING */ + /*************/ + /** + * Creates a binding between Firebase and the inputted bind variable as either an array or + * an object. + * + * @param {Firebase} firebaseRef The Firebase ref whose data to bind. + * @param {string} bindVar The state variable to which to bind the data. + * @param {function} cancelCallback The Firebase reference's cancel callback. + * @param {boolean} bindAsArray Whether or not to bind as an array or object. + */ + function _bind(firebaseRef, bindVar, cancelCallback, bindAsArray) { + if (Object.prototype.toString.call(firebaseRef) !== '[object Object]') { + _throwError('Invalid Firebase reference'); + } + + _validateBindVar(bindVar); + + if (typeof this.firebaseRefs[bindVar] !== 'undefined') { + _throwError('this.state.' + bindVar + ' is already bound to a Firebase reference'); + } + + // Keep track of the Firebase reference we are setting up listeners on + this.firebaseRefs[bindVar] = firebaseRef.ref(); + + if (bindAsArray) { + // Set initial state to an empty array + this.data[bindVar] = []; + this.setState(this.data); + + // Add listeners for all 'child_*' events + this.firebaseListeners[bindVar] = { + child_added: firebaseRef.on('child_added', _arrayChildAdded.bind(this, bindVar), cancelCallback), + child_removed: firebaseRef.on('child_removed', _arrayChildRemoved.bind(this, bindVar), cancelCallback), + child_changed: firebaseRef.on('child_changed', _arrayChildChanged.bind(this, bindVar), cancelCallback), + child_moved: firebaseRef.on('child_moved', _arrayChildMoved.bind(this, bindVar), cancelCallback) + }; + } else { + // Add listener for 'value' event + this.firebaseListeners[bindVar] = { + value: firebaseRef.on('value', _objectValue.bind(this, bindVar), cancelCallback) + }; + } + } + + + var ReactFireMixin = { + /********************/ + /* MIXIN LIFETIME */ + /********************/ + /** + * Initializes the Firebase refs and listeners arrays. + **/ + componentWillMount: function() { + this.data = {}; + this.firebaseRefs = {}; + this.firebaseListeners = {}; + }, + + /** + * Unbinds any remaining Firebase listeners. + */ + componentWillUnmount: function() { + for (var bindVar in this.firebaseRefs) { + /* istanbul ignore else */ + if (this.firebaseRefs.hasOwnProperty(bindVar)) { + this.unbind(bindVar); + } + } + }, + + + /*************/ + /* BINDING */ + /*************/ + /** + * Creates a binding between Firebase and the inputted bind variable as an array. + * + * @param {Firebase} firebaseRef The Firebase ref whose data to bind. + * @param {string} bindVar The state variable to which to bind the data. + * @param {function} cancelCallback The Firebase reference's cancel callback. + */ + bindAsArray: function(firebaseRef, bindVar, cancelCallback) { + var bindPartial = _bind.bind(this); + bindPartial(firebaseRef, bindVar, cancelCallback, /* bindAsArray */ true); + }, + + /** + * Creates a binding between Firebase and the inputted bind variable as an object. + * + * @param {Firebase} firebaseRef The Firebase ref whose data to bind. + * @param {string} bindVar The state variable to which to bind the data. + * @param {function} cancelCallback The Firebase reference's cancel callback. + */ + bindAsObject: function(firebaseRef, bindVar, cancelCallback) { + var bindPartial = _bind.bind(this); + bindPartial(firebaseRef, bindVar, cancelCallback, /* bindAsArray */ false); + }, + + /** + * Removes the binding between Firebase and the inputted bind variable. + * + * @param {string} bindVar The state variable to which the data is bound. + * @param {function} callback Called when the data is unbound and the state has been updated. + */ + unbind: function(bindVar, callback) { + _validateBindVar(bindVar); + + if (typeof this.firebaseRefs[bindVar] === 'undefined') { + _throwError('this.state.' + bindVar + ' is not bound to a Firebase reference'); + } + + // Turn off all Firebase listeners + for (var event in this.firebaseListeners[bindVar]) { + /* istanbul ignore else */ + if (this.firebaseListeners[bindVar].hasOwnProperty(event)) { + var offListener = this.firebaseListeners[bindVar][event]; + this.firebaseRefs[bindVar].off(event, offListener); + } + } + delete this.firebaseRefs[bindVar]; + delete this.firebaseListeners[bindVar]; + + // Update state + var newState = {}; + newState[bindVar] = undefined; + this.setState(newState, callback); + } + }; + + return ReactFireMixin; +})); diff --git a/dist/reactfire.min.js b/dist/reactfire.min.js new file mode 100644 index 0000000..0d962b5 --- /dev/null +++ b/dist/reactfire.min.js @@ -0,0 +1,10 @@ +/*! + * ReactFire is an open-source JavaScript library that allows you to add a + * realtime data source to your React apps by providing an easy way to let + * Firebase populate the state of React components. + * + * ReactFire 0.5.1 + * https://github.com/firebase/reactfire/ + * License: MIT + */ +!function(e,t){"use strict";"function"==typeof define&&define.amd?define([],function(){return e.ReactFireMixin=t()}):"object"==typeof exports?module.exports=t():e.ReactFireMixin=t()}(this,function(){"use strict";function e(e,t){for(var i=0,n=e.length;n>i;++i)if(e[i][".key"]===t)return i;return-1}function t(e){throw new Error("ReactFire: "+e)}function i(e){var i;"string"!=typeof e?i="Bind variable must be a string. Got: "+e:0===e.length?i='Bind variable must be a non-empty string. Got: ""':e.length>768?i="Bind variable is too long to be stored in Firebase. Got: "+e:/[\[\].#$\/\u0000-\u001F\u007F]/.test(e)&&(i="Bind variable cannot contain any of the following characters: . # $ ] [ /. Got: "+e),"undefined"!=typeof i&&t(i)}function n(e,t){var i={};return"object"==typeof t&&null!==t?i=t:i[".value"]=t,i[".key"]=e,i}function s(e,t){var i=t.key(),s=t.val();this.data[e]=n(i,s),this.setState(this.data)}function a(t,i,s){var a,r=i.key(),o=i.val(),f=this.data[t];if(null===s)a=0;else{var d=e(f,s);a=d+1}f.splice(a,0,n(r,o)),this.setState(this.data)}function r(t,i){var n=this.data[t],s=e(n,i.key());n.splice(s,1),this.setState(this.data)}function o(t,i){var s=i.key(),a=i.val(),r=this.data[t],o=e(r,s);r[o]=n(s,a),this.setState(this.data)}function f(t,i,n){var s,a=i.key(),r=this.data[t],o=e(r,a),f=r.splice(o,1)[0];if(null===n)s=0;else{var d=e(r,n);s=d+1}r.splice(s,0,f),this.setState(this.data)}function d(e,n,d,h){"[object Object]"!==Object.prototype.toString.call(e)&&t("Invalid Firebase reference"),i(n),"undefined"!=typeof this.firebaseRefs[n]&&t("this.state."+n+" is already bound to a Firebase reference"),this.firebaseRefs[n]=e.ref(),h?(this.data[n]=[],this.setState(this.data),this.firebaseListeners[n]={child_added:e.on("child_added",a.bind(this,n),d),child_removed:e.on("child_removed",r.bind(this,n),d),child_changed:e.on("child_changed",o.bind(this,n),d),child_moved:e.on("child_moved",f.bind(this,n),d)}):this.firebaseListeners[n]={value:e.on("value",s.bind(this,n),d)}}var h={componentWillMount:function(){this.data={},this.firebaseRefs={},this.firebaseListeners={}},componentWillUnmount:function(){for(var e in this.firebaseRefs)this.firebaseRefs.hasOwnProperty(e)&&this.unbind(e)},bindAsArray:function(e,t,i){var n=d.bind(this);n(e,t,i,!0)},bindAsObject:function(e,t,i){var n=d.bind(this);n(e,t,i,!1)},unbind:function(e,n){i(e),"undefined"==typeof this.firebaseRefs[e]&&t("this.state."+e+" is not bound to a Firebase reference");for(var s in this.firebaseListeners[e])if(this.firebaseListeners[e].hasOwnProperty(s)){var a=this.firebaseListeners[e][s];this.firebaseRefs[e].off(s,a)}delete this.firebaseRefs[e],delete this.firebaseListeners[e];var r={};r[e]=void 0,this.setState(r,n)}};return h}); \ No newline at end of file diff --git a/package.json b/package.json index cd90609..baeaef0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "reactfire", "description": "Firebase mixin for ReactJS", - "version": "0.0.0", + "version": "0.5.1", "author": "Firebase (https://www.firebase.com/)", "homepage": "https://github.com/firebase/reactfire/", "repository": {