Skip to content

Instantly share code, notes, and snippets.

@nhusher
Created August 17, 2016 14:36
Show Gist options
  • Save nhusher/cdae91e79932348b399dbb40790c0af2 to your computer and use it in GitHub Desktop.
Save nhusher/cdae91e79932348b399dbb40790c0af2 to your computer and use it in GitHub Desktop.
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