diff --git a/lib/create_store.js b/lib/create_store.js index 7a3bf97..a1ba0b2 100644 --- a/lib/create_store.js +++ b/lib/create_store.js @@ -1,7 +1,10 @@ var _each = require("lodash/collection/forEach"), - _isFunction = require("lodash/lang/isFunction"), Store = require("./store"), - inherits = require("./util/inherits"); + inherits = require("./util/inherits"), + _extend = require("lodash/object/assign"), + _isObject = require("lodash/lang/isObject"), + _isArray = require("lodash/lang/isArray"), + FunctionChainingError = require('./function_chaining_error'); var RESERVED_KEYS = ["flux", "waitFor"]; @@ -16,20 +19,15 @@ var createStore = function(spec) { options = options || {}; Store.call(this); - for (var key in spec) { - if (key === "actions") { - this.bindActions(spec[key]); - } else if (key === "initialize") { - // do nothing - } else if (_isFunction(spec[key])) { - this[key] = spec[key].bind(this); - } else { - this[key] = spec[key]; - } + // Assign mixins first + if (_isArray(spec.mixins)) { + spec.mixins.forEach(createStore.mixSpecIntoComponent.bind(null, this)); } - if (spec.initialize) { - spec.initialize.call(this, options); + createStore.mixSpecIntoComponent(this, spec); + + if (this.initialize) { + this.initialize(options); } }; @@ -37,4 +35,68 @@ var createStore = function(spec) { return constructor; }; +// Define helper functions as properties on createStore so that they can be overridden. +createStore.mixSpecIntoComponent = function mixSpecIntoComponent(component, spec) { + for (var key in spec) { + + // Actions are bound, as seen. + if (key === "actions") { + if (!_isObject(spec[key])) { + throw new Error("Actions must be defined as an object."); + } + component.bindActions(spec[key]); + } + + // Functions are chained, with mixin functions being run before spec functions. + else if (typeof spec[key] === "function") { + // Allow users to mark a function as unchainable via 'chainable' + if (component[key] && component[key].chainable !== false) { + // Only functions can be chained. + if (typeof component[key] !== "function") { + throw new FunctionChainingError(key); + } + component[key] = createStore.createChainedFunction(component[key], spec[key]).bind(component); + } else { + component[key] = spec[key].bind(component); + if (spec[key].chainable === false) { + component[key].chainable = false; + } + } + } + + // Other values are merged. Objects are merged with _.extend() and arrays are concatenated. + // Define a custom merge function by overriding `createStore.merge`. + else { + if (typeof component[key] === "function") { + throw new FunctionChainingError(key); + } + component[key] = createStore.merge(component[key], spec[key]); + } + + } + return component; +}; + +createStore.createChainedFunction = function createChainedFunction(one, two) { + return function chainedFunction() { + one.apply(this, arguments); + two.apply(this, arguments); + }; +}; + +createStore.merge = function merge(one, two) { + if (one === undefined || one === null) { + return two; + } else if (two === undefined || two === null) { + return one; + } else if (_isObject(one) && _isObject(two)) { + return _extend(one, two); + } else if (_isArray(one) && _isArray(two)) { + return one.concat(two); + } + // If types cannot be merged (strings, numbers, etc), simply override the value. + return two; +}; + + module.exports = createStore; diff --git a/lib/function_chaining_error.js b/lib/function_chaining_error.js new file mode 100644 index 0000000..8081be8 --- /dev/null +++ b/lib/function_chaining_error.js @@ -0,0 +1,13 @@ +var inherits = require('./util/inherits'); + +function FunctionChainingError(key) { + Error.call(this); + this.name = "FunctionChainingError"; + this.message = "You are attempting to define " + + "`" + key + "` on your store more than once, but that is only supported " + + "for functions, which are chained together."; +} + +inherits(FunctionChainingError, Error); + +module.exports = FunctionChainingError; diff --git a/test/unit/test_store.js b/test/unit/test_store.js index 90114be..17dbd09 100644 --- a/test/unit/test_store.js +++ b/test/unit/test_store.js @@ -3,7 +3,8 @@ var Fluxxor = require("../../"); var chai = require("chai"), expect = chai.expect, sinon = require("sinon"), - sinonChai = require("sinon-chai"); + sinonChai = require("sinon-chai"), + FunctionChainingError = require('../../lib/function_chaining_error'); chai.use(sinonChai); @@ -167,4 +168,74 @@ describe("Store", function() { new Store(); }).to.throw(/handler.*type ACTION.*falsy/); }); + + it("Allows use of mixins", function() { + var value = 0; + var mixin = { + actions: { + "ACTION3": "handleAction3" + }, + handleAction: function() { + value++; + }, + handleAction3: function() { + value++; + } + }; + var mixin2 = { + handleAction2: function() { + value++; + } + }; + + var Store = Fluxxor.createStore({ + mixins: [mixin, mixin2], + actions: { + "ACTION": "handleAction", + "ACTION2": "handleAction2" + }, + + handleAction: function() { + value++; + }, + handleAction2: function() { + value++; + } + }); + var store = new Store(); + + store.__handleAction__({type: "ACTION"}); + expect(value).to.equal(2); + store.__handleAction__({type: "ACTION2"}); + expect(value).to.equal(4); + store.__handleAction__({type: "ACTION3"}); + expect(value).to.equal(5); + }); + + it("Throws errors when incorrectly using mixins", function() { + var mixin = { + handleAction: function() {} + }; + // An error should be thrown when attempting to mix a string with a function. + var mixin2 = { + handleAction: "string" + }; + + function createStore(){ + var Store = Fluxxor.createStore({ + mixins: [mixin, mixin2], + actions: { + "ACTION": "handleAction" + }, + + handleAction: function() {} + }); + return new Store(); + } + + expect(createStore).to.throw( + FunctionChainingError, + /You are attempting to define `handleAction`.*/ + ); + }); });