Form with undo/redo stack¶
This examples demonstrates how to implement undo/redo functionality with React Forms. Because React Forms keeps its state (value and validation info) immutable it is possible to make a state snapshot at any time with almost no overhead.
This particular examples only make a snapshot before destructive operations such as adding/removing a fieldset in repeating fieldset.
Implementation¶
Due to the dataflow provided by React Forms the implementation is quite compact.
UndoStack¶
var React = require('react')
var ReactForms = require('react-forms')
var Form = ReactForms.Form
var FormFor = ReactForms.FormFor
var Schema = ReactForms.schema.Schema
var List = ReactForms.schema.List
var Property = ReactForms.schema.Property
var RepeatingFieldset = ReactForms.RepeatingFieldset
First we define a reusable UndoStack mixin:
var UndoStack = {
getInitialState: function() {
return {undo: [], redo: []}
},
snapshot: function() {
var undo = this.state.undo.concat(this.getStateSnapshot())
this.setState({undo: undo, redo: []})
},
hasUndo: function() {
return this.state.undo.length > 0
},
hasRedo: function() {
return this.state.redo.length > 0
},
redo: function() {
this._undoImpl(true)
},
undo: function() {
this._undoImpl()
},
_undoImpl: function(isRedo) {
var undo = this.state.undo.slice(0)
var redo = this.state.redo.slice(0)
var snapshot
if (isRedo) {
if (redo.length === 0) {
return
}
snapshot = redo.pop()
undo.push(this.getStateSnapshot())
} else {
if (undo.length === 0) {
return
}
snapshot = undo.pop()
redo.push(this.getStateSnapshot())
}
this.setStateSnapshot(snapshot)
this.setState({undo: undo, redo: redo})
}
}
This mixin is completely reusable outside of React Forms, it expects a component which uses it to define getStateSnapshot() and setStateSnapshot(snapshot) methods which returns and installs state snapshots.
UndoControls¶
Next we define a simple undo controls component which renders two buttons for “undo” and “redo” actions and fire corresponding callbacks:
var UndoControls = React.createClass({
render: function() {
return (
<div className="UndoControls">
<button
disabled={!this.props.hasUndo}
onClick={this.props.onUndo}
type="button" className="button">
⟲ Undo
</button>
<button
disabled={!this.props.hasRedo}
onClick={this.props.onRedo}
type="button" className="button">
⟳ Redo
</button>
</div>
)
}
})
FormWithUndo¶
The final part is to define a custom Form component which renders UndoControls and mixes in UndoStack mixin:
var FormWithUndo = React.createClass({
mixins: [ReactForms.FormMixin, UndoStack],
getStateSnapshot: function() {
return {value: this.value()}
},
setStateSnapshot: function(snapshot) {
this.onValueUpdate(snapshot.value)
},
render: function() {
return this.transferPropsTo(
<form className="Form">
<UndoControls
hasUndo={this.hasUndo()}
hasRedo={this.hasRedo()}
onUndo={this.undo}
onRedo={this.redo}
/>
<RepeatingFieldset
onRemove={this.snapshot}
onAdd={this.snapshot} />
</form>
)
}
})
The FormWithUndo usage is no different than using an original Form component:
function Product(props) {
props = props || {}
return (
<Schema required={props.required} name={props.name} label={props.label}>
<Property name="name" label="Name" />
<Property type="number" name="price" label="Price" />
</Schema>
)
}
var Products = (
<List label="Products">
<Product />
</List>
)
React.renderComponent(
<FormWithUndo schema={Products} value={[{name: 'TV', price: 1000}]} />,
document.getElementById('example')
)