There are many advantages to centralizing the status of your application in the Vuex store. One advantage is that all transactions are recorded. This allows you to use handy features, such as run-time debugging , where you can switch between previous states to separate execution tasks.
In this article, I will show you how to create the Undo / Redo function further on the Rollback / Return using Vuex, which works similarly to debugging during debug. This feature can be used in various scenarios, from complex forms to browser-based games.
You can check out the finished code here on Github and try the demo in this Codepen . I also created a plugin as an NPM module called vuex-undo-redo if you want to use it in a project.
Note: this article was originally posted here on the developer blog Vue.js 2017/11/13
To make this function reusable, we will create it as a Vue plugin. This function requires us to add some methods and data to the Vue instance, so we structure the plugin as a mixin.
module.exports = { install(Vue) { Vue.mixin({ // Code goes here }); } };
To use it in the project, we can simply import the plugin and connect it:
import VuexUndoRedo from './plugin.js'; Vue.use(VuexUndoRedo);
The work of the feature will be to roll back the last mutation if the user wants to cancel, and then reapply it if he wants to repeat. How do we do this?
The first possible approach is to take "snapshots" of the state of the repository after each mutation and place the snapshot into an array. To undo / redo, we can get the correct snapshot and replace it with the storage status.
The problem with this approach is that the state of the repository is a JavaScript object. When you put a JavaScript object in an array, you simply place an object reference. A naive implementation, like the following, will not work:
var state = { ... }; var snapshot = []; // Push the first state snapshot.push(state); // Push the second state state.val = "new val"; snapshot.push(state); // Both snapshots are simply a reference to state console.log(snapshot[0] === snapshot[1]); // true
The snapshot approach will require that you first make a state clone before push. Given that the state of Vue becomes reactive due to the automatic addition of get and set functions, it does not work well with cloning.
Another possible approach is to register each recorded mutation. To cancel, we reset the repository to its initial state and then rerun the mutations; all but the last. Return a similar concept.
Given the principles of Flux, restarting mutations from the same initial state should ideally recreate the state. Since this is a cleaner approach than the first, let's continue.
Vuex offers an API method for subscribing to mutations that we can use to register them. We will set this to the created
hook. In the callback, we simply put the mutation into an array, which can later be re-run.
Vue.mixin({ data() { return { done: [] } }, created() { this.$store.subscribe(mutation => { this.done.push(mutation); } } });
To cancel the mutation, we will clear the repository, and then rerun all mutations except the last one. This is how the code works:
pop
array method to remove the last mutation.EMPTY_STATE
mutation (explained below)pop
. const EMPTY_STATE = 'emptyState'; Vue.mixin({ data() { ... }, created() { ... }, methods() { undo() { this.done.pop(); this.$store.commit(EMPTY_STATE); this.done.forEach(mutation => { this.$store.commit(`${mutation.type}`, mutation.payload); this.done.pop(); }); } } });
Whenever this plugin is used, the developer must implement a mutation in his repository called emptyState. The challenge is to bring the store back to its original state so that it is ready for recovery from scratch.
The developer must do this on his own, because the plugin we create does not have access to the store, only to the Vue instance. Here is an example implementation:
new Vuex.Store({ state: { myVal: null }, mutations: { emptyState() { this.replaceState({ myval: null }); } } });
Returning to our plugin, the emptyState
mutation should not be added to our done
list, since we do not want to re-fix it in the rollback process. Prevent this with the following logic:
Vue.mixin({ data() { ... }, created() { this.$store.subscribe(mutation => { if (mutation.type !== EMPTY_STATE) { this.done.push(mutation); } }); }, methods() { ... } });
Let's create a new data property, undone
which will be an array. When we remove the last mutation from done
in the rollback process, we put it in this array:
Vue.mixin({ data() { return { done: [], undone: [] } }, methods: { undo() { this.undone.push(this.done.pop()); ... } } });
Now we can create a redo
method that will simply take the last undone
mutation undone
and re-fix it.
methods: { undo() { ... }, redo() { let commit = this.undone.pop(); this.$store.commit(`${commit.type}`, commit.payload); } }
If the user initiates a cancellation one or more times, and then makes a new new commit, the contents of undone
will be invalidated. If this happens, we must empty out undone
.
We can detect new commits from our callback subscription when adding a commit. However, the logic is tricky, since the callback has no obvious way to know what a new commit is and what undo / redo is.
The easiest approach is to set the newMutation flag. It will be true by default, but the rollback and return methods will temporarily set it to false. If mutation is set to true, then the subscribe
callback clears the undone
array.
module.exports = { install(Vue) { Vue.mixin({ data() { return { done: [], undone: [], newMutation: true }; }, created() { this.$store.subscribe(mutation => { if (mutation.type !== EMPTY_STATE) { this.done.push(mutation); } if (this.newMutation) { this.undone = []; } }); }, methods: { redo() { let commit = this.undone.pop(); this.newMutation = false; this.$store.commit(`${commit.type}`, commit.payload); this.newMutation = true; }, undo() { this.undone.push(this.done.pop()); this.newMutation = false; this.$store.commit(EMPTY_STATE); this.done.forEach(mutation => { this.$store.commit(`${mutation.type}`, mutation.payload); this.done.pop(); }); this.newMutation = true; } } }); }, }
The main functionality is now complete! Add the plugin to your own project or to my demo to test it.
In my demonstration, you will notice that the cancel and return buttons are disabled if their functionality is not currently possible. For example, if there were no commits yet, you obviously cannot undo or redo. A developer using this plugin may want to implement this functionality.
To allow this, the plugin can provide two calculated properties canUndo
and canRedo
as part of the public API. This is trivial to implement:
module.exports = { install(Vue) { Vue.mixin({ data() { ... }, created() { ... }, methods: { ... }, computed: {}, computed: { canRedo() { return this.undone.length; }, canUndo() { return this.done.length; } }, }); }, }
Source: https://habr.com/ru/post/433986/
All Articles