diff --git a/.jshintrc b/.jshintrc index 8249e5be..100fcc36 100644 --- a/.jshintrc +++ b/.jshintrc @@ -2,8 +2,10 @@ "predef": [ "define", "module", - "Firebase" + "Firebase", + "Immutable" ], + "node": true, "bitwise": true, "curly": true, "eqeqeq": true, diff --git a/bower.json b/bower.json index e7940415..1471e41f 100644 --- a/bower.json +++ b/bower.json @@ -33,7 +33,8 @@ ], "dependencies": { "react": "0.12.x", - "firebase": "2.0.x" + "firebase": "2.0.x", + "immutable": "3.4.x" }, "devDependencies": { "jasmine": "~2.0.0" diff --git a/build/header b/build/header index 6a63bef9..b3d3222b 100644 --- a/build/header +++ b/build/header @@ -10,17 +10,19 @@ ;(function (root, factory) { "use strict"; + var Immutable = root.Immutable; if (typeof define === "function" && define.amd) { // AMD - define([], function() { - return (root.ReactFireMixin = factory()); + define(["../bower_components/immutable/dist/immutable.min.js"], function(Immutable) { + return (root.ReactFireMixin = factory(Immutable)); }); } else if (typeof exports === "object") { // CommonJS - module.exports = factory(); + Immutable = require("immutable"); + module.exports = factory(Immutable); } else { // Global variables - root.ReactFireMixin = factory(); + root.ReactFireMixin = factory(Immutable); } -}(this, function() { +}(this, function(Immutable) { "use strict"; diff --git a/gulpfile.js b/gulpfile.js index 0c66962d..788d7643 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -44,6 +44,7 @@ var paths = { "bower_components/firebase/firebase.js", "tests/phantomjs-es5-shim.js", "bower_components/react/react-with-addons.js", + "bower_components/immutable/dist/immutable.js", "src/*.js", "tests/specs/*.spec.js" ] diff --git a/package.json b/package.json index 778f01f5..feafaaad 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ ], "dependencies": { "firebase": "2.0.x", + "immutable": "3.4.x", "react": "0.12.x" }, "devDependencies": { diff --git a/src/reactfire.js b/src/reactfire.js index 9bb1b9c5..d520452a 100644 --- a/src/reactfire.js +++ b/src/reactfire.js @@ -31,6 +31,21 @@ var ReactFireMixin = { this._bind(firebaseRef, bindVar, cancelCallback, false); }, + /* Checks all bound vars to see if they have changed. The check is cheap + * because we are using immutable data structures. Returns true if any of + * the bound vars are updated in `nextState`, otherwise false. + * + * This is intended to be used in a more comprehensive `shouldComponentUpdate`. + */ + boundVarsHaveUpdated: function(nextState) { + for (var varName in nextState) { + if (this.firebaseRefs[varName] && !Immutable.is(this.state && this.state[varName], nextState[varName])) { + return true; + } + } + return false; + }, + /* Creates a binding between Firebase and the inputted bind variable as either an array or object */ _bind: function(firebaseRef, bindVar, cancelCallback, bindAsArray) { this._validateBindVar(bindVar); @@ -55,10 +70,10 @@ var ReactFireMixin = { this.firebaseListeners[bindVar] = firebaseRef.on("value", function(dataSnapshot) { var newState = {}; if (bindAsArray) { - newState[bindVar] = this._toArray(dataSnapshot.val()); + newState[bindVar] = this._toList(dataSnapshot); } else { - newState[bindVar] = dataSnapshot.val(); + newState[bindVar] = this._toOrderedMap(dataSnapshot); } this.setState(newState); }.bind(this), cancelCallback); @@ -109,25 +124,40 @@ var ReactFireMixin = { } }, - - /* Returns true if the inputted object is a JavaScript array */ - _isArray: function(obj) { - return (Object.prototype.toString.call(obj) === "[object Array]"); + _toOrderedMap: function(snapshot) { + var out = Immutable.OrderedMap(); + if (snapshot) { + if (Immutable.OrderedMap.isOrderedMap(snapshot)) { + out = snapshot; + } + else if (typeof(snapshot) === "object") { + out = out.withMutations(function(map) { + snapshot.forEach(function(child) { + var immutableChild = Immutable.fromJS(child.val()); + immutableChild.snapshot = child; + map.set(child.key(), immutableChild); + }); + }); + } + } + return out; }, - /* Converts a Firebase object to a JavaScript array */ - _toArray: function(obj) { - var out = []; - if (obj) { - if (this._isArray(obj)) { - out = obj; + /* Converts a Firebase object to an Immutable List */ + _toList: function(snapshot) { + var out = Immutable.List(); + if (snapshot) { + if (Immutable.List.isList(snapshot)) { + out = snapshot; } - else if (typeof(obj) === "object") { - for (var key in obj) { - if (obj.hasOwnProperty(key)) { - out.push(obj[key]); - } - } + else if (typeof(snapshot) === "object") { + out = out.withMutations(function(list) { + snapshot.forEach(function(child) { + var immutableChild = Immutable.fromJS(child.val()); + immutableChild.snapshot = child; + list.push(immutableChild); + }); + }); } } return out; diff --git a/tests/index.html b/tests/index.html index c51064ec..7b713909 100644 --- a/tests/index.html +++ b/tests/index.html @@ -15,6 +15,9 @@ + + + @@ -24,5 +27,6 @@ +
diff --git a/tests/karma.conf.js b/tests/karma.conf.js index 84a25ee7..3e6108c3 100644 --- a/tests/karma.conf.js +++ b/tests/karma.conf.js @@ -2,7 +2,7 @@ module.exports = function(config) { config.set({ frameworks: ["jasmine"], autowatch: false, - singleRun: true, + singleRun: false, preprocessors: { "../src/*.js": "coverage" diff --git a/tests/specs/reactfire.spec.js b/tests/specs/reactfire.spec.js index 0066f028..6db2cd3e 100644 --- a/tests/specs/reactfire.spec.js +++ b/tests/specs/reactfire.spec.js @@ -97,6 +97,7 @@ describe("ReactFireMixin Tests:", function() { }); it("bindAsArray() binds to remote Firebase data as an array", function(done) { + var result = Immutable.List([1, 2, 3]); var TestComponent = React.createClass({ mixins: [ReactFireMixin], @@ -109,7 +110,7 @@ describe("ReactFireMixin Tests:", function() { }, componentDidUpdate: function(prevProps, prevState) { - expect(this.state).toEqual({ items: [1, 2, 3] }); + expect(Immutable.is(this.state.items, result)).toBe(true); done(); }, @@ -122,6 +123,7 @@ describe("ReactFireMixin Tests:", function() { }); it("bindAsArray() binds to remote Firebase data as an array (limit query)", function(done) { + var result = Immutable.List([2, 3]); var TestComponent = React.createClass({ mixins: [ReactFireMixin], @@ -134,7 +136,7 @@ describe("ReactFireMixin Tests:", function() { }, componentDidUpdate: function(prevProps, prevState) { - expect(this.state).toEqual({ items: [2, 3] }); + expect(Immutable.is(this.state.items, result)).toBe(true); done(); }, @@ -237,6 +239,7 @@ describe("ReactFireMixin Tests:", function() { }); it("bindAsObject() binds to remote Firebase data as an object", function(done) { + var result = Immutable.OrderedMap({ a: 1, b: 2, c: 3 }); var TestComponent = React.createClass({ mixins: [ReactFireMixin], @@ -249,7 +252,7 @@ describe("ReactFireMixin Tests:", function() { }, componentDidUpdate: function(prevProps, prevState) { - expect(this.state).toEqual({ items: { a: 1, b: 2, c: 3 } }); + expect(Immutable.is(this.state.items, result)).toBe(true); done(); }, @@ -262,6 +265,7 @@ describe("ReactFireMixin Tests:", function() { }); it("bindAsObject() binds to remote Firebase data as an object (limit query)", function(done) { + var result = Immutable.OrderedMap({ b: 2, c: 3 }); var TestComponent = React.createClass({ mixins: [ReactFireMixin], @@ -274,7 +278,7 @@ describe("ReactFireMixin Tests:", function() { }, componentDidUpdate: function(prevProps, prevState) { - expect(this.state).toEqual({ items: { b: 2, c: 3 } }); + expect(Immutable.is(this.state.items, result)).toBe(true); done(); }, @@ -435,6 +439,64 @@ describe("ReactFireMixin Tests:", function() { }); }); + describe("boundVarsHaveUpdated(): ", function() { + it("boundVarsHaveUpdated() returns false if bound objects haven't changed", function(done) { + var data = { a: 1, b: 2, c: 3 }; + var TestComponent = React.createClass({ + mixins: [ReactFireMixin], + + componentWillMount: function() { + this.bindAsObject(firebaseRef, "items"); + }, + + componentDidMount: function() { + firebaseRef.set(data); + }, + + componentDidUpdate: function(prevProps, prevState) { + expect(this.boundVarsHaveUpdated({ + items: Immutable.OrderedMap(data) + })).toBe(false); + done(); + }, + + render: function() { + return React.DOM.div(null, "Testing"); + } + }); + + React.render(new TestComponent(), document.body); + }); + + it("boundVarsHaveUpdated() returns true if bound objects have changed", function(done) { + var data = { a: 1, b: 2, c: 3 }; + var TestComponent = React.createClass({ + mixins: [ReactFireMixin], + + componentWillMount: function() { + this.bindAsObject(firebaseRef, "items"); + }, + + componentDidMount: function() { + firebaseRef.set(data); + }, + + componentDidUpdate: function(prevProps, prevState) { + expect(this.boundVarsHaveUpdated({ + items: Immutable.Map({ a: 5, b: 6 }) + })).toBe(true); + done(); + }, + + render: function() { + return React.DOM.div(null, "Testing"); + } + }); + + React.render(new TestComponent(), document.body); + }); + }); + describe("_bind():", function() { it("_bind() throws errors given invalid third input parameter", function() { var nonBooleanParams = [null, undefined, [], {}, 0, 5, "", "a", {a : 1}, ["hi", 1]]; @@ -482,4 +544,5 @@ describe("ReactFireMixin Tests:", function() { React.render(new TestComponent(), document.body); }); }); + });