Use the Immutable. js-based instance code to cancel the redo function, and use immutable. js to redo
The browser becomes more and more powerful. Many functions originally provided by other clients are gradually transferred to the front-end, and front-end applications are becoming more and more complex. Many front-end applications, especially some online editing software, need to constantly process user interactions during operation, and provide the cancel redo function to ensure smooth interaction. However, implementing the cancel redo function for an application is not easy. The Redux official document describes how to undo the redo function in redux applications. Redux-based revocation is a top-down solution:redux-undo
After that, all the operations are changed to "Detachable", and then we constantly modify its configuration to make the revocation function more and more useful (this is alsoredux-undo
There are so many configuration items ).
This article uses a bottom-up approach. Taking a simple online drawing tool as an example, we use TypeScript and Immutable. js to implement a practical "undo redo" function. Shows the general effect:
Step 1: Determine which statuses require historical records and create custom State classes
Not all statuses require historical records. Many States are very trivial, especially those related to mouse or keyboard interaction. For example, when you drag a graph in a drawing tool, you need to set a "dragging" flag, the page displays the corresponding drag prompt Based on the tag. It is clear that the drag tag should not appear in the history. other statuses cannot be undone or do not need to be undone, for example, the webpage window size and the list of requests sent to the background.
To exclude States that do not require historical records, we encapsulate the remaining states with Immutable Record and define the State class:
// State. tsimport {Record, List, Set} from 'immutable' const StateRecord = Record ({items: List <Item> transform: d3.ZoomTransform selection: number}) // class encapsulation, it is easy to write TypeScript. It is best to use export default class State extends StateRecord {} In Immutable 4.0 or later {}
The example here is a simple online drawing tool. Therefore, the State class above contains three fields. items is used to record the drawing, transform is used to record the translation and scaling status of the canvas. selection indicates the ID of the currently selected image. Other states in the drawing tool, such as preview, automatic alignment configuration, and Operation prompt text, are not placed in the State class.
Step 2: Define the Action base class and create corresponding Action subclass for each different operation
Unlike redux-undo, we still adopt the command mode: defines the base class Action, and all the operations on the State are encapsulated as an Action instance; defines several Action subclasses, corresponding to different types of operations.
In TypeScript, it is easier to define the basic Action Class using Abstract Class.
// actions/index.tsexport default abstract class Action { abstract next(state: State): State abstract prev(state: State): State prepare(appHistory: AppHistory): AppHistory { return appHistory } getMessage() { return this.constructor.name }}
The next method of the Action object is used to calculate the next state, and the prev method is used to calculate the previous state 」. The getMessage method is used to obtain a brief description of the Action object. Using the getMessage method, we can display the user's Operation Records on the page, so that users can more easily understand what happened recently. The prepare method is used to "prepare" an Action before it is applied for the first time. The definition of AppHistory is provided later in this article.
Example of an Action subclass
The following AddItemAction is a typical Action subclass used to express "Add a new image 」.
// Actions/AddItemAction. tsexport default class AddItemAction extends Action {newItem: Item prevSelection: number constructor (newItem: Item) {super () this. newItem = newItem} prepare (history: AppHistory) {// This image is automatically selected after a new image is created. In order to cancel this operation, state. selection changes to the original value // The prepare method reads the "selection value before adding a graph" and saves it to this. prevSelection this. prevSelection = history. state. selection return history} next (state: State) {return state. setIn (['items ', this. newItem. id], this. newItem ). set ('selection ', this. newItemId)} prev (state: State) {return state. deleteIn (['items ', this. newItem. id]). set ('selection ', this. prevSelection)} getMessage () {return 'add item $ {this. newItem. id }'}}
Runtime behavior
When an application is running, user interaction generates an Action stream. Every time an Action object is generated, we call the next method of the object to calculate the next state, then, save the action to a list for future use. When you cancel the action, we retrieve the most recent Action from the action list and call its prev method. When an application is running, the next/prev method is called as follows:
// InitState is the initial state of the application at the beginning. // at a certain time point, action1 is generated for user interaction... state1 = action1.next (initState) // at another time, user interaction produces action2... state2 = action2.next (state1) // Similarly, action3 also appears... state3 = action3.next (state2) // you must call the prev method of the latest action state4 = action3.prev (state3). // If you cancel the action again, we can retrieve the corresponding action from the action list and call its prev method state5 = action2.prev (state4) // when redoing, retrieve the most recent undo action, call its next method state6 = action2.next (state5) Applied-Action
To facilitate the subsequent description, we will make a simple definition of Applied-Action: Applied-Action refers to the actions whose operation results have been reflected in the current application status; when the next method of an action is executed, the action changes to applied. When the prev method is executed, the action changes to unapplied.
Step 3: Create a history container AppHistory
The previous State class is used to indicate the application State at a certain time. Next we define the AppHistory class to indicate the application history. Similarly, we still use Immutable Record to define historical records. The state field is used to express the current application status, the list field is used to store all actions, and the index field is used to record the subscript of the recent applied-action. The historical application status can be calculated using the undo/redo method. The apply method is used to add and execute specific actions to AppHistory. The Code is as follows:
// AppHistory. tsconst emptyAction = Symbol ('empty-Action') export const undo = Symbol ('undo ') export type undo = typeof undo // support symbol after TypeScript2.7 is greatly enhanced export const redo = Symbol ('redo ') export type redo = typeof redoconst AppHistoryRecord = Record ({// current application state: new State (), // action list: List <Action> (), // index indicates the subscript of the last applied-action in the list. -1 indicates that no applied-action index:-1,}) export default class AppHistory extends AppHistoryRecord {pop () {// remove the last operation record return this. update ('LIST', list => list. splice (this. index, 1 )). update ('index', x => x-1)} getLastAction () {return this. index =-1? EmptyAction: this. list. get (this. index)} getNextAction () {return this. list. get (this. index + 1, emptyAction)} apply (action: Action) {if (action = emptyAction) return this. merge ({list: this. list. setSize (this. index + 1 ). push (action), index: this. index + 1, state: action. next (this. state),})} redo () {const action = this. getNextAction () if (action = emptyAction) return this. merge ({list: this. list, index: this. index + 1, state: action. next (this. state),})} undo () {const action = this. getLastAction () if (action = emptyAction) return this. merge ({list: this. list, index: this. index-1, state: action. prev (this. state ),})}}
Step 4: add the "cancel redo" Function
Assuming that other code in the application has converted the interaction on the webpage into a series of Action objects, the general code for adding the "undo redo" function to the application is as follows:
Type HybridAction = undo | redo | Action // If Redux is used to manage the status, use the following reudcer to manage the "states that require Historical Records" // then place the CER in the appropriate position of the application status tree: function reducer (history: AppHistory, action: HybridAction): AppHistory {if (action = undo) {return history. undo ()} else if (action = redo) {return history. redo ()} else {// general Action // note that you need to call the prepare method to make the action "ready" return action. prepare (history ). apply (action) }}// if it is in the Stream/Observable environment, use reducerconst action $: Stream <HybridAction >= generatedFromUserInteractionconst appHistory $: stream <AppHistory> = action $. fold (reducer, new AppHistory () const state $ = appHistory $. map (h => h. state) // If the callback function is used, use the reduceronActionHappen = function (action: HybridAction) {const nextHistory = CER (getLastHistory (), action) updateAppHistory (nextHistory) as follows) updateState (nextHistory. state )}
Step 5: merge actions to improve user interaction experience
Through the above four steps, the drawing tool has the cancel redo function, but the user experience of this function is not good. When a drawing is dragged in a drawing tool, the MoveItemAction generation frequency is the same as the occurrence frequency of the mousemove event. If we do not handle this situation, MoveItemAction will immediately pollute the entire history. We need to merge those actions that are too frequent so that each recorded action has a reasonable revocation granularity.
Before an Action is applied, its prepare method is called. We can modify the history in the prepare method. For example, for MoveItemAction, we determine whether the previous action is the same as the current action, and then decide whether to remove the previous action before the current action is applied. The Code is as follows:
// Actions/MoveItemAction. tsexport default class MoveItemAction extends Action {prevItem: Item // a graphical drag operation can be described by the following three variables: // startPos ), the position of the mouse (movingPos) during dragging and the ID constructor of the dragged image (readonly startPos: Point, readonly movingPos: Point, readonly itemId: number) {// readonly startPos: Point in the previous row is equivalent to the following two steps: // 1. define the startPos read-only field in MoveItemAction // 2. execute this in the constructor. startPos = startPos super ()} prepare (history: AppHistory) {const lastAction = history. getLastAction () if (lastAction instanceof MoveItemAction & lastAction. startPos = this. startPos) {// if the previous action is also a MoveItemAction, And the cursor start point of the drag operation is the same as the current action // we think the two actions are in the same move operation. prevItem = lastAction. prevItem return history. pop () // call the pop method to remove the last action} else {// record the status before the image is moved, used to cancel this. prevItem = history. state. items. get (this. itemId) return history} next (state: State): State {const dx = this. movingPos. x-this. startPos. x const dy = this. movingPos. y-this. startPos. y const moved = this. prevItem. move (dx, dy) return state. setIn (['items ', this. itemId], moved)} prev (state: State) {// When revoking, we can directly return state by using the saved prevItem. setIn (['items ', this. itemId], this. prevItem)} getMessage (){/*... */}}
From the code above, we can see that the prepare method not only prepares the action itself, but also prepares the history. Different Action types have different merging rules. After a reasonable prepare function is implemented for each Action, the user experience of revoking the redo function can be greatly improved.
Other notes
The undo redo function is highly dependent on non-variability. After an Action object is put into AppHistory. list, its referenced objects should be immutable. If the object referenced by the action changes, an error may occur during subsequent revocation. In this scheme, in order to easily record the necessary information when an operation occurs, the prepare method of the Action object allows in-situ modification, however, the prepare method is called only once before the action is put into the history record. Once the action enters the record list, it is immutable.
Summary
The above is all the steps to implement a practical redo function. Different front-end projects have different requirements and technical solutions, and the above Code may not be used in one line of your project. However, the idea of revoking redo should be the same, I hope this article will give you some inspiration.
The above section describes Immutable. js implements the instance code for revoking the redo function. I hope it will be helpful to you. If you have any questions, please leave a message and I will reply to you in a timely manner. Thank you very much for your support for the help House website!