Last active
September 3, 2015 20:40
-
-
Save cpsubrian/b7a85d8bbc71ac67299f to your computer and use it in GitHub Desktop.
A stab at a redux history (undo/redo) 'transducer' utilizing Immutable.js
This file contains 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
/** | |
* History action creators. | |
*/ | |
import * as types from '../types' | |
export function resetHistory () { | |
return {type: types.HISTORY_RESET} | |
} | |
export function undo (options = {}) { | |
return {type: types.HISTORY_UNDO, ...options} | |
} | |
export function redo (options = {}) { | |
return {type: types.HISTORY_REDO, ...options} | |
} | |
export function revert (index) { | |
return {type: types.HISTORY_REVERT, index} | |
} |
This file contains 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 Immutable from 'immutable' | |
import _ from 'lodash' | |
import { | |
HISTORY_RESET, | |
HISTORY_UNDO, | |
HISTORY_REDO, | |
HISTORY_REVERT | |
} from '../types' | |
const MAX_SIZE = 200 | |
const initialState = { | |
cursor: -1, | |
snapshot: null, | |
actions: Immutable.List() | |
} | |
export default function historyTransducer (reducer) { | |
const reducers = { | |
[HISTORY_RESET]: (state) => { | |
return {...state, history: initialState} | |
}, | |
[HISTORY_UNDO]: (state, {ignore}) => { | |
if (state.history) { | |
let {cursor, actions, snapshot} = state.history | |
if (cursor >= 0) { | |
let index = actions.findLastIndex((action, i) => { | |
if (i <= cursor) { | |
if (ignore && ignore.indexOf(action.type) >= 0) { | |
cursor-- | |
} | |
return (i < cursor) | |
} | |
}) | |
if (index < state.history.cursor) { | |
// Apply actions to snapshot. | |
let undoState = {...snapshot} | |
for (let i = 0; i <= index; i++) { | |
undoState = reducer(undoState, actions.get(i)) | |
} | |
// Merge and return undo state. | |
return {...undoState, history: {...state.history, cursor: index}} | |
} | |
} | |
} | |
return state | |
}, | |
[HISTORY_REDO]: (state, {ignore}) => { | |
if (state.history) { | |
let {cursor, actions} = state.history | |
if (cursor < actions.size - 1) { | |
let index = actions.findIndex((action, i) => { | |
if (i >= cursor) { | |
if (ignore && ignore.indexOf(action.type) >= 0) { | |
cursor++ | |
} | |
return (i > cursor) | |
} | |
}) | |
if (index >= 0) { | |
// Apply actions to cursor state. | |
let redoState = {...state} | |
for (let i = cursor + 1; i <= index; i++) { | |
redoState = reducer(redoState, actions.get(i)) | |
} | |
// Merge and return redo state. | |
return {...redoState, history: {...state.history, cursor: index}} | |
} | |
} | |
} | |
return state | |
}, | |
[HISTORY_REVERT]: (state, {index}) => { | |
// @todo Implement this if needed. | |
return state | |
}, | |
__SAVE: (state, action) => { | |
let history = state.history || initialState | |
let {cursor, snapshot} = history | |
if (!snapshot) { | |
snapshot = _.omit(state, 'history') | |
} | |
// Update actions. | |
let actions = history.actions.withMutations((actions) => { | |
// Clear history after cursor. | |
for (let i = actions.size - 1; i > cursor; i--) { | |
actions = actions.pop() | |
} | |
// If we're at MAX_SIZE, shift off action and update snapshot. | |
if (actions.size >= MAX_SIZE) { | |
let first = actions.first() | |
snapshot = reducer(snapshot, first) | |
actions.shift() | |
} | |
// Add action to the stack. | |
actions.push(action) | |
}) | |
// Merge history into state and return. | |
return { | |
...state, | |
history: { | |
cursor: actions.size - 1, | |
snapshot, | |
actions | |
} | |
} | |
} | |
} | |
return function (state, action) { | |
if (reducers[action.type]) { | |
return reducers[action.type]({...state, ...reducer(state, action)}, action) | |
} else { | |
return {...reducers['__SAVE'](state, action), ...reducer(state, action)} | |
} | |
} | |
} |
This file contains 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
/** | |
* Example store initialization. | |
*/ | |
import {createStore, applyMiddleware, compose} from 'redux' | |
import reducers from './reducers' | |
import historyTransducer from './historyTransducer' | |
const initialState = {} | |
const store = createStore(historyTransducer(reducers), initialState) | |
export default store |
This file contains 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
/** | |
* Example action type constants. | |
*/ | |
// Feed Fetching | |
export const FEED_FETCH = 'FEED_FETCH' | |
export const FEED_FETCH_FAILED = 'FEED_FETCH_FAILED' | |
export const FEED_FETCH_SUCCESS = 'FEED_FETCH_SUCCESS' | |
// History | |
export const HISTORY_RESET = 'HISTORY_RESET' | |
export const HISTORY_UNDO = 'HISTORY_UNDO' | |
export const HISTORY_REDO = 'HISTORY_REDO' | |
export const HISTORY_REVERT = 'HISTORY_REVERT' |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment