├── .gitignore ├── README.md ├── action.js ├── app.js ├── combine.js ├── domain.js ├── effect.js ├── event.js ├── example.js ├── handleActions.js ├── handleEffects.js ├── href.js ├── index.js ├── navigate.js ├── package.json ├── reduceUpdates.js ├── route.js ├── router.js ├── run.js ├── runMany.js ├── scopeUpdate.js └── test └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log* 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # inux 2 | 3 | an experiment in opinionated helpers for [`inu`](https://github.com/ahdinosaur/inu) 4 | 5 | ```shell 6 | npm install --save inux 7 | ``` 8 | 9 | ![robot doge](https://pbs.twimg.com/profile_images/449012646851780608/yyIufvO-.png) 10 | 11 | ## demos 12 | 13 | - [holodex/app#compost](https://github.com/holodex/app/tree/compost): full-stack user directory app using [`inu`](https://github.com/ahdinosaur/inu), [`inux`](https://github.com/ahdinosaur/inux), and [`vas`](https://github.com/ahdinosaur/vas) 14 | 15 | ## philosophy 16 | 17 | `inu` provides the foundation, `inux` lets us fly. :) 18 | 19 | ## concepts 20 | 21 | `inux` introduces some opinionated concepts to `inu`: 22 | 23 | ### action or effect type 24 | 25 | a unique `String` or `Symbol` that corresponds to a type of action or effect payload 26 | 27 | ```js 28 | const SET_TEXT = 'NOTEPAD_SET_TEXT' 29 | // or 30 | const SET_TEXT = Symbol('setText') 31 | ``` 32 | 33 | ### action or effect creator 34 | 35 | a function that returns an action or effect object of the form `{ type, payload }` 36 | 37 | ```js 38 | function setText (text) { 39 | return { 40 | type: SET_TEXT, 41 | payload: text 42 | } 43 | } 44 | ``` 45 | 46 | ### domain 47 | 48 | like an `inu` app, but 49 | 50 | - optionally namespaced to `name` property, which determines where the domain model will be merged into the overall app model 51 | - `update` or `run` can be objects mapping types of actions or effects, respectively, to handler functions 52 | - `routes` can be an array of arrays describing routes for [`sheet-router`](https://github.com/yoshuawuyts/sheet-router) 53 | 54 | in this example, `textEditor` refers to a hypothetical re-usable view component. 55 | 56 | ```js 57 | { 58 | name: 'notepad', 59 | init: { 60 | model: '', 61 | effect: 62 | } 63 | update: { 64 | [SET_TEXT]: (model, text) { 65 | return text 66 | } 67 | }, 68 | routes: [ 69 | ['/notepad', function (params, model, dispatch) { 70 | return html` 71 |
72 | ${textEditor(model.notepad, save)} 73 |
74 | ` 75 | 76 | function save (text) { 77 | dispatch(setText(text)) 78 | } 79 | }] 80 | ] 81 | } 82 | ``` 83 | 84 | ## enhancer 85 | 86 | a function that take an `inu` app and return another `inu` app 87 | 88 | ```js 89 | function log (app) { 90 | return extend(app, { 91 | init: () => { 92 | const state = app.init() 93 | console.log('init:', state) 94 | return state 95 | }, 96 | update: (model, action) => { 97 | console.log('update before:', model, action) 98 | const state = app.update(model, action) 99 | console.log('update after:', state) 100 | return state 101 | }, 102 | run: (effect, sources) => { 103 | console.log('run', effect) 104 | return app.run(effect, sources) 105 | } 106 | }) 107 | } 108 | ``` 109 | 110 | (for a better `inu` log enhancer, see [`inu-log`](https://github.com/ahdinosaur/inu-log)) 111 | 112 | ## app 113 | 114 | a list of domains to become a single `inu` app 115 | 116 | each domain is a group of functionality corresponding to a small specific theme in your overall app. 117 | 118 | ```js 119 | [ 120 | notepad, 121 | settings, 122 | auth 123 | ] 124 | ``` 125 | 126 | ## example 127 | 128 | ```js 129 | const { start, html } = require('inu') 130 | const { App, Domain, Action } = require('inux') 131 | const extend = require('xtend') 132 | 133 | const INCREMENT = Symbol('increment') 134 | const DECREMENT = Symbol('decrement') 135 | const SET = Symbol('set') 136 | 137 | const increment = Action(INCREMENT) 138 | const decrement = Action(DECREMENT) 139 | const set = Action(SET) 140 | 141 | const view = (model, dispatch) => html` 142 |
143 | 146 | { 148 | dispatch(set(Number(ev.target.value))) 149 | }} 150 | value=${model} 151 | /> 152 | 155 |
156 | ` 157 | 158 | const counter = Domain({ 159 | name: 'counter', 160 | init: () => ({ model: 0 }), 161 | update: { 162 | [INCREMENT]: (model) => ({ model: model + 1 }), 163 | [DECREMENT]: (model) => ({ model: model - 1 }), 164 | [SET]: (model, value) => ({ model: value }) 165 | }, 166 | routes: [ 167 | ['/', (params, model, dispatch) => { 168 | return view(model.counter, dispatch) 169 | }] 170 | ] 171 | }) 172 | 173 | const app = App([ 174 | counter 175 | ]) 176 | 177 | // const sources = start(app) ... 178 | ``` 179 | 180 | ## usage 181 | 182 | ### `inux = require('inux')` 183 | 184 | ### `inux.Domain(Object domain)` 185 | 186 | extends `domain.update` and `domain.run` with `handleActions` and `handleEffects`, respectively. 187 | 188 | ### `inux.App(Array domains)` 189 | 190 | combines an `Array` of `inux` domains into a single `inu` app. 191 | 192 | each domains's model is namespaced to `domain.name`, 193 | any domains's effect is added to an `Array` of effects. 194 | 195 | also 196 | 197 | - adds `inux.apps.href` domain to handle `href` changes 198 | - adds `inux.apps.run` domain to handle `run` actions 199 | - sets `app.view` using `route(combinedRoutes, app) 200 | - enhances resulting app with [`inu-multi`](https://github.com/ahdinosaur/inu-multi) 201 | 202 | ### `inux.Action(String|Symbol type, Function payloadCreator)` 203 | ### `inux.Effect(String|Symbol type, Function payloadCreator)` 204 | ### `inux.Event(String|Symbol type, Function payloadCreator)` 205 | 206 | create an action or effect creator. 207 | 208 | creator returns object of form 209 | 210 | ```js 211 | { 212 | type: type, 213 | payload: payloadCreator(...args) 214 | } 215 | ``` 216 | 217 | ### `inux.run(effect)` 218 | 219 | creates action objects to run an effect. 220 | 221 | corresponding `inux.apps.run` update: 222 | 223 | ``` 224 | { 225 | [RUN]: (model, effect) => ({ model, effect }) 226 | } 227 | ``` 228 | 229 | --- 230 | 231 | ### `inux.apps.run` 232 | 233 | app to handle `run` actions. 234 | 235 | ### `inux.apps.href` 236 | 237 | app to track `window.location.href`. 238 | 239 | ### `inux.combine(Array apps)` 240 | 241 | combines an array of apps into a single app. 242 | 243 | ### `inux.route(Object app, Function viewCreator)` 244 | 245 | given an app with `app.routes` 246 | 247 | and a function of the form `(routes) => (model, dispatch) => ...`, 248 | 249 | creates the router with `Router(app.routes)` and sets the corresponding view as `app.view`. 250 | 251 | ### `inux.Router(Array routes)` 252 | 253 | given an array of routes, return a router using `sheet-router`. 254 | 255 | the last route will be used as the default route. 256 | 257 | ### `inux.handleActions(Object actionHandlers)` 258 | 259 | given an `Object` of `String` or `Symbol` keys to action handlers, returns a single update `Function`. 260 | 261 | each `String` or `Symbol` key corresponds to an `action.type`. 262 | 263 | each action handler is a `Function` of the form `(model, action.payload) => state`. 264 | 265 | ### `inux.handleEffects(Object effectHandlers)` 266 | 267 | given an `Object` of `String` or `Symbol` keys to effect handlers, returns a single update `Function`. 268 | 269 | each `String` or `Symbol` key corresponds to an `effect.type`. 270 | 271 | each effect handler is a `Function` of the form `(effect.payload, sources) => nextActions or null`. 272 | 273 | ### `inux.scopeUpdate(Function update, String key)` 274 | 275 | given an update function and a key, returns a new update function that namespaces updates at the key. 276 | 277 | ### `inux.reduceUpdates(Array updates)` 278 | 279 | given a list of update functions, return a new update function that is the result of applying all of them. 280 | 281 | ### `inux.runMany(Array runs)` 282 | 283 | given a list of run functions, return a new run function that is the result of applying all of them. 284 | 285 | ## inspiration 286 | 287 | - [`redux.combineReducers`](http://redux.js.org/docs/api/combineReducers.html) 288 | - [`redux-actions`](https://github.com/acdlite/redux-actions) 289 | - [`choo`](https://github.com/yoshuawuyts/choo) 290 | 291 | ## license 292 | 293 | The Apache License 294 | 295 | Copyright © 2016 Michael Williams 296 | 297 | Licensed under the Apache License, Version 2.0 (the "License"); 298 | you may not use this file except in compliance with the License. 299 | You may obtain a copy of the License at 300 | 301 | http://www.apache.org/licenses/LICENSE-2.0 302 | 303 | Unless required by applicable law or agreed to in writing, software 304 | distributed under the License is distributed on an "AS IS" BASIS, 305 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 306 | See the License for the specific language governing permissions and 307 | limitations under the License. 308 | -------------------------------------------------------------------------------- /action.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./event') 2 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const href = require('./href') 2 | const run = require('./run').app 3 | const route = require('./route') 4 | const combine = require('./combine') 5 | 6 | module.exports = App 7 | 8 | function App (apps) { 9 | if (!Array.isArray(apps)) apps = [apps] 10 | 11 | // handle run action to trigger effect 12 | apps.unshift(run) 13 | 14 | // handle href state 15 | apps.push(href) 16 | 17 | return route(combine(apps), (router) => { 18 | return (model, dispatch) => { 19 | return router(model.href, model, dispatch) 20 | } 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /combine.js: -------------------------------------------------------------------------------- 1 | const extend = require('xtend') 2 | const getIn = require('get-in') 3 | const defaults = require('inu/defaults') 4 | const multi = require('inu-multi') 5 | 6 | const reduceUpdates = require('./reduceUpdates') 7 | const runMany = require('./runMany') 8 | const scopeUpdate = require('./scopeUpdate') 9 | 10 | module.exports = combineApps 11 | 12 | function combineApps (apps) { 13 | return multi({ 14 | init: combineInits(apps), 15 | update: combineUpdates(apps), 16 | run: combineRuns(apps), 17 | routes: combineRoutes(apps) 18 | }) 19 | } 20 | 21 | function combineInits (apps) { 22 | return function combinedInit () { 23 | return apps.reduce((state, app) => { 24 | if (!app.init) return state 25 | const nextState = app.init() 26 | const nextModel = extend( 27 | state.model, 28 | app.name 29 | ? { [app.name]: nextState.model } 30 | : nextState.model 31 | ) 32 | const nextEffect = nextState.effect 33 | ? state.effect.concat([nextState.effect]) 34 | : state.effect 35 | 36 | return { 37 | model: nextModel, 38 | effect: nextEffect 39 | } 40 | }, { model: {}, effect: [] }) 41 | } 42 | } 43 | 44 | function combineUpdates (apps) { 45 | const scoped = apps.map(app => { 46 | if (!app.update) return defaults.update 47 | return app.name 48 | ? scopeUpdate(app.update, app.name) 49 | : app.update 50 | }) 51 | 52 | return reduceUpdates(scoped) 53 | } 54 | 55 | function findTopView (apps) { 56 | const app = apps.find(app => 57 | !app.name && app.view 58 | ) 59 | return (app || {}).view 60 | } 61 | 62 | function combineRuns (apps) { 63 | return runMany(apps.map(app => app.run)) 64 | } 65 | 66 | function combineRoutes (apps) { 67 | return apps.reduce( 68 | (sofar, app) => { 69 | const routes = app.routes 70 | return routes 71 | ? sofar.concat(routes) 72 | : sofar 73 | }, 74 | [] 75 | ) 76 | } 77 | -------------------------------------------------------------------------------- /domain.js: -------------------------------------------------------------------------------- 1 | const extend = require('xtend') 2 | 3 | const handleActions = require('./handleActions') 4 | const handleEffects = require('./handleEffects') 5 | 6 | module.exports = Domain 7 | 8 | function Domain (domain) { 9 | return extend(domain, { 10 | update: typeof domain.update === 'object' 11 | ? handleActions(domain.update) 12 | : domain.update 13 | , 14 | run: typeof domain.run === 'object' 15 | ? handleEffects(domain.run) 16 | : domain.run 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /effect.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./event') 2 | -------------------------------------------------------------------------------- /event.js: -------------------------------------------------------------------------------- 1 | module.exports = Event 2 | 3 | function Event (type, payloadCreator = identity) { 4 | return (...args) => ({ 5 | type, 6 | payload: payloadCreator(...args) 7 | }) 8 | } 9 | 10 | function identity (payload) { return payload } 11 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | const inux = require('./') 2 | 3 | console.log('inux', inux) 4 | -------------------------------------------------------------------------------- /handleActions.js: -------------------------------------------------------------------------------- 1 | const keys = require('own-enumerable-keys') 2 | 3 | const reduceUpdates = require('./reduceUpdates') 4 | 5 | module.exports = handleActions 6 | 7 | function handleActions (actionHandlers) { 8 | const updates = 9 | keys(actionHandlers).map((actionType) => { 10 | const update = actionHandlers[actionType] 11 | 12 | return function (model, action) { 13 | if (action.type === actionType) { 14 | return update(model, action.payload) 15 | } 16 | return { model } 17 | } 18 | }) 19 | 20 | return reduceUpdates(updates) 21 | } 22 | -------------------------------------------------------------------------------- /handleEffects.js: -------------------------------------------------------------------------------- 1 | const keys = require('own-enumerable-keys') 2 | 3 | const runMany = require('./runMany') 4 | 5 | module.exports = handleEffects 6 | 7 | function handleEffects (effectHandlers) { 8 | const runs = 9 | keys(effectHandlers).map((effectType) => { 10 | const run = effectHandlers[effectType] 11 | 12 | return function (effect, sources) { 13 | if (effect.type === effectType) { 14 | return run(effect.payload, sources) 15 | } 16 | } 17 | }) 18 | 19 | return runMany(runs) 20 | } 21 | -------------------------------------------------------------------------------- /href.js: -------------------------------------------------------------------------------- 1 | const pull = require('pull-stream') 2 | const href = require('sheet-router/href') 3 | const history = require('sheet-router/history') 4 | const Pushable = require('pull-pushable') 5 | 6 | const Domain = require('./domain') 7 | const Action = require('./action') 8 | const Effect = require('./effect') 9 | 10 | 11 | const SET = Symbol('set') 12 | const INIT = Symbol('init') 13 | const GO = Symbol('go') 14 | 15 | const set = Action(SET) 16 | const init = Effect(INIT) 17 | const go = Effect(GO) 18 | 19 | module.exports = Domain({ 20 | 21 | name: 'href', 22 | 23 | init: () => ({ 24 | model: document.location.href, 25 | effect: init() 26 | }), 27 | 28 | update: { 29 | [SET]: (model, href) => ({ model: href }) 30 | }, 31 | 32 | run: { 33 | [INIT]: (_, sources) => { 34 | const effectActions = Pushable(function onClose (error) { 35 | // cleanup href and/or history 36 | console.error(error) 37 | }) 38 | // enable catching links 39 | href(push) 40 | // enable HTML5 history API 41 | history(push) 42 | 43 | return effectActions 44 | 45 | function push (href) { 46 | effectActions.push({ 47 | type: SET, 48 | payload: href 49 | }) 50 | } 51 | }, 52 | [GO]: (href) => { 53 | console.log('href', href) 54 | href = href.replace(/#.*/, '') 55 | if (window.location.href !== href) { 56 | window.history.pushState({}, null, href) 57 | return pull.values([set(href)]) 58 | } 59 | } 60 | } 61 | }) 62 | module.exports.go = go 63 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | App: require('./app'), 3 | Domain: require('./domain'), 4 | Event: require('./event'), // Action | Effect 5 | Action: require('./action'), 6 | Effect: require('./effect'), 7 | run: require('./run').run, 8 | navigate: require('./navigate'), 9 | 10 | // --- 11 | apps: { 12 | run: require('./run').app, 13 | href: require('./href') 14 | }, 15 | Router: require('./router'), 16 | route: require('./route'), 17 | handleActions: require('./handleActions'), 18 | handleEffects: require('./handleEffects'), 19 | scopeUpdate: require('./scopeUpdate'), 20 | reduceUpdates: require('./reduceUpdates'), 21 | runMany: require('./runMany') 22 | } 23 | -------------------------------------------------------------------------------- /navigate.js: -------------------------------------------------------------------------------- 1 | const Url = require('url') 2 | 3 | const run = require('./run').run 4 | const go = require('./href').go 5 | 6 | module.exports = navigate 7 | 8 | function navigate (pathname) { 9 | const current = window.location.href 10 | const next = Url.resolve(current, pathname) 11 | return run(go(next)) 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inux", 3 | "version": "2.1.0", 4 | "description": "experimental flavor of inu helpers based on redux", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "tape test.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/ahdinosaur/inux.git" 12 | }, 13 | "keywords": [], 14 | "author": "Mikey (http://dinosaur.is)", 15 | "license": "Apache-2.0", 16 | "bugs": { 17 | "url": "https://github.com/ahdinosaur/inux/issues" 18 | }, 19 | "homepage": "https://github.com/ahdinosaur/inux#readme", 20 | "browserify": { 21 | "transform": [ 22 | "es2040" 23 | ] 24 | }, 25 | "devDependencies": { 26 | "tape": "^4.5.1" 27 | }, 28 | "dependencies": { 29 | "es2040": "^1.2.2", 30 | "get-in": "^2.0.0", 31 | "inu": "^3.1.1", 32 | "inu-multi": "^1.0.4", 33 | "own-enumerable-keys": "^1.0.0", 34 | "pull-many": "^1.0.6", 35 | "pull-pushable": "^2.0.1", 36 | "pull-stream": "^3.4.3", 37 | "sheet-router": "github:yoshuawuyts/sheet-router#v3.2.0", 38 | "xtend": "^4.0.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /reduceUpdates.js: -------------------------------------------------------------------------------- 1 | module.exports = reduceUpdates 2 | 3 | function reduceUpdates (updates) { 4 | return function reducedUpdate (model, action) { 5 | const nextState = updates.reduce( 6 | (state, update) => { 7 | const nextState = update(state.model, action) 8 | const nextModel = nextState.model 9 | const nextEffect = nextState.effect != null 10 | ? state.effect.concat(nextState.effect) 11 | : state.effect 12 | 13 | return { 14 | model: nextModel, 15 | effect: nextEffect 16 | } 17 | }, 18 | { model, effect: [] } 19 | ) 20 | 21 | nextState.effect = nextState.effect.length !== 0 22 | ? nextState.effect : null 23 | 24 | return nextState 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /route.js: -------------------------------------------------------------------------------- 1 | const extend = require('xtend') 2 | 3 | const Router = require('./router') 4 | 5 | module.exports = route 6 | 7 | function route (app, handler) { 8 | const router = Router(app.routes) 9 | 10 | return extend(app, { 11 | view: handler(router) 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /router.js: -------------------------------------------------------------------------------- 1 | const createRouter = require('sheet-router') 2 | 3 | module.exports = Router 4 | 5 | function Router (routes) { 6 | const [lastPath] = routes[routes.length - 1] 7 | return createRouter(lastPath, routes) 8 | } 9 | -------------------------------------------------------------------------------- /run.js: -------------------------------------------------------------------------------- 1 | const Domain = require('./domain') 2 | const Action = require('./action') 3 | 4 | const RUN = Symbol('run') 5 | const run = Action(RUN) 6 | 7 | const app = Domain({ 8 | update: { 9 | [RUN]: (model, effect) => ({ model, effect }) 10 | } 11 | }) 12 | 13 | module.exports = { 14 | RUN, 15 | run, 16 | app 17 | } 18 | -------------------------------------------------------------------------------- /runMany.js: -------------------------------------------------------------------------------- 1 | const empty = require('pull-stream/sources/empty') 2 | const pullMany = require('pull-many') 3 | 4 | module.exports = runMany 5 | 6 | function runMany (runs) { 7 | return function runEffect (effect, sources) { 8 | const nextActions = runs.map(run => { 9 | if (run == null) return empty() 10 | return run(effect, sources) || empty() 11 | }) 12 | 13 | return pullMany(nextActions) 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /scopeUpdate.js: -------------------------------------------------------------------------------- 1 | const extend = require('xtend') 2 | 3 | module.exports = scopeUpdate 4 | 5 | function scopeUpdate (update, key) { 6 | return function scopedUpdate (model, action) { 7 | const scopeState = update(model[key], action) 8 | const nextModel = extend(model, { 9 | [key]: scopeState.model 10 | }) 11 | 12 | return { 13 | model: nextModel, 14 | effect: scopeState.effect 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const test = require('tape') 2 | 3 | const inux = require('../') 4 | 5 | describe('inux', function(t) { 6 | t.ok(inux, 'module is require-able') 7 | t.end() 8 | }) 9 | --------------------------------------------------------------------------------