Redux’s underlying pattern — which @ngrx/store brings to Angular 2 — eases several challenges when managing intricate user interfaces. Most notably, it dramatically simplifies Undo/Redo flows. This post explores that topic using my example for managing flight bookings.
To expose the intended functionality, the state is extended with two stacks. The undoStack collects all prior states that a developer might restore through Undo. In contrast, the redoStack holds every undone state that the application can bring back via Redo:
export var initialBoardingState: BoardingState = {
undoStack: [],
redoStack: [],
buchungen: [],
message: "",
statistik: {
countBoarded: 0,
countBooked: 0,
countCheckedIn: 0
}
};
For clarity, the sample represents both stacks as simple arrays. Each action appends the previous state to the end of the undo stack. Because Redo is only feasible right after an Undo — never following a standard action — the redoStack is reset to an empty array:
function buchungStateChanged(state: BoardingState, buchung): BoardingState {
[...]
return {
undoStack: [...state.undoStack, state],
redoStack: [],
buchungen: [...],
statistik: [...],
message: ""
};
}
To add a new entry to an immutable array, the example leverages the spread operator introduced with EcmaScript 6 (three dots as a prefix). The expression ...state.undoStack expands the array’s entries into the form eintrag1, eintrag2, ..., eintragN. Writing [...state.undoStack, state] appends the state element to that sequence and constructs a fresh array from it.
The action responsible for Undo is outlined below. It pops the final item from the undo stack — that element is the state slated for restoration. To let the user return to the current state via Redo, that state is pushed onto the new redo stack. Beyond that, the fresh state adopts the data owned by the one being restored.
function undo(state: BoardingState) {
var oldState = state;
var prevState = state.undoStack[state.undoStack.length-1];
// Neues Array ohne dem letzten Element mit slice erzeugen
var newUndoStack = state.undoStack.slice(0, state.undoStack.length-1);
return {
undoStack: newUndoStack,
redoStack: [...oldState.redoStack, oldState],
buchungen: prevState.buchungen,
message: prevState.message,
statistik: prevState.statistik,
}
}
Redo operates along similar lines: it removes the last element from the redo stack and pulls its properties into the new state. This holds true even for the undoStack carried by that element:
function redo(state: BoardingState) {
var oldState = state;
var redoState = state.redoStack[state.redoStack.length-1];
var newRedoStack = oldState.redoStack.slice(0, oldState.redoStack.length-1);
return {
undoStack: redoState.undoStack,
redoStack: newRedoStack,
message: redoState.message,
buchungen: redoState.buchungen,
statistik: redoState.statistik
}
}
