|
| 1 | +/*! |
| 2 | + * ReactFire is an open-source JavaScript library that allows you to add a |
| 3 | + * realtime data source to your React apps by providing an easy way to let |
| 4 | + * Firebase populate the state of React components. |
| 5 | + * |
| 6 | + * ReactFire 0.6.0 |
| 7 | + * https://github.com/firebase/reactfire/ |
| 8 | + * License: MIT |
| 9 | + */ |
| 10 | +/* eslint "strict": [2, "function"] */ |
| 11 | +(function(root, factory) { |
| 12 | + 'use strict'; |
| 13 | + |
| 14 | + /* istanbul ignore next */ |
| 15 | + if (typeof define === 'function' && define.amd) { |
| 16 | + // AMD |
| 17 | + define([], function() { |
| 18 | + return (root.ReactFireMixin = factory()); |
| 19 | + }); |
| 20 | + } else if (typeof exports === 'object') { |
| 21 | + // CommonJS |
| 22 | + module.exports = factory(); |
| 23 | + } else { |
| 24 | + // Global variables |
| 25 | + root.ReactFireMixin = factory(); |
| 26 | + } |
| 27 | +}(this, function() { |
| 28 | + 'use strict'; |
| 29 | + |
| 30 | + /*************/ |
| 31 | + /* HELPERS */ |
| 32 | + /*************/ |
| 33 | + /** |
| 34 | + * Returns the index of the key in the list. If an item with the key is not in the list, -1 is |
| 35 | + * returned. |
| 36 | + * |
| 37 | + * @param {Array<any>} list A list of items. |
| 38 | + * @param {string} key The key for which to search. |
| 39 | + * @return {number} The index of the item which has the provided key or -1 if no items have the |
| 40 | + * provided key. |
| 41 | + */ |
| 42 | + function _indexForKey(list, key) { |
| 43 | + for (var i = 0, length = list.length; i < length; ++i) { |
| 44 | + if (list[i]['.key'] === key) { |
| 45 | + return i; |
| 46 | + } |
| 47 | + } |
| 48 | + |
| 49 | + /* istanbul ignore next */ |
| 50 | + return -1; |
| 51 | + } |
| 52 | + |
| 53 | + /** |
| 54 | + * Throws a formatted error message. |
| 55 | + * |
| 56 | + * @param {string} message The error message to throw. |
| 57 | + */ |
| 58 | + function _throwError(message) { |
| 59 | + throw new Error('ReactFire: ' + message); |
| 60 | + } |
| 61 | + |
| 62 | + /** |
| 63 | + * Validates the name of the variable which is being bound. |
| 64 | + * |
| 65 | + * @param {string} bindVar The variable which is being bound. |
| 66 | + */ |
| 67 | + function _validateBindVar(bindVar) { |
| 68 | + var errorMessage; |
| 69 | + |
| 70 | + if (typeof bindVar !== 'string') { |
| 71 | + errorMessage = 'Bind variable must be a string. Got: ' + bindVar; |
| 72 | + } else if (bindVar.length === 0) { |
| 73 | + errorMessage = 'Bind variable must be a non-empty string. Got: ""'; |
| 74 | + } else if (bindVar.length > 768) { |
| 75 | + // Firebase can only stored child paths up to 768 characters |
| 76 | + errorMessage = 'Bind variable is too long to be stored in Firebase. Got: ' + bindVar; |
| 77 | + } else if (/[\[\].#$\/\u0000-\u001F\u007F]/.test(bindVar)) { |
| 78 | + // Firebase does not allow node keys to contain the following characters |
| 79 | + errorMessage = 'Bind variable cannot contain any of the following characters: . # $ ] [ /. Got: ' + bindVar; |
| 80 | + } |
| 81 | + |
| 82 | + if (typeof errorMessage !== 'undefined') { |
| 83 | + _throwError(errorMessage); |
| 84 | + } |
| 85 | + } |
| 86 | + |
| 87 | + /** |
| 88 | + * Creates a new record given a key-value pair. |
| 89 | + * |
| 90 | + * @param {string} key The new record's key. |
| 91 | + * @param {any} value The new record's value. |
| 92 | + * @return {Object} The new record. |
| 93 | + */ |
| 94 | + function _createRecord(key, value) { |
| 95 | + var record = {}; |
| 96 | + if (typeof value === 'object' && value !== null) { |
| 97 | + record = value; |
| 98 | + } else { |
| 99 | + record['.value'] = value; |
| 100 | + } |
| 101 | + record['.key'] = key; |
| 102 | + |
| 103 | + return record; |
| 104 | + } |
| 105 | + |
| 106 | + |
| 107 | + /******************************/ |
| 108 | + /* BIND AS OBJECT LISTENERS */ |
| 109 | + /******************************/ |
| 110 | + /** |
| 111 | + * 'value' listener which updates the value of the bound state variable. |
| 112 | + * |
| 113 | + * @param {string} bindVar The state variable to which the data is being bound. |
| 114 | + * @param {Firebase.DataSnapshot} snapshot A snapshot of the data being bound. |
| 115 | + */ |
| 116 | + function _objectValue(bindVar, snapshot) { |
| 117 | + var key = snapshot.key(); |
| 118 | + var value = snapshot.val(); |
| 119 | + |
| 120 | + this.data[bindVar] = _createRecord(key, value); |
| 121 | + |
| 122 | + this.setState(this.data); |
| 123 | + } |
| 124 | + |
| 125 | + |
| 126 | + /*****************************/ |
| 127 | + /* BIND AS ARRAY LISTENERS */ |
| 128 | + /*****************************/ |
| 129 | + /** |
| 130 | + * 'child_added' listener which adds a new record to the bound array. |
| 131 | + * |
| 132 | + * @param {string} bindVar The state variable to which the data is being bound. |
| 133 | + * @param {Firebase.DataSnapshot} snapshot A snapshot of the data being bound. |
| 134 | + * @param {string|null} previousChildKey The key of the child after which the provided snapshot |
| 135 | + * is positioned; null if the provided snapshot is in the first position. |
| 136 | + */ |
| 137 | + function _arrayChildAdded(bindVar, snapshot, previousChildKey) { |
| 138 | + var key = snapshot.key(); |
| 139 | + var value = snapshot.val(); |
| 140 | + var array = this.data[bindVar]; |
| 141 | + |
| 142 | + // Determine where to insert the new record |
| 143 | + var insertionIndex; |
| 144 | + if (previousChildKey === null) { |
| 145 | + insertionIndex = 0; |
| 146 | + } else { |
| 147 | + var previousChildIndex = _indexForKey(array, previousChildKey); |
| 148 | + insertionIndex = previousChildIndex + 1; |
| 149 | + } |
| 150 | + |
| 151 | + // Add the new record to the array |
| 152 | + array.splice(insertionIndex, 0, _createRecord(key, value)); |
| 153 | + |
| 154 | + // Update state |
| 155 | + this.setState(this.data); |
| 156 | + } |
| 157 | + |
| 158 | + /** |
| 159 | + * 'child_removed' listener which removes a record from the bound array. |
| 160 | + * |
| 161 | + * @param {string} bindVar The state variable to which the data is bound. |
| 162 | + * @param {Firebase.DataSnapshot} snapshot A snapshot of the bound data. |
| 163 | + */ |
| 164 | + function _arrayChildRemoved(bindVar, snapshot) { |
| 165 | + var array = this.data[bindVar]; |
| 166 | + |
| 167 | + // Look up the record's index in the array |
| 168 | + var index = _indexForKey(array, snapshot.key()); |
| 169 | + |
| 170 | + // Splice out the record from the array |
| 171 | + array.splice(index, 1); |
| 172 | + |
| 173 | + // Update state |
| 174 | + this.setState(this.data); |
| 175 | + } |
| 176 | + |
| 177 | + /** |
| 178 | + * 'child_changed' listener which updates a record's value in the bound array. |
| 179 | + * |
| 180 | + * @param {string} bindVar The state variable to which the data is bound. |
| 181 | + * @param {Firebase.DataSnapshot} snapshot A snapshot of the data to bind. |
| 182 | + */ |
| 183 | + function _arrayChildChanged(bindVar, snapshot) { |
| 184 | + var key = snapshot.key(); |
| 185 | + var value = snapshot.val(); |
| 186 | + var array = this.data[bindVar]; |
| 187 | + |
| 188 | + // Look up the record's index in the array |
| 189 | + var index = _indexForKey(array, key); |
| 190 | + |
| 191 | + // Update the record's value in the array |
| 192 | + array[index] = _createRecord(key, value); |
| 193 | + |
| 194 | + // Update state |
| 195 | + this.setState(this.data); |
| 196 | + } |
| 197 | + |
| 198 | + /** |
| 199 | + * 'child_moved' listener which updates a record's position in the bound array. |
| 200 | + * |
| 201 | + * @param {string} bindVar The state variable to which the data is bound. |
| 202 | + * @param {Firebase.DataSnapshot} snapshot A snapshot of the bound data. |
| 203 | + * @param {string|null} previousChildKey The key of the child after which the provided snapshot |
| 204 | + * is positioned; null if the provided snapshot is in the first position. |
| 205 | + */ |
| 206 | + function _arrayChildMoved(bindVar, snapshot, previousChildKey) { |
| 207 | + var key = snapshot.key(); |
| 208 | + var array = this.data[bindVar]; |
| 209 | + |
| 210 | + // Look up the record's index in the array |
| 211 | + var currentIndex = _indexForKey(array, key); |
| 212 | + |
| 213 | + // Splice out the record from the array |
| 214 | + var record = array.splice(currentIndex, 1)[0]; |
| 215 | + |
| 216 | + // Determine where to re-insert the record |
| 217 | + var insertionIndex; |
| 218 | + if (previousChildKey === null) { |
| 219 | + insertionIndex = 0; |
| 220 | + } else { |
| 221 | + var previousChildIndex = _indexForKey(array, previousChildKey); |
| 222 | + insertionIndex = previousChildIndex + 1; |
| 223 | + } |
| 224 | + |
| 225 | + // Re-insert the record into the array |
| 226 | + array.splice(insertionIndex, 0, record); |
| 227 | + |
| 228 | + // Update state |
| 229 | + this.setState(this.data); |
| 230 | + } |
| 231 | + |
| 232 | + |
| 233 | + /*************/ |
| 234 | + /* BINDING */ |
| 235 | + /*************/ |
| 236 | + /** |
| 237 | + * Creates a binding between Firebase and the inputted bind variable as either an array or |
| 238 | + * an object. |
| 239 | + * |
| 240 | + * @param {Firebase} firebaseRef The Firebase ref whose data to bind. |
| 241 | + * @param {string} bindVar The state variable to which to bind the data. |
| 242 | + * @param {function} cancelCallback The Firebase reference's cancel callback. |
| 243 | + * @param {boolean} bindAsArray Whether or not to bind as an array or object. |
| 244 | + */ |
| 245 | + function _bind(firebaseRef, bindVar, cancelCallback, bindAsArray) { |
| 246 | + if (Object.prototype.toString.call(firebaseRef) !== '[object Object]') { |
| 247 | + _throwError('Invalid Firebase reference'); |
| 248 | + } |
| 249 | + |
| 250 | + _validateBindVar(bindVar); |
| 251 | + |
| 252 | + if (typeof this.firebaseRefs[bindVar] !== 'undefined') { |
| 253 | + _throwError('this.state.' + bindVar + ' is already bound to a Firebase reference'); |
| 254 | + } |
| 255 | + |
| 256 | + // Keep track of the Firebase reference we are setting up listeners on |
| 257 | + this.firebaseRefs[bindVar] = firebaseRef.ref(); |
| 258 | + |
| 259 | + if (bindAsArray) { |
| 260 | + // Set initial state to an empty array |
| 261 | + this.data[bindVar] = []; |
| 262 | + this.setState(this.data); |
| 263 | + |
| 264 | + // Add listeners for all 'child_*' events |
| 265 | + this.firebaseListeners[bindVar] = { |
| 266 | + child_added: firebaseRef.on('child_added', _arrayChildAdded.bind(this, bindVar), cancelCallback), |
| 267 | + child_removed: firebaseRef.on('child_removed', _arrayChildRemoved.bind(this, bindVar), cancelCallback), |
| 268 | + child_changed: firebaseRef.on('child_changed', _arrayChildChanged.bind(this, bindVar), cancelCallback), |
| 269 | + child_moved: firebaseRef.on('child_moved', _arrayChildMoved.bind(this, bindVar), cancelCallback) |
| 270 | + }; |
| 271 | + } else { |
| 272 | + // Add listener for 'value' event |
| 273 | + this.firebaseListeners[bindVar] = { |
| 274 | + value: firebaseRef.on('value', _objectValue.bind(this, bindVar), cancelCallback) |
| 275 | + }; |
| 276 | + } |
| 277 | + } |
| 278 | + |
| 279 | + |
| 280 | + var ReactFireMixin = { |
| 281 | + /********************/ |
| 282 | + /* MIXIN LIFETIME */ |
| 283 | + /********************/ |
| 284 | + /** |
| 285 | + * Initializes the Firebase refs and listeners arrays. |
| 286 | + **/ |
| 287 | + componentWillMount: function() { |
| 288 | + this.data = {}; |
| 289 | + this.firebaseRefs = {}; |
| 290 | + this.firebaseListeners = {}; |
| 291 | + }, |
| 292 | + |
| 293 | + /** |
| 294 | + * Unbinds any remaining Firebase listeners. |
| 295 | + */ |
| 296 | + componentWillUnmount: function() { |
| 297 | + for (var bindVar in this.firebaseRefs) { |
| 298 | + /* istanbul ignore else */ |
| 299 | + if (this.firebaseRefs.hasOwnProperty(bindVar)) { |
| 300 | + this.unbind(bindVar); |
| 301 | + } |
| 302 | + } |
| 303 | + }, |
| 304 | + |
| 305 | + |
| 306 | + /*************/ |
| 307 | + /* BINDING */ |
| 308 | + /*************/ |
| 309 | + /** |
| 310 | + * Creates a binding between Firebase and the inputted bind variable as an array. |
| 311 | + * |
| 312 | + * @param {Firebase} firebaseRef The Firebase ref whose data to bind. |
| 313 | + * @param {string} bindVar The state variable to which to bind the data. |
| 314 | + * @param {function} cancelCallback The Firebase reference's cancel callback. |
| 315 | + */ |
| 316 | + bindAsArray: function(firebaseRef, bindVar, cancelCallback) { |
| 317 | + var bindPartial = _bind.bind(this); |
| 318 | + bindPartial(firebaseRef, bindVar, cancelCallback, /* bindAsArray */ true); |
| 319 | + }, |
| 320 | + |
| 321 | + /** |
| 322 | + * Creates a binding between Firebase and the inputted bind variable as an object. |
| 323 | + * |
| 324 | + * @param {Firebase} firebaseRef The Firebase ref whose data to bind. |
| 325 | + * @param {string} bindVar The state variable to which to bind the data. |
| 326 | + * @param {function} cancelCallback The Firebase reference's cancel callback. |
| 327 | + */ |
| 328 | + bindAsObject: function(firebaseRef, bindVar, cancelCallback) { |
| 329 | + var bindPartial = _bind.bind(this); |
| 330 | + bindPartial(firebaseRef, bindVar, cancelCallback, /* bindAsArray */ false); |
| 331 | + }, |
| 332 | + |
| 333 | + /** |
| 334 | + * Removes the binding between Firebase and the inputted bind variable. |
| 335 | + * |
| 336 | + * @param {string} bindVar The state variable to which the data is bound. |
| 337 | + * @param {function} callback Called when the data is unbound and the state has been updated. |
| 338 | + */ |
| 339 | + unbind: function(bindVar, callback) { |
| 340 | + _validateBindVar(bindVar); |
| 341 | + |
| 342 | + if (typeof this.firebaseRefs[bindVar] === 'undefined') { |
| 343 | + _throwError('this.state.' + bindVar + ' is not bound to a Firebase reference'); |
| 344 | + } |
| 345 | + |
| 346 | + // Turn off all Firebase listeners |
| 347 | + for (var event in this.firebaseListeners[bindVar]) { |
| 348 | + /* istanbul ignore else */ |
| 349 | + if (this.firebaseListeners[bindVar].hasOwnProperty(event)) { |
| 350 | + var offListener = this.firebaseListeners[bindVar][event]; |
| 351 | + this.firebaseRefs[bindVar].off(event, offListener); |
| 352 | + } |
| 353 | + } |
| 354 | + delete this.firebaseRefs[bindVar]; |
| 355 | + delete this.firebaseListeners[bindVar]; |
| 356 | + |
| 357 | + // Update state |
| 358 | + var newState = {}; |
| 359 | + newState[bindVar] = undefined; |
| 360 | + this.setState(newState, callback); |
| 361 | + } |
| 362 | + }; |
| 363 | + |
| 364 | + return ReactFireMixin; |
| 365 | +})); |
0 commit comments