ngrx-undo-reducer
Heavily inspired by redux-undo
See Demo
Installation
# NPM
npm install ngrx-undo-reducer
# Yarn
yarn add ngrx-undo-reducer
API
AOT Builds (--prod)
Need to use InjectionToken
to support AOT builds due to Function call of undoable()
.
Without it, angular-cli will throw the following error: Function calls are not supported in decorators but 'undoable' was called.
import { undoable } from 'ngrx-undo-reducer';
const config: UndoableOptions<MyState> = { ... }
const reducers: ActionReducerMap<MyState> = {
stateProp: undoable(fromStateProp.reducer, config),
};
export const reducersToken = new InjectionToken<ActionReducerMap<MyState>>(
'MyState Reducers',
{ factory: () => reducers }
);
@NgModule({
imports: [
StoreModule.forRoot(reducersToken)
// ...
]
// ...
})
export class MyModule {}
For non-AOT builds
You can import reducers as you normally would.
import { undoable } from 'ngrx-undo-reducer';
const config: UndoableOptions<MyState> = { ... }
export const reducers: ActionReducerMap<MyState> = {
stateProp: undoable(fromStateProp.reducer, config),
};
@NgModule({
imports: [
StoreModule.forRoot(reducers)
// ...
]
// ...
})
export class MyModule {}
History API
Wrapping your reducer with undoable makes the state look like this:
{
past: [...pastStatesHere...],
present: {...currentStateHere...},
future: [...futureStatesHere...],
bookmark: {...bookmarkedStateHere...}
}
Configuration
A configuration object can be passed to undoable()
like this (values shown
are default values):
undoable(reducer, {
limit: undefined, // set to a number to turn on a limit for the history
filter: () => true, // see `Filtering Actions`
undoType: ActionTypes.Undo, // define a custom action type for this undo action
redoType: ActionTypes.Redo, // define a custom action type for this redo action
clearHistoryTypes: [ActionTypes.ClearHistory], // define several action types that would clear the history
bookmarkType: ActionTypes.Bookmark, // define action type that would bookmark the state passed via action.payload or will take state.present if no payload was passed.
revertToBookmarkTypes: [ActionTypes.RevertToBookmark], // define action types that would revert state to the bookmarked state
initTypes: [ActionTypes.Init], // will revert to initialState
debug: false, // set to `true` to turn on debugging
neverSkipReducer: false // prevent undoable from skipping the reducer on undo/redo and clearHistoryType actions
});
Filtering Actions
If you don't want to include every action in the undo/redo history, you can add
a filter
function to undoable
. This is useful for, for example, excluding
actions that were not triggered by the user.
ngrx-undo-reducer
provides you with the includeActions
and excludeActions
helpers
for basic filtering. They should be imported like this:
import { undoable, includeActions, excludeActions } from 'ngrx-undo-reducer';
// Single Action
undoable(reducer, { filter: includeActions(SOME_ACTION) });
undoable(reducer, { filter: excludeActions(SOME_ACTION) });
// Multiple Actions
undoable(reducer, { filter: includeActions(SOME_ACTION, SOME_OTHER_ACTION) });
undoable(reducer, { filter: excludeActions(SOME_ACTION, SOME_OTHER_ACTION) });
Custom Filters
If you want to create your own filter, pass in a function with the signature
(action, currentState, previousHistory)
. For example:
undoable(reducer, {
filter: function filterActions(action, currentState, previousHistory) {
return action.type === SOME_ACTION; // only add to history if action is SOME_ACTION
}
});
// The entire `history` state is available to your filter, so you can make
// decisions based on past or future states:
undoable(reducer, {
filter: function filterState(action, currentState, previousHistory) {
let { past, present, future } = previousHistory;
return future.length === 0; // only add to history if future is empty
}
});
Combining Filters
You can also use our helper to combine filters.
import { undoable, combineFilters } from 'ngrx-undo-reducer';
function isActionSelfExcluded(action) {
return action.wouldLikeToBeInHistory;
}
function areWeRecording(action, state) {
return state.recording;
}
undoable(reducer, {
filter: combineFilters(isActionSelfExcluded, areWeRecording)
});