├── .gitignore ├── package.json ├── LICENSE ├── README.md └── superflux.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.swp 3 | *.log 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "superflux", 3 | "version": "0.3.0", 4 | "description": "Opinionated model layer for Mithril based on Flux", 5 | "keywords": [ 6 | "flux", 7 | "mithril", 8 | "superflux" 9 | ], 10 | "files": [ 11 | "superflux.js", 12 | "src/", 13 | "LICENSE" 14 | ], 15 | "main": "superflux.js", 16 | "author": { 17 | "name": "David Colgan", 18 | "email": "dvcolgan@gmail.com", 19 | "url": "http://lessboring.com" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/dvcolgan/superflux" 24 | }, 25 | "bugs": { 26 | "url": "https://github.com/dvcolgan/superflux/issues" 27 | }, 28 | "license": "MIT", 29 | "dependencies": { 30 | "mithril": "^0.1.34" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 David Colgan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Superflux! 2 | 3 | Opinionated model layer for Mithril based on Flux 4 | 5 | ## Motivation 6 | 7 | In my book, Mithril.js is the best thing since CSS preprocessors for front end dev. But as a relatively young dev in the grand scheme of things, I am not the best when it comes to architecting grand code architectures. Mithril has great support for the view and controller part of the MVC triangle, but doesn't make any prescriptions about the model layer. 8 | 9 | Coming from a Django background with its opinionated opinions on where everything should go, I am a big fan of convention over configuration. When the Facebook Flux idea started gaining hype, I was excited, because here was some rails for the model layer of the single page app. 10 | 11 | After trying most of the 10 different implementations of the Flux pattern, I was still unsatisfied because none of them seemed to address both of two problems I saw: where do you put AJAX and/or websocket request code, and why is there so much boilerplate? 12 | 13 | Coming from the Paul Graham school of "code brevity is next to godliness," it was frustrating to have to duplicate the names of each action many times over the course of the codebase. Also all of the documentation and example code for most of these libraries were simplistic todo list apps, which were good for showing off the libraries' features, but I don't know that very many people build a single page app without some kind of API integration. 14 | 15 | And so, like all good little programmers, I decided that I could do one better. Also all of the Flux libraries only seem to want to play nicely with React.js, which is cool for me, Mithril is simpler to understand and less verbose, so I made a Flux for Mithril. I'm also hoping to add a full example with API, as I think I've got a way of doing it cleanly here. 16 | 17 | Superflux was the first thing that came to mind for a name and I can't think of anything better. If anyone else has any ideas I'm open to changing it. 18 | 19 | Is this all a good idea? We'll shall see. 20 | 21 | ## Installation 22 | 23 | npm install superflux 24 | 25 | ## Usage 26 | 27 | Currently you must be using Browserify or something similar that allows you to `require` packages installed from npm. In a file in the root of your project, perhaps in a file called `flux.js`, create an instance of superflux and export it for the app to use. If you are going to be using socket.io, create a new socket and pass it in as well: 28 | 29 | var Superflux = require('superflux') 30 | var io = require('socket.io-client') 31 | 32 | var flux = new Superflux({socket: io()}); 33 | module.exports = flux; 34 | 35 | Then, somewhere, perhaps in a file called `actions.js`, declare your flux actions. `flux.createActions` can create 4 different types of actions: 36 | 37 | * `local`: normal flux actions 38 | * `socketListen`: only fired when a socket request comes in from the server with the action's name 39 | * `socketEmit`: when fired, these actions also send their payload to the server with a name the same as the action's name 40 | * `async`: when fired, a local action is fired, as well as an ajax request with the same payload. When the request comes back, an action called `Success` is fired if the ajax request is successful, or `Failure` if the request fails. 41 | 42 | var flux = require('path/to/flux.js'); 43 | 44 | module.exports = flux.createActions({ 45 | socketListen: [ 46 | 'welcome' 47 | 'userJoined' 48 | 'userDisconnected' 49 | 'chatMessageReceived' 50 | ], 51 | 52 | socketEmit: [ 53 | 'mapSectorNeeded' 54 | 'chatMessageCreate' 55 | 'userNameChange' 56 | ], 57 | 58 | local: [ 59 | 'uiToggle' 60 | ] 61 | 62 | async: { 63 | 'login': function(data) { 64 | method: 'POST' 65 | url: '/auth/login' 66 | data: data 67 | }, 68 | 'usernameExists': function(data) { 69 | method: 'GET' 70 | url: 71 | }, 72 | 'userCreate': function(data) { 73 | method: 'POST' 74 | url: '/users' 75 | data: data 76 | }, 77 | 'userList': function() { 78 | method: 'GET' 79 | url: '/users' 80 | } 81 | } 82 | }); 83 | 84 | # TODO do more documentations 85 | -------------------------------------------------------------------------------- /superflux.js: -------------------------------------------------------------------------------- 1 | var m = require('mithril'); 2 | 3 | // Thanks to http://stackoverflow.com/a/8809472/356789 4 | function generateUUID(){ 5 | var d = new Date().getTime(); 6 | var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 7 | var r = (d + Math.random()*16)%16 | 0; 8 | d = Math.floor(d/16); 9 | return (c=='x' ? r : (r&0x3|0x8)).toString(16); 10 | }); 11 | return uuid; 12 | }; 13 | 14 | 15 | var SUPERFLUX = function(args) { 16 | if (args == null) { 17 | args = {}; 18 | } 19 | var socket = args.socket || function(){}; 20 | var requestAuthHeaderKey = args.requestAuthHeaderKey || null; 21 | var requestAuthHeaderValue = args.requestAuthHeaderValue || null; 22 | 23 | var successMiddleware = args.successMiddleware || function(res){return res;}; 24 | var failureMiddleware = args.failureMiddleware || function(res){return res;}; 25 | 26 | var actions = {}; 27 | var stores = {}; 28 | var callbacks = {}; 29 | 30 | return { 31 | setAuthHeader: function(key, value) { 32 | requestAuthHeaderKey = key; 33 | requestAuthHeaderValue = value; 34 | }, 35 | createStore: function(store) { 36 | store.notify = m.redraw; 37 | 38 | for (key in store) { 39 | var value = store[key]; 40 | 41 | // Register functions in the store as listeners if they start with 'on' 42 | if (typeof(value) === 'function' && key.slice(0, 2) === 'on') { 43 | 44 | // Extract the action name from the function name: 45 | // onCreateTodo -> createTodo 46 | var actionName = key[2].toLowerCase() + key.slice(3); 47 | 48 | if (!(actionName in callbacks)) { 49 | callbacks[actionName] = []; 50 | } 51 | callbacks[actionName].push(value.bind(store)); 52 | } 53 | } 54 | 55 | // Call the constructor if present 56 | if ('init' in store) { 57 | store.init(); 58 | } 59 | 60 | return store; 61 | }, 62 | 63 | createActions: function(spec) { 64 | var socketListen = spec.socketListen || []; 65 | var socketEmit = spec.socketEmit || []; 66 | var local = spec.local || []; 67 | var async = spec.async || {}; 68 | 69 | local.map(function(name) { 70 | actions[name] = function(args) { 71 | if (name in callbacks) { 72 | for (var i = 0; i < callbacks[name].length; i++) { 73 | var callback = callbacks[name][i]; 74 | callback(args); 75 | } 76 | } 77 | }; 78 | }); 79 | 80 | socketListen.map(function(name) { 81 | socket.on(name, function(res) { 82 | if (name in callbacks) { 83 | for (var i = 0; i < callbacks[name].length; i++) { 84 | var callback = callbacks[name][i]; 85 | callback(res); 86 | } 87 | } 88 | }); 89 | }); 90 | 91 | socketEmit.map(function(name) { 92 | actions[name] = function(args) { 93 | socket.emit(name, args); 94 | if (name in callbacks) { 95 | for (var i = 0; i < callbacks[name].length; i++) { 96 | var callback = callbacks[name][i]; 97 | callback(args); 98 | } 99 | } 100 | }; 101 | }); 102 | 103 | Object.keys(async).map(function(name) { 104 | var configFn = async[name]; 105 | 106 | var successFnName = name + 'Success'; 107 | var failureFnName = name + 'Failure'; 108 | 109 | var successFn = function(res) { 110 | if (successFnName in callbacks) { 111 | for (var i = 0; i < callbacks[successFnName].length; i++) { 112 | var callback = callbacks[successFnName][i]; 113 | callback(res); 114 | } 115 | } 116 | }; 117 | 118 | var failureFn = function(res) { 119 | if (failureFnName in callbacks) { 120 | for (var i = 0; i < callbacks[failureFnName].length; i++) { 121 | var callback = callbacks[failureFnName][i]; 122 | callback(res); 123 | } 124 | } 125 | }; 126 | 127 | actions[successFnName] = successFn; 128 | actions[failureFnName] = failureFn; 129 | 130 | actions[name] = function(args) { 131 | // Pass the same uuid to initial and result callbacks to tie them together 132 | var uuid = generateUUID(); 133 | if (name in callbacks) { 134 | for (var i = 0; i < callbacks[name].length; i++) { 135 | var callback = callbacks[name][i]; 136 | callback(args, uuid); 137 | } 138 | } 139 | 140 | var options = configFn(args); 141 | m.request({ 142 | method: options.method, 143 | url: options.url, 144 | data: options.data, 145 | background: true, 146 | config: function(xhr) { 147 | xhr.setRequestHeader('Content-Type', 'application/json'); 148 | if (requestAuthHeaderKey != null && requestAuthHeaderValue != null) { 149 | xhr.setRequestHeader( 150 | requestAuthHeaderKey, 151 | requestAuthHeaderValue 152 | ); 153 | } 154 | } 155 | }) 156 | .then( 157 | function(res) { 158 | if ('requestSuccess' in callbacks) { 159 | for (var i = 0; i < callbacks.requestSuccess.length; i++) { 160 | var callback = callbacks.requestSuccess[i]; 161 | callback(res); 162 | } 163 | } 164 | return res; 165 | }, 166 | function(err) { 167 | if ('requestFailure' in callbacks) { 168 | for (var i = 0; i < callbacks.requestFailure.length; i++) { 169 | var callback = callbacks.requestFailure[i]; 170 | callback(err); 171 | } 172 | } 173 | throw err; 174 | } 175 | ) 176 | .then(function(successRes) { 177 | successFn(successRes, uuid); 178 | }, function(failureRes) { 179 | failureFn(failureRes, uuid); 180 | }); 181 | }; 182 | }); 183 | 184 | return actions; 185 | } 186 | }; 187 | }; 188 | 189 | module.exports = SUPERFLUX; 190 | --------------------------------------------------------------------------------