From c4f5ea3af53a2f98ef5bb90c4bc203750feec5fe Mon Sep 17 00:00:00 2001 From: Samuel Reed Date: Sat, 28 Jun 2014 16:51:40 -0500 Subject: [PATCH 1/4] Allow defining store mixins via a spec syntax similar to React Component mixins. --- lib/create_store.js | 81 +++++++++++++++++++++++++++++----- lib/function_chaining_error.js | 13 ++++++ test/unit/test_store.js | 70 +++++++++++++++++++++++++++++ 3 files changed, 153 insertions(+), 11 deletions(-) create mode 100644 lib/function_chaining_error.js diff --git a/lib/create_store.js b/lib/create_store.js index c95834c..ad2cb34 100644 --- a/lib/create_store.js +++ b/lib/create_store.js @@ -1,7 +1,11 @@ var _each = require("lodash-node/modern/collection/forEach"), _isFunction = require("lodash-node/modern/lang/isFunction"), Store = require("./store"), - inherits = require("inherits"); + inherits = require("inherits"), + util = require("util"), + _extend = require("lodash-node/modern/objects/assign"), + _isObject = require("lodash-node/modern/objects/isObject"), + FunctionChainingError = require('./function_chaining_error'); var RESERVED_KEYS = ["flux", "waitFor"]; @@ -16,18 +20,13 @@ 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 (util.isArray(spec.mixins)) { + spec.mixins.forEach(createStore.mixSpecIntoComponent.bind(null, this)); } + createStore.mixSpecIntoComponent(this, spec); + if (spec.initialize) { spec.initialize.call(this, options); } @@ -37,4 +36,64 @@ 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") { + if (component[key]) { + // 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); + } + } + + // 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 (util.isArray(one) && util.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..ddaebb7 --- /dev/null +++ b/lib/function_chaining_error.js @@ -0,0 +1,13 @@ +var util = require('util'); + +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."; +} + +util.inherits(FunctionChainingError, Error); + +module.exports = FunctionChainingError; diff --git a/test/unit/test_store.js b/test/unit/test_store.js index 90114be..23366a4 100644 --- a/test/unit/test_store.js +++ b/test/unit/test_store.js @@ -167,4 +167,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( + require('../../lib/function_chaining_error'), + /You are attempting to define `handleAction`.*/ + ); + }); }); From 4929b7caf126bdc18af4191573795f1b7c309381 Mon Sep 17 00:00:00 2001 From: Samuel Reed Date: Sun, 17 Aug 2014 19:45:09 -0400 Subject: [PATCH 2/4] Allow marking mixin functions as unchainable. --- lib/create_store.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/create_store.js b/lib/create_store.js index ad2cb34..d9cf8a9 100644 --- a/lib/create_store.js +++ b/lib/create_store.js @@ -50,7 +50,8 @@ createStore.mixSpecIntoComponent = function mixSpecIntoComponent(component, spec // Functions are chained, with mixin functions being run before spec functions. else if (typeof spec[key] === "function") { - if (component[key]) { + // 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); @@ -58,6 +59,7 @@ createStore.mixSpecIntoComponent = function mixSpecIntoComponent(component, spec 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; } } From dc571f72ef9a7a72d290d8fb3292cbc72a6bf0aa Mon Sep 17 00:00:00 2001 From: Samuel Reed Date: Fri, 6 Feb 2015 16:22:36 +0700 Subject: [PATCH 3/4] Lodash 3.0 fixes --- lib/create_store.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/create_store.js b/lib/create_store.js index d9cf8a9..b28c177 100644 --- a/lib/create_store.js +++ b/lib/create_store.js @@ -1,10 +1,9 @@ var _each = require("lodash-node/modern/collection/forEach"), - _isFunction = require("lodash-node/modern/lang/isFunction"), Store = require("./store"), inherits = require("inherits"), util = require("util"), - _extend = require("lodash-node/modern/objects/assign"), - _isObject = require("lodash-node/modern/objects/isObject"), + _extend = require("lodash-node/modern/object/assign"), + _isObject = require("lodash-node/modern/lang/isObject"), FunctionChainingError = require('./function_chaining_error'); var RESERVED_KEYS = ["flux", "waitFor"]; @@ -59,7 +58,9 @@ createStore.mixSpecIntoComponent = function mixSpecIntoComponent(component, spec 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; + if (spec[key].chainable === false) { + component[key].chainable = false; + } } } From 0f14b25dd3bd4dc11b50f1a15033aeaa4d401362 Mon Sep 17 00:00:00 2001 From: Samuel Reed Date: Tue, 21 Apr 2015 11:30:54 -0500 Subject: [PATCH 4/4] Allow mixins to define initialize(), which will be chained and run before store's initialize. --- lib/create_store.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/create_store.js b/lib/create_store.js index b28c177..9dfbea9 100644 --- a/lib/create_store.js +++ b/lib/create_store.js @@ -26,8 +26,8 @@ var createStore = function(spec) { createStore.mixSpecIntoComponent(this, spec); - if (spec.initialize) { - spec.initialize.call(this, options); + if (this.initialize) { + this.initialize(options); } };