Skip to content

OleksandrSymonov/undo-redo-vuex

 
 

Repository files navigation

undo-redo-vuex

A Vuex plugin for module namespaced undo and redo functionality. This plugin takes inspiration from and extends the work of vuex-undo-redo.

Installation

yarn add undo-redo-vuex

Browser

<script
  type="text/javascript"
  src="node_modules/undo-redo-vuex/dist/undo-redo-vuex.min.js"
></script>

Module

import undoRedo from "undo-redo-vuex";

Usage

As a standard plugin for Vuex, undo-redo-vuex can be used with the following setup:

Named or basic store module

The scaffoldStore helper function will bootstrap a vuex store to setup the state, actions and mutations to work with the plugin.

import { scaffoldStore } from "undo-redo-vuex";

const state = {
  list: [],
  // Define vuex state properties as normal
};
const actions = {
  // Define vuex actions as normal
};
const mutations = {
  /*
   * NB: The emptyState mutation HAS to be impemented.
   * This mutation resets the state props to a "base" state,
   * on top of which subsequent mutations are "replayed"
   * whenever undo/redo is dispatched.
   */
  emptyState: state => {
    // Sets some state prop to an initial value
    state.list = [];
  },

  // Define vuex mutations as normal
};

export default scaffoldStore({
  state,
  actions,
  mutations,
  namespaced: true, // NB: do not include this is non-namespaced stores
});

Alternatively, the scaffoldState, scaffoldActions, and scaffoldMutations helper functions can be individually required to bootstrap the vuex store. This will expose canUndo and canRedo as vuex state properties which can be used to enable/disable UI controls (e.g. undo/redo buttons).

import {
  scaffoldState,
  scaffoldActions,
  scaffoldMutations,
} from "undo-redo-vuex";

const state = {
  // Define vuex state properties as normal
};
const actions = {
  // Define vuex actions as normal
};
const mutations = {
  // Define vuex mutations as normal
};

export default {
  // Use the respective helper function to scaffold state, actions and mutations
  state: scaffoldState(state),
  actions: scaffoldActions(actions),
  mutations: scaffoldMutations(mutations),
  namespaced: true, // NB: do not include this is non-namespaced stores
};

store/index.js

  • Namespaced modules
import Vuex from "vuex";
import undoRedo from "undo-redo-vuex";

// NB: The following config is used for namespaced store modules.
// Please see below for configuring a non-namespaced (basic) vuex store
export default new Vuex.Store({
  plugins: [
    undoRedo({
      // The config object for each store module is defined in the 'paths' array
      paths: [
        {
          namespace: "list",
          // Any mutations that you want the undo/redo mechanism to ignore
          ignoreMutations: ["addShadow", "removeShadow"],
        },
      ],
    }),
  ],
  /*
   * For non-namespaced stores:
   * state,
   * actions,
   * mutations,
   */
  // Modules for namespaced stores:
  modules: {
    list,
  },
});
  • Non-namespaced (basic) vuex store
import Vuex from "vuex";
import undoRedo from "undo-redo-vuex";

export default new Vuex.Store({
  state,
  getters,
  actions,
  mutations,
  plugins: [
    undoRedo({
      // NB: Include 'ignoreMutations' at root level, and do not provide the list of 'paths'.
      ignoreMutations: ["addShadow", "removeShadow"],
    }),
  ],
});

Accessing canUndo and canRedo properties

  • Vue SFC (.vue)
import { mapState } from "vuex";

const MyComponent = {
  computed: {
    ...mapState({
      undoButtonEnabled: "canUndo",
      redoButtonEnabled: "canRedo",
    }),
  },
};

Undoing actions with actionGroups

In certain scenarios, undo/redo is required on store actions which may consist of one or more mutations. This feature is accessible by including a actionGroup property in the payload object of the associated vuex action. Please refer to test/test-action-group-undo.js for more comprehensive scenarios.

  • vuex module
const actions = {
  myAction({ commit }, payload) {
    // An arbitrary label to identify the group of mutations to undo/redo
    const actionGroup = "myAction";

    // All mutation payloads should contain the actionGroup property
    commit("mutationA", {
      ...payload,
      actionGroup,
    });
    commit("mutationB", {
      someProp: true,
      actionGroup,
    });
  },
};
  • Undo/redo stack illustration
// After dispatching 'myAction' once
done = [
  { type: "mutationA", payload: { ...payload, actionGroup: "myAction" } },
  { type: "mutationB", payload: { someProp: true, actionGroup: "myAction" } },
];
undone = [];

// After dispatching 'undo'
done = [];
undone = [
  { type: "mutationA", payload: { ...payload, actionGroup: "myAction" } },
  { type: "mutationB", payload: { someProp: true, actionGroup: "myAction" } },
];

Working with undo/redo on mutations produced by side effects (i.e. API/database calls)

In Vue.js apps, Vuex mutations are often committed inside actions that contain asynchronous calls to an API/database service: For instance, commit("list/addItem", item) could be called after an axios request to PUT https://<your-rest-api>/v1/list. When undoing the commit("list/addItem", item) mutation, an appropriate API call is required to DELETE this item. The inverse also applies if the first API call is the DELETE method (PUT would have to be called when the commit("list/removeItem", item) is undone). View the unit test for this feature here.

This scenario can be implemented by providing the respective action names as the undoCallback and redoCallback fields in the mutation payload (NB: the payload object should be parameterized as an object literal):

const actions = {
  saveItem: async (({ commit }), item) => {
    await axios.put(PUT_ITEM, item);
    commit("addItem", {
      item,
      // dispatch("deleteItem", { item }) on undo()
      undoCallback: "deleteItem",
      // dispatch("saveItem", { item }) on redo()
      redoCallback: "saveItem"
    });
  },
  deleteItem: async (({ commit }), item) => {
    await axios.delete(DELETE_ITEM, item);
    commit("removeItem", {
      item,
      // dispatch("saveItem", { item }) on undo()
      undoCallback: "saveItem",
      // dispatch("deleteItem", { item }) on redo()
      redoCallback: "deleteItem"
    });
  }
};

const mutations = {
  // NB: payload parameters as object literal props
  addItem: (state, { item }) => {
    // adds the item to the list
  },
  removeItem: (state, { item }) => {
    // removes the item from the list
  }
};

Clearing the undo/redo stacks with the clear action

The internal done and undone stacks used to track mutations in the vuex store/modules can be cleared (i.e. emptied) with the clear action. This action is scaffolded when using scaffoldActions(actions) of scaffoldStore(store). This enhancement is described further in issue #7, with accompanying unit tests.

/**
 * Current done stack: [mutationA, mutation B]
 * Current undone stack: [mutationC]
 **/
this.$store.dispatch("list/clear");

await this.$nextTick();
/**
 * Current done stack: []
 * Current undone stack: []
 **/

Testing and test scenarios

Development tests are run using the Jest test runner. The ./tests/store directory contains a basic Vuex store with a namespaced list module.

The test blocks (each it() declaration) in ./tests/unit directory are grouped to mimic certain user interactions with the store, making it possible to track the change in state over time.

yarn test

Demo Vue.js app in tests/vue-undo-redo-demo

A demo Vue.js TODO application featuring this plugin is included in the tests/vue-undo-redo-demo directory. Example test specs for Vue.js unit testing with Jest, and E2E testing with Cypress are also provided.

API documentation and reference

Public API

store/plugins/undoRedo(options)function

The Undo-Redo plugin module

store/plugins/undoRedo:redo()

The Redo function - commits the latest undone mutation to the store, and pushes it to the done stack

store/plugins/undoRedo:undo()

The Undo function - pushes the latest done mutation to the top of the undone stack by popping the done stack and 'replays' all mutations in the done stack

store/plugins/undoRedo(options) ⇒ function

The Undo-Redo plugin module

Returns: function - plugin - the plugin function which accepts the store parameter

Param Type Description
options Object
options.namespace String The named vuex store module
options.ignoreMutations Array.<String> The list of store mutations (belonging to the module) to be ignored

store/plugins/undoRedo:redo()

The Redo function - commits the latest undone mutation to the store, and pushes it to the done stack

store/plugins/undoRedo:undo()

The Undo function - pushes the latest done mutation to the top of the undone stack by popping the done stack and 'replays' all mutations in the done stack

Internal functions (reference only)

store/plugins/undoRedo:pipeActions(actions)

Piping async action calls sequentially using Array.prototype.reduce to chain and initial, empty promise

store/plugins/undoRedo:getConfig(namespace)Object

Piping async action calls sequentially using Array.prototype.reduce to chain and initial, empty promise

store/plugins/undoRedo:setConfig(namespace, config)

Piping async action calls sequentially using Array.prototype.reduce to chain and initial, empty promise

store/plugins/undoRedo:pipeActions(actions)

Piping async action calls sequentially using Array.prototype.reduce to chain and initial, empty promise

Param Type Description
actions Array.<Object> The array of objects containing the each action's name and payload

store/plugins/undoRedo:getConfig(namespace) ⇒ Object

Piping async action calls sequentially using Array.prototype.reduce to chain and initial, empty promise

Returns: Object - config - The object containing the undo/redo stacks of the store module

Param Type Description
namespace String The name of the store module

store/plugins/undoRedo:setConfig(namespace, config)

Piping async action calls sequentially using Array.prototype.reduce to chain and initial, empty promise

Param Type Description
namespace String The name of the store module
config String The object containing the updated undo/redo stacks of the store module

About

A Vuex plugin for module namespaced undo and redo functionality

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • HTML 63.3%
  • TypeScript 21.0%
  • JavaScript 7.5%
  • Vue 5.4%
  • CSS 2.8%