├── .gitignore ├── LICENSE ├── README.md ├── examples ├── connect │ ├── components │ │ ├── greeting.js │ │ ├── list.js │ │ └── name.js │ ├── index.js │ └── reducer.js └── simple │ ├── component.js │ ├── index.js │ └── reducer.js ├── index.js ├── package.json └── test ├── all.js ├── connect.js └── simple.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jeremy Freeman 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hxdx 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![js-standard-style][standard-image]][standard-url] 5 | 6 | > simple connecter for state dispatching and rendering 7 | 8 | Connects a `redux`-style store to a `virtual-dom`-style view and sets up rendering with minimal boilerplate. Works well with functional components that take state and sometimes dispatch. 9 | 10 | Exposes an `hx` function for constructing components elements, and a `dx` function for dispatching to the store within your components. Thus the name! Currently uses [`hyperx`](http://github.com/substack/hyperx) for defining components and [`main-loop`](http://github.com/Raynos/main-loop) for rendering. Doesn't require `redux`, just something that acts as a state store. 11 | 12 | I wrote this because I love the `redux` design pattern, but found the `react-redux` bindings, and `react` in general, big and complex and hard to reason about. If you care about performance those are supposed to be much faster! 13 | 14 | See also 15 | - [`virtual-app`](https://github.com/sethvincent/virtual-app) related idea with different dependencies 16 | - [`redux-react`](https://github.com/reactjs/react-redux) official connector for using redux with react 17 | 18 | ## install 19 | 20 | ``` 21 | npm install hxdx 22 | ``` 23 | 24 | ## example 25 | 26 | Set up a simple `redux` store with one action 27 | 28 | ```javascript 29 | var reducer = function (state, action) { 30 | switch (action.type) { 31 | case 'INCREMENT': 32 | return state + 1 33 | default: 34 | return state 35 | } 36 | } 37 | 38 | var store = require('redux').createStore(reducer, 0) 39 | ``` 40 | 41 | Then create your components (normally these would be in separate files) 42 | 43 | We'll make one that renders 44 | 45 | ```javascript 46 | var display = function (state) { 47 | return hx`
${state}
` 48 | } 49 | ``` 50 | 51 | And one that dipatches 52 | 53 | ```javascript 54 | var button = function (state) { 55 | function onclick () { 56 | dx({type: 'INCREMENT'}) 57 | } 58 | return hx`` 59 | } 60 | ``` 61 | 62 | Then just render your top-level component using the store 63 | 64 | ```javascript 65 | var app = function (state) { 66 | return hx`
${display(state)}${button()}
` 67 | } 68 | 69 | hxdx.render(app, store) 70 | ``` 71 | 72 | and the DOM will be updated using diffing on every click. 73 | 74 | ## api 75 | 76 | #### `hxdx.render(component, store, [root])` 77 | 78 | Render a component and connect it to a store. 79 | 80 | - `component` function mapping state to a virtual dom element 81 | - `store` a state store with `subscribe`, `dispatch`, and `getState` methods 82 | - `root` a base DOM element to append to (if undefined will appemd to body) 83 | 84 | Store can be able object that follows the [`redux`](https://github.com/reactjs/redux) API. 85 | 86 | #### `hxdx.hx('<>')` 87 | 88 | Tagged template function for generating `virtual-dom` elements. Can be required inside any of your components. 89 | 90 | #### `hxdx.dx(action)` 91 | 92 | Dispatch action to the store. Can be required inside any of your components. 93 | 94 | [npm-image]: https://img.shields.io/badge/npm-v1.0.0-lightgray.svg?style=flat-square 95 | [npm-url]: https://npmjs.org/package/hxdx 96 | [standard-image]: https://img.shields.io/badge/code%20style-standard-lightgray.svg?style=flat-square 97 | [standard-url]: https://github.com/feross/standard 98 | -------------------------------------------------------------------------------- /examples/connect/components/greeting.js: -------------------------------------------------------------------------------- 1 | const hxdx = require('../../../index') 2 | const hx = hxdx.hx 3 | const connect = hxdx.connect 4 | 5 | function greeting (state) { 6 | return hx` 7 | ${state.name} 8 | ` 9 | } 10 | 11 | module.exports = connect({ name: 'name' }, greeting) 12 | 13 | -------------------------------------------------------------------------------- /examples/connect/components/list.js: -------------------------------------------------------------------------------- 1 | const hxdx = require('../../../index') 2 | const hx = hxdx.hx 3 | const connect = hxdx.connect 4 | 5 | const greeting = require('./greeting.js') 6 | 7 | function list (state) { 8 | function greetings () { 9 | return state.greetings.map(function (word) { 10 | return hx`
  • ${word}${greeting()}
  • ` 11 | }) 12 | } 13 | 14 | return hx` 15 | 16 | ` 17 | } 18 | 19 | module.exports = connect({ greetings: 'greetings' }, list) 20 | -------------------------------------------------------------------------------- /examples/connect/components/name.js: -------------------------------------------------------------------------------- 1 | const hxdx = require('../../../index') 2 | const hx = hxdx.hx 3 | const dx = hxdx.dx 4 | const connect = hxdx.connect 5 | 6 | function name (state) { 7 | function updateName (event) { 8 | dx({ 9 | type: 'UPDATE_NAME', 10 | name: event.target.value 11 | }) 12 | } 13 | 14 | return hx` 15 | 16 | ` 17 | } 18 | 19 | module.exports = connect({ name: 'name' }, name) 20 | -------------------------------------------------------------------------------- /examples/connect/index.js: -------------------------------------------------------------------------------- 1 | const hxdx = require('../../index.js') 2 | const hx = hxdx.hx 3 | const redux = require('redux') 4 | const reducer = require('./reducer') 5 | 6 | const name = require('./components/name') 7 | const list = require('./components/list') 8 | 9 | var store = redux.createStore(reducer, { 10 | name: null, 11 | greetings: [ 12 | 'Hello, ', 13 | 'Shalom, ', 14 | 'Hola, ' 15 | ] 16 | }) 17 | 18 | var wrapper = function (state) { 19 | return hx` 20 |
    21 |
    ${name()}
    22 |
    ${list()}
    23 |
    24 | ` 25 | } 26 | 27 | hxdx.render(wrapper, store) 28 | -------------------------------------------------------------------------------- /examples/connect/reducer.js: -------------------------------------------------------------------------------- 1 | var assign = require('object-assign') 2 | 3 | var reducer = function (state, action) { 4 | switch (action.type) { 5 | case 'UPDATE_NAME': 6 | return assign({}, state, { name: action.name }) 7 | default: 8 | return state 9 | } 10 | } 11 | 12 | module.exports = reducer 13 | -------------------------------------------------------------------------------- /examples/simple/component.js: -------------------------------------------------------------------------------- 1 | var hxdx = require('../../index') 2 | var hx = hxdx.hx 3 | var dx = hxdx.dx 4 | 5 | module.exports = function (state) { 6 | function increment () { 7 | dx({type: 'INCREMENT'}) 8 | } 9 | 10 | function decrement () { 11 | dx({type: 'DECREMENT'}) 12 | } 13 | 14 | function onchange () { 15 | dx({type: 'ENTER', value: document.querySelector('#input').value}) 16 | } 17 | 18 | return hx` 19 |
    value: ${state}

    20 | add/subtract:
    21 | 22 |

    23 | enter a value:
    24 | 25 |
    26 | ` 27 | } 28 | -------------------------------------------------------------------------------- /examples/simple/index.js: -------------------------------------------------------------------------------- 1 | var hxdx = require('../../index.js') 2 | var redux = require('redux') 3 | var reducer = require('./reducer') 4 | var component = require('./component') 5 | 6 | var store = redux.createStore(reducer, 0) 7 | 8 | hxdx.render(component, store) 9 | -------------------------------------------------------------------------------- /examples/simple/reducer.js: -------------------------------------------------------------------------------- 1 | var reducer = function counter (state, action) { 2 | console.log('action:') 3 | console.log(action) 4 | console.log('state:') 5 | console.log(state) 6 | 7 | switch (action.type) { 8 | case 'INCREMENT': 9 | return state + 1 10 | case 'DECREMENT': 11 | return state - 1 12 | case 'ENTER': 13 | return parseInt(action.value, 16) || 0 14 | default: 15 | return state 16 | } 17 | } 18 | 19 | module.exports = reducer 20 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var get = require('lodash.get') 2 | var fromPairs = require('lodash.frompairs') 3 | var map = require('lodash.map') 4 | var vdom = require('virtual-dom') 5 | var hyperx = require('hyperx') 6 | var hx = hyperx(vdom.h) 7 | 8 | var dx, globalStore 9 | 10 | var filterState = function (mapping, state) { 11 | if (!mapping) { 12 | return state 13 | } 14 | const pairs = map(mapping, function (path, prop) { return [prop, get(state, path)] }) 15 | return fromPairs(pairs) 16 | } 17 | 18 | module.exports = { 19 | dx: function (action) { 20 | dx(action) 21 | }, 22 | 23 | hx: hx, 24 | 25 | connect: function (mapping, comp) { 26 | return function () { 27 | return comp(filterState(mapping, globalStore.getState())) 28 | } 29 | }, 30 | 31 | render: function (el, store, root) { 32 | globalStore = store 33 | dx = store.dispatch 34 | 35 | var main = require('main-loop') 36 | 37 | var loop = main(store.getState(), render, vdom) 38 | 39 | if (root) root.appendChild(loop.target) 40 | else document.body.appendChild(loop.target) 41 | 42 | function render (state) { 43 | return el(state) 44 | } 45 | 46 | function update () { 47 | loop.update(store.getState()) 48 | } 49 | 50 | store.subscribe(update) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hxdx", 3 | "version": "1.0.0", 4 | "description": "really simple connector for redux", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "budo examples/simple/index.js", 8 | "test": "browserify test/all.js | testron | tap-spec", 9 | "start:simple": "budo examples/simple/index.js", 10 | "start:connect": "budo examples/connect/index.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/freeman-lab/hxdx.git" 15 | }, 16 | "keywords": [ 17 | "redux", 18 | "react", 19 | "frontend", 20 | "reactive", 21 | "hyperx" 22 | ], 23 | "author": "freeman-lab", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/freeman-lab/hxdx/issues" 27 | }, 28 | "homepage": "https://github.com/freeman-lab/hxdx#readme", 29 | "dependencies": { 30 | "hyperx": "^1.3.1", 31 | "lodash.foreach": "^4.1.0", 32 | "lodash.frompairs": "^4.0.1", 33 | "lodash.get": "^4.1.2", 34 | "lodash.map": "^4.2.1", 35 | "main-loop": "^3.2.0", 36 | "virtual-dom": "^2.1.1" 37 | }, 38 | "devDependencies": { 39 | "browserify": "^13.0.0", 40 | "budo": "^8.0.4", 41 | "redux": "^3.3.1", 42 | "tap-spec": "^4.1.1", 43 | "tape": "^4.5.0", 44 | "testron": "^1.2.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/all.js: -------------------------------------------------------------------------------- 1 | require('./simple') 2 | require('./connect.js') 3 | -------------------------------------------------------------------------------- /test/connect.js: -------------------------------------------------------------------------------- 1 | var forEach = require('lodash.foreach') 2 | var test = require('tape') 3 | var redux = require('redux') 4 | 5 | var hxdx = require('../index.js') 6 | var hx = hxdx.hx 7 | 8 | var name = require('../examples/connect/components/name') 9 | var list = require('../examples/connect/components/list') 10 | var reducer = require('../examples/connect/reducer') 11 | 12 | var store = redux.createStore(reducer, { 13 | name: 'Hello', 14 | greetings: [ 15 | 'Hello, ', 16 | 'Shalom, ', 17 | 'Hola, ' 18 | ] 19 | }) 20 | 21 | test('connect-wrapper', function (t) { 22 | var wrapper = function (state) { 23 | return hx` 24 |
    25 |
    ${name()}
    26 |
    ${list()}
    27 |
    28 | ` 29 | } 30 | 31 | hxdx.render(wrapper, store) 32 | 33 | var el = document.querySelector('#name') 34 | 35 | t.equal(el.value, 'Hello') 36 | t.equal(el.id, 'name') 37 | t.end() 38 | }) 39 | 40 | test('connect-dispatch', function (t) { 41 | var wrapper = function (state) { 42 | return hx` 43 |
    44 |
    ${name()}
    45 |
    ${list()}
    46 |
    47 | ` 48 | } 49 | 50 | hxdx.render(wrapper, store) 51 | 52 | var el = document.querySelector('#name') 53 | t.equal(el.value, 'Hello') 54 | el.value = 'World' 55 | t.equal(el.value, 'World') 56 | el.oninput({ target: { value: el.value } }) 57 | 58 | setTimeout(function () { 59 | var items = document.querySelectorAll('#greeting') 60 | forEach(items, function (item) { 61 | if (item.innerHTML !== 'World') { 62 | t.fail(item.innerHTML + ' !== ' + 'World') 63 | } 64 | }) 65 | t.pass('all greeting names were equal to World') 66 | t.end() 67 | }, 2000) 68 | }) 69 | -------------------------------------------------------------------------------- /test/simple.js: -------------------------------------------------------------------------------- 1 | var hxdx = require('../index.js') 2 | var hx = hxdx.hx 3 | var dx = hxdx.dx 4 | var test = require('tape') 5 | var redux = require('redux') 6 | 7 | var reducer = function (state, action) { 8 | switch (action.type) { 9 | case 'INCREMENT': 10 | return state + 1 11 | default: 12 | return state 13 | } 14 | } 15 | 16 | var store = redux.createStore(reducer, 0) 17 | 18 | test('simple-display', function (t) { 19 | var display = function (state) { 20 | return hx`
    ${state}
    ` 21 | } 22 | 23 | hxdx.render(display, store) 24 | 25 | var el = document.querySelector('#display') 26 | 27 | t.equal(el.innerHTML, '0') 28 | t.equal(el.id, 'display') 29 | t.end() 30 | }) 31 | 32 | test('simple-root', function (t) { 33 | var display = function (state) { 34 | return hx`
    ${state}
    ` 35 | } 36 | 37 | var root = document.body.appendChild(document.createElement('div')) 38 | 39 | hxdx.render(display, store, root) 40 | 41 | var el = document.querySelector('#display') 42 | 43 | t.equal(el.innerHTML, '0') 44 | t.equal(el.id, 'display') 45 | t.end() 46 | }) 47 | 48 | test('simple-dispatch', function (t) { 49 | var display = function (state) { 50 | return hx`
    ${state}
    ` 51 | } 52 | 53 | var button = function (state) { 54 | function onclick () { 55 | dx({type: 'INCREMENT'}) 56 | } 57 | return hx`` 58 | } 59 | 60 | var app = function (state) { 61 | return hx`
    ${display(state)}${button()}
    ` 62 | } 63 | 64 | hxdx.render(app, store) 65 | 66 | document.querySelector('#button').click() 67 | 68 | setTimeout(function () { 69 | var el1 = document.querySelector('#display') 70 | t.equal(el1.innerHTML, '1') 71 | t.end() 72 | }, 100) 73 | }) 74 | --------------------------------------------------------------------------------