Created
August 17, 2016 14:36
-
-
Save nhusher/cdae91e79932348b399dbb40790c0af2 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react' | |
// Call the provided function with the provided value as the only argument, | |
// if the call throws an exception, return the original value, otherwise | |
// return the result of applying fn to val | |
function attempt(fn, val) { | |
try { | |
return fn(val) | |
} catch (e) { | |
return val | |
} | |
} | |
// For a given initial condition, walk over the provided events and generate a new result | |
// state. `index` is provided as a convenience so that only a subset of the events can be | |
// walked over. | |
function getHistoricalState(initial, events, index) { | |
return events.slice(0, index + 1).reduce((state, event) => attempt(event, state), initial) | |
} | |
/* | |
Historian is a React component that tracks changes to value and provides callbacks to | |
traverse those changes via undo and redo. | |
Historian takes a single function as a child and calls that function with a hash of | |
values and functions for handling the undo/redo values. Example: | |
// A component that doubles the provided value any time it is clicked | |
function Doubler ({ value, change, undo, redo }) { | |
return <div> | |
<button onClick={() => change(value => value * 2)}> | |
{value} | |
</button> | |
<button onClick={undo}>undo</button> | |
<button onClick={redo}>redo</button> | |
</div> | |
} | |
// Wrap the Doubler with the Historian component: | |
function DoublerWithHistorian({ value }) { | |
return <Historian state={value}> | |
{({ onUpdate, onUndo, onRedo, state }) => | |
<Doubler change={onUpdate} undo={onUndo} redo={onRedo} value={state} />} | |
</Historian> | |
} | |
The Historian component tracks changes to the value (in this case, a number) and | |
knows how to go backwards and forwards in that value's history. Note that values | |
used by Historian should always be immutable, otherwise weird things will happen. | |
*/ | |
class Historian extends React.Component { | |
constructor(props, ...args) { | |
super(props, ...args) | |
this.state = { | |
events: [], | |
index: -1, | |
state: props.state | |
} | |
let goto = index => { | |
let | |
events = this.state.events, | |
state = this.props.state | |
if (index >= events.length || index < -1) throw new RangeError('Index out of bounds') | |
if (index === -1) { | |
this.setState({index, state}) | |
} else { | |
this.setState({ | |
index, | |
state: getHistoricalState(state, events, index) | |
}) | |
} | |
} | |
// Triggers an undo event: | |
this.undo = () => { | |
if (this.canUndo()) goto(this.state.index - 1) | |
} | |
// Triggers a redo event: | |
this.redo = () => { | |
if (this.canRedo()) goto(this.state.index + 1) | |
} | |
// Adds a new event to the stack of edits, clearing out any future edits along the way | |
this.update = event => { | |
let { events, state, index } = this.state | |
this.setState({ | |
events: events.slice(0, index + 1).concat(event), | |
state: attempt(event, state), | |
index: index + 1 | |
}) | |
} | |
// Wipe out the stack of edits | |
this.flush = () => { | |
this.setState({ | |
events: [], | |
index: -1 | |
}) | |
} | |
} | |
canUndo() { | |
return this.state.index > -1 | |
} | |
canRedo() { | |
return this.state.index < this.state.events.length - 1 | |
} | |
componentWillReceiveProps(nextProps) { | |
let { events, index } = this.state | |
// When we receive new props, regenerate the current state by reducing over the events | |
// that have been stored up: | |
this.setState({ | |
state: getHistoricalState(nextProps.state, events, index) | |
}) | |
} | |
render() { | |
// Passes the following values to the child function: | |
let rendered = this.props.children({ | |
onUndo: this.undo, // onUndo -- callback that a child can trigger when an undo needs to happen | |
onRedo: this.redo, // onRedo -- callback for redo | |
onUpdate: this.update, // onUpdate -- push a new event and update the state | |
onFlush: this.flush, // onFlush -- wipe out the undo/redo stack | |
canUndo: this.canUndo(), // canUndo -- boolean that's true there's an available undo action | |
canRedo: this.canRedo(), // canRedo -- same as above | |
state: this.state.state // state -- the current memoized state, passed to a child | |
}) | |
return rendered && React.Children.only(rendered) | |
} | |
} | |
Historian.propTypes = { | |
state: React.PropTypes.any, | |
children: React.PropTypes.func | |
} | |
export default Historian |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment