diff --git a/examples/constrained-counters/Counter.js b/examples/constrained-counters/Counter.js new file mode 100644 index 0000000..73065f7 --- /dev/null +++ b/examples/constrained-counters/Counter.js @@ -0,0 +1,68 @@ +import React from 'react'; +import { Record } from 'immutable'; +import { Union, Maybe } from 'results'; +import { component, Update } from '../../spindle'; + + +const Model = Record({ + value: 0, + min: -Infinity, + max: Infinity, +}); + + +const Msg = Union({ + Increment: null, + Decrement: null, +}); + + +// this is just a helper, it's not a special funciton +const constrain = model => { + const { value, min, max } = model.toObject(); + return model.set('value', Math.max(min, Math.min(max, value))); +} + + +const handleProps = ({ min = -Infinity, max = Infinity }, model) => + Update({ model: constrain(model.merge({ min, max })) }); + + +const update = (msg, model) => Msg.match(msg, { + Increment: () => { + const newModel = constrain(model.update('value', v => v + 1)); + return Update({ + model: newModel, + emit: newModel.get('value'), + }); + }, + + Decrement: () => { + const newModel = constrain(model.update('value', v => v - 1)); + return Update({ + model: newModel, + emit: newModel.get('value'), + }); + }, +}); + + +const view = (model, BoundMsg) => ( +

+ + {model.get('value')} + +

+); + + +export default component('Counter', + { Model, Msg, handleProps, update, view }); diff --git a/examples/constrained-counters/Parent.js b/examples/constrained-counters/Parent.js new file mode 100644 index 0000000..5efdd96 --- /dev/null +++ b/examples/constrained-counters/Parent.js @@ -0,0 +1,50 @@ +import React from 'react'; +import { Record } from 'immutable'; +import { Union } from 'results'; +import { component, Update } from '../../spindle'; +import Counter from './Counter'; + + +const Model = Record({ + min: 0, + max: 0, +}); + + +const Msg = Union({ + SetMin: null, + SetMax: null, +}); + + +const update = (msg, model) => Msg.match(msg, { + SetMin: v => + Update({ model: model.set('min', v) }), + + SetMax: v => + Update({ model: model.set('max', v) }), +}); + + +const view = (model, BoundMsg) => ( +
+

min

+ + +

val

+ + +

max

+ +
+); + + +export default component('Parent', + { Model, Msg, update, view }); diff --git a/examples/constrained-counters/app.js b/examples/constrained-counters/app.js new file mode 100644 index 0000000..3475db0 --- /dev/null +++ b/examples/constrained-counters/app.js @@ -0,0 +1,6 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import Parent from './Parent'; + + +ReactDOM.render(, document.getElementById('app')); diff --git a/package.json b/package.json index 9ae7789..3c2ca21 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "watch-counter": "watchify examples/counter/app.js -d -t babelify --outfile bundle.js", "watch-pair": "watchify examples/pair-of-counters/app.js -d -t babelify --outfile bundle.js", "watch-n": "watchify examples/n-counters/app.js -d -t babelify --outfile bundle.js", - "watch-sum": "watchify examples/sum-counters/app.js -d -t babelify --outfile bundle.js" + "watch-sum": "watchify examples/sum-counters/app.js -d -t babelify --outfile bundle.js", + "watch-constrained": "watchify examples/constrained-counters/app.js -d -t babelify --outfile bundle.js" }, "author": "uniphil", "license": "GPL", diff --git a/spindle.js b/spindle.js index 058aee5..8892075 100644 --- a/spindle.js +++ b/spindle.js @@ -16,6 +16,16 @@ const bindMsg = (Msg, update, c) => .reduce((a, b) => Object.assign(a, b), {}); +const propsEq = (a, b) => { + for (const k in a) { + if (a[k] !== b[k] && k !== 'onEmit' && k !== 'children') return false; + } + const aCount = Object.keys(a).length - !!a.onEmit - !!a.children; + const bCount = Object.keys(b).length - !!b.onEmit - !!b.children; + return aCount === bCount; +}; + + export const Cmd = Immutable.Record({ run: null, abort: null, @@ -89,6 +99,7 @@ const createSpindle = () => { export function component(name, { Model = Immutable.Record({}), Msg = Union({}), + handleProps = (_, model) => Update({ model }), update, view, subscriptions = () => [], @@ -111,13 +122,19 @@ export function component(name, { this._isSpindleRoot = false; } this.getSpindle().register(this); - this.run(Update({ model: Model() })); + this.run(handleProps(this.props, Model())); } getChildContext() { return { spindle: this.context.spindle || this._spindle }; } + componentWillReceiveProps(nextProps) { + if (!propsEq(this.props, nextProps)) { + this.run(handleProps(nextProps, this.state.model)); + } + } + shouldComponentUpdate(_, nextState) { return !Immutable.is(nextState.model, this.state.model); } @@ -139,7 +156,7 @@ export function component(name, { const { model, cmds, emit } = update.toObject(); model && this.setState({ model }); cmds && this.getSpindle().pushCmds(this, cmds); - emit && this.props.onEmit && this.props.onEmit(emit); + typeof emit !== 'undefined' && this.props.onEmit && this.props.onEmit(emit); } render() {