├── .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 | 
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 |
--------------------------------------------------------------------------------