├── .gitignore ├── .eslintrc ├── .travis.yml ├── package.json ├── HISTORY.md ├── test └── index.js ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["standard"] 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '4' 4 | cache: 5 | directories: 6 | - node_modules 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deku-stateful", 3 | "version": "1.7.0", 4 | "description": "Keep states in a Deku component", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node test/index.js | tap-spec", 8 | "test:watch": "tape-watch test/index.js -r deku -p tap-spec" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/rstacruz/deku-stateful.git" 13 | }, 14 | "keywords": [ 15 | "deku", 16 | "state", 17 | "component", 18 | "virtual", 19 | "dom" 20 | ], 21 | "author": "Rico Sta. Cruz ", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/rstacruz/deku-stateful/issues" 25 | }, 26 | "homepage": "https://github.com/rstacruz/deku-stateful#readme", 27 | "dependencies": { 28 | "object-assign": "4.0.1", 29 | "simpler-debounce": "1.0.0" 30 | }, 31 | "devDependencies": { 32 | "decca": "2.0.0", 33 | "deku": "2.0.0-rc12", 34 | "eslint": "1.10.3", 35 | "eslint-config-standard": "4.4.0", 36 | "eslint-plugin-standard": "1.3.1", 37 | "jsdom": "7.2.2", 38 | "jsdom-global": "1.3.0", 39 | "tap-spec": "4.1.1", 40 | "tape": "4.4.0", 41 | "tape-eslint": "1.2.0", 42 | "tape-plus": "1.0.0", 43 | "tape-watch": "1.2.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ## [v1.7.0] 2 | > Sep 20, 2016 3 | 4 | - Implement `getState()`. (Related: [#4], [@11111000000]) 5 | 6 | [v1.7.0]: https://github.com/rstacruz/deku-stateful/compare/v1.6.0...v1.7.0 7 | 8 | ## [v1.6.0] 9 | > Sep 20, 2016 10 | 11 | - [#7] - Fix issue with onRemove not being able to access state. ([#8], [@borisirota]) 12 | 13 | [v1.6.0]: https://github.com/rstacruz/deku-stateful/compare/v1.5.0...v1.6.0 14 | 15 | ## [v1.5.0] 16 | > Aug 15, 2016 17 | 18 | - Support [Decca](http://ricostacruz.com/decca)'s function-only components. 19 | 20 | [v1.5.0]: https://github.com/rstacruz/deku-stateful/compare/v1.4.0...v1.5.0 21 | 22 | ## [v1.4.0] 23 | > Jan 29, 2016 24 | 25 | - [#2] - Fix issue with states being reset randomly. ([#3], [@11111000000]) 26 | 27 | [v1.4.0]: https://github.com/rstacruz/deku-stateful/compare/v1.3.0...v1.4.0 28 | 29 | ## [v1.3.0] 30 | > Jan 25, 2016 31 | 32 | - [#1] - Add onCreate hook. 33 | 34 | [v1.3.0]: https://github.com/rstacruz/deku-stateful/compare/v1.2.0...v1.3.0 35 | 36 | ## [v1.2.0] 37 | > Jan 10, 2016 38 | 39 | - Make state changes immutable, allowing you to run `oldstate === newstate` to check for deep equality. 40 | 41 | [v1.2.0]: https://github.com/rstacruz/deku-stateful/compare/v1.1.0...v1.2.0 42 | 43 | ## [v1.1.0] 44 | > Jan 10, 2016 45 | 46 | - Remove `shouldUpdate` support. 47 | 48 | [v1.1.0]: https://github.com/rstacruz/deku-stateful/compare/v1.0.0...v1.1.0 49 | 50 | ## [v1.0.0] 51 | > Jan 10, 2016 52 | 53 | Initial release. 54 | 55 | [v1.0.0]: https://github.com/rstacruz/deku-stateful/tree/v1.0.0 56 | [#1]: https://github.com/rstacruz/deku-stateful/issues/1 57 | [#2]: https://github.com/rstacruz/deku-stateful/issues/2 58 | [#3]: https://github.com/rstacruz/deku-stateful/issues/3 59 | [@11111000000]: https://github.com/11111000000 60 | [#7]: https://github.com/rstacruz/deku-stateful/issues/7 61 | [#8]: https://github.com/rstacruz/deku-stateful/issues/8 62 | [@borisirota]: https://github.com/borisirota 63 | [#4]: https://github.com/rstacruz/deku-stateful/issues/4 64 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | require('jsdom-global')() 2 | 3 | var deku = require('deku') 4 | var stateful = require('../index') 5 | var h = deku.element 6 | var describe = require('tape-plus').describe 7 | 8 | describe('deku-stateful', function (it) { 9 | var el 10 | 11 | it.beforeEach(function (t) { 12 | el = document.createElement('div') 13 | }) 14 | 15 | function render (component) { 16 | var render = deku.dom.createRenderer(el, dispatch) 17 | function dispatch () { render(h(component)) } 18 | dispatch() 19 | } 20 | 21 | it('works', function (t) { 22 | var component = stateful({ 23 | render: function (model) { 24 | return h('div', {}, 'hello world') 25 | } 26 | }) 27 | 28 | render(component) 29 | t.equal(el.innerHTML, '
hello world
', 'first render') 30 | }) 31 | 32 | it('works with pure components', function (t) { 33 | function component (model) { 34 | return h('div', {}, 'hello world') 35 | } 36 | 37 | render(component) 38 | t.equal(el.innerHTML, '
hello world
', 'first render') 39 | }) 40 | 41 | it('supports initialState', function (t) { 42 | t.plan(2) 43 | 44 | var component = stateful({ 45 | initialState: function (model) { 46 | t.pass('initialstate called') 47 | return { name: 'jake' } 48 | }, 49 | 50 | render: function (model) { 51 | return h('div', {}, 'hello ', model.state.name) 52 | } 53 | }) 54 | 55 | render(component) 56 | t.equal(el.innerHTML, '
hello jake
', 'first render') 57 | }) 58 | 59 | it('supports setState', function (t, end) { 60 | t.plan(4) // 2 dispatch, 2 renders 61 | var render = deku.dom.createRenderer(el, dispatch) 62 | 63 | function dispatch () { 64 | t.pass('dispatch called') 65 | render(h(component)) 66 | } 67 | 68 | var component = stateful({ 69 | initialState: function (model) { 70 | return { name: 'jake' } 71 | }, 72 | 73 | render: function (model) { 74 | setTimeout(function () { 75 | if (model.getState().name === 'jake') { 76 | model.setState({ name: 'john' }) 77 | } 78 | }) 79 | return h('div', {}, 'hello ', model.getState().name) 80 | } 81 | }) 82 | 83 | dispatch() 84 | 85 | t.equal(el.innerHTML, '
hello jake
', 'first render') 86 | 87 | setTimeout(function () { 88 | t.equal(el.innerHTML, '
hello john
', 'next render') 89 | end() 90 | }, 100) 91 | }) 92 | }) 93 | 94 | require('tape')('eslint', require('tape-eslint')()) 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # deku-stateful 2 | 3 | > Keep state in Deku components 4 | 5 | Deku v2 has no states in components. This is a higher-order component that adds `state` and `setState` to the model. 6 | See this [conversation here](https://github.com/dekujs/deku/issues/337#issuecomment-168034492). 7 | 8 | Compatible with Deku 2.0.0 (tested with 2.0.0-rc11) and Decca 2.0.0. 9 | 10 | [![Status](https://travis-ci.org/rstacruz/deku-stateful.svg?branch=master)](https://travis-ci.org/rstacruz/deku-stateful "See test builds") 11 | 12 | ```js 13 | import stateful from 'deku-stateful' 14 | 15 | function initialState () { 16 | return { clicked: 0 } 17 | } 18 | 19 | function render ({ getState, setState }) { 20 | return
21 | Clicked { getState().clicked } times. 22 | 25 |
26 | } 27 | 28 | export default stateful({ initialState, render }) 29 | ``` 30 | 31 | ## Example 32 | 33 | - [Tabs example](https://jsfiddle.net/rstacruz/jwLncxfd/) 34 | - [Simple counter example](https://jsfiddle.net/rstacruz/m6mkac75/) 35 | 36 | ## API 37 | 38 | ### render, onCreate, onUpdate, onRemove 39 | 40 | The `render` function and the lifecycle hooks will also be passed `getState` and `setState`. 41 | 42 | ```js 43 | function render({ getState, setState }) { 44 | } 45 | ``` 46 | 47 | - `setState(object)` — Updates the state when called. When `setState` is ran, it will queue up changes and dispatch an event like `dispatch({ type: 'UI_STATE_CHANGE' })`. This is meant to be picked up by your Redux store, which we're assuming will retrigger a `render()` when called. 48 | - `getState()` — Returns the current state. 49 | - `state` — The current state; it's preferred to use `getState()` instead, but it's here for legacy compatibility. 50 | 51 | ### initialState 52 | 53 | Your component can have an `initialState` function. Return the first state here. 54 | 55 | ```js 56 | function initialState ({ props }) { 57 | return { clicked: false } 58 | } 59 | 60 | export default stateful({ initialState, render }) 61 | ``` 62 | 63 | ## Thanks 64 | 65 | **deku-stateful** © 2016+, Rico Sta. Cruz. Released under the [MIT] License.
66 | Authored and maintained by Rico Sta. Cruz with help from contributors ([list][contributors]). 67 | 68 | > [ricostacruz.com](http://ricostacruz.com)  ·  69 | > GitHub [@rstacruz](https://github.com/rstacruz)  ·  70 | > Twitter [@rstacruz](https://twitter.com/rstacruz) 71 | 72 | [MIT]: http://mit-license.org/ 73 | [contributors]: http://github.com/rstacruz/deku-stateful/contributors 74 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var assign = require('object-assign') 2 | var debounce = require('simpler-debounce') 3 | 4 | /** 5 | * Decorates a Deku/Decca `Component` to add stateful-ness. 6 | * 7 | * @param {Component} component The component to render 8 | * @param {object=} options Options to be passed 9 | * @param {string=} options.action The action to be dispatched 10 | * @return {Component} 11 | */ 12 | 13 | function stateful (Component, options) { 14 | if (typeof Component === 'function') { 15 | Component = { render: Component } 16 | } 17 | 18 | if (!options) options = {} 19 | if (!options.action) options.action = { type: 'UI_STATE_CHANGE' } 20 | 21 | var states = {} 22 | var dispatch 23 | 24 | var update = debounce(function () { 25 | dispatch(options.action) 26 | }, 0) 27 | 28 | /* 29 | * Pass through `render()` with state and setState added. 30 | * Also, if it's the first render, call `initialState` if it exists. 31 | */ 32 | 33 | function render (model) { 34 | if (!states.hasOwnProperty(model.path)) { 35 | states[model.path] = (Component.initialState && Component.initialState(model)) 36 | } 37 | 38 | return Component.render(decorateModel(model)) 39 | } 40 | 41 | /* 42 | * Updates state and schedules a dispatch on the next tick. 43 | */ 44 | 45 | function setState (model) { 46 | return function (values) { 47 | if (typeof states[model.path] === 'object' && typeof values === 'object') { 48 | states[model.path] = assign({}, states[model.path], values) 49 | } else { 50 | states[model.path] = values 51 | } 52 | dispatch = model.dispatch 53 | update() 54 | } 55 | } 56 | 57 | /* 58 | * Clear out states on remove. 59 | */ 60 | 61 | function onRemove (model) { 62 | if (Component.onRemove) Component.onRemove(decorateModel(model)) 63 | delete states[model.path] 64 | } 65 | 66 | /* 67 | * Pass through `onUpdate()` with state and setState added. 68 | */ 69 | 70 | function onUpdate (model) { 71 | if (Component.onUpdate) Component.onUpdate(decorateModel(model)) 72 | } 73 | 74 | function onCreate (model) { 75 | if (Component.onCreate) Component.onCreate(decorateModel(model)) 76 | } 77 | 78 | /* 79 | * Adds `state` and `setState` to the model. 80 | */ 81 | 82 | function decorateModel (model) { 83 | return assign({}, model, { 84 | state: states[model.path], 85 | getState: function () { return states[model.path] }, 86 | setState: setState(model) 87 | }) 88 | } 89 | 90 | return assign({}, Component, { 91 | render: render, 92 | onRemove: onRemove, 93 | onUpdate: onUpdate, 94 | onCreate: onCreate 95 | }) 96 | } 97 | 98 | module.exports = stateful 99 | --------------------------------------------------------------------------------