├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── extras ├── MaterialUiFormikUndoControlBar.tsx └── formik-undo.gif ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── CheckpointPicker.ts ├── EditedFieldChangedCheckpointPicker.ts ├── FormikUndo.tsx ├── FormikUndoControlBar.tsx ├── FormikUndoProvider.tsx ├── WordEditingCheckpointPicker.ts ├── getFormikValuesDiff.ts ├── hooks.ts ├── index.ts ├── types.ts └── useFormikUndoAutoSave.tsx └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true, 6 | }, 7 | parserOptions: { 8 | ecmaVersion: 2017, 9 | sourceType: 'module', 10 | ecmaFeatures: { 11 | jsx: true, 12 | } 13 | }, 14 | plugins: [ 15 | '@typescript-eslint', 16 | "react", 17 | 'react-hooks', 18 | ], 19 | extends: [ 20 | 'eslint:recommended', 21 | 'plugin:@typescript-eslint/eslint-recommended', 22 | 'plugin:@typescript-eslint/recommended', 23 | 'react-app', 24 | 'plugin:react/recommended' 25 | ], 26 | rules: { 27 | '@typescript-eslint/explicit-function-return-type': 'off', 28 | '@typescript-eslint/no-empty-interface': 'off', 29 | '@typescript-eslint/camelcase': 'warn', 30 | '@typescript-eslint/member-delimiter-style': 'warn', 31 | '@typescript-eslint/no-inferrable-types': 'warn', 32 | '@typescript-eslint/no-non-null-assertion': 'off', 33 | '@typescript-eslint/no-empty-function': 'warn', 34 | '@typescript-eslint/semi': ['warn', 'always'], 35 | '@typescript-eslint/type-annotation-spacing': 'warn', 36 | '@typescript-eslint/interface-name-prefix': 'off', 37 | '@typescript-eslint/no-unused-vars': ['warn', { 'argsIgnorePattern': '(next)|(returns)|(of)|(type)|(_\\w)', ignoreRestSiblings: true }], 38 | '@typescript-eslint/no-explicit-any': 'off', 39 | 'array-bracket-spacing': ['warn', 'never'], 40 | 'arrow-parens': 'off', 41 | 'comma-spacing': ['warn', {"before": false, "after": true}], 42 | "indent": ['warn', 2, { SwitchCase: 1 }], 43 | 'interface-name': 'off', 44 | 'interface-over-type-literal': 'off', 45 | 'jsx-no-multiline-js': 'off', 46 | 'keyword-spacing': ['warn', { before: true, after: true }], 47 | 'max-classes-per-file': 'off', 48 | 'max-len': ['warn', { 'code': 120, ignoreRegExpLiterals: true, ignoreUrls: true, ignorePattern: '^(import|export)\\s.+\\sfrom\\s.+;?$' }], 49 | 'member-ordering': 'off', 50 | 'new-parens': 'warn', 51 | 'no-angle-bracket-type-assertion': 'off', 52 | 'no-bitwise': 'warn', 53 | 'no-consecutive-blank-lines': 'off', 54 | 'no-console': 'off', 55 | 'no-multi-spaces': ['warn', { exceptions: { 'Property': false } }], 56 | 'no-multiple-empty-lines': ['warn', { max: 2, maxBOF: 0, maxEOF: 0 }], 57 | 'no-empty': 'warn', 58 | 'no-extra-semi': 'off', 59 | 'no-return-await': 'off', 60 | 'no-shadowed-variable': 'off', 61 | 'no-trailing-whitespace': 'off', 62 | 'no-useless-escape': 'warn', 63 | 'object-curly-spacing': ['warn', 'always'], 64 | 'object-literal-sort-keys': 'off', 65 | 'ordered-imports': 'off', 66 | 'prefer-const': 'warn', 67 | 'quotemark': 'off', 68 | 'semi': 'off', // https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/semi.md 69 | 'space-infix-ops': ['warn', { 'int32Hint': false }], 70 | 'trailing-comma': 'off', 71 | 'variable-name': 'off', 72 | }, 73 | ignorePatterns: [ 74 | '**/node_modules/*', 75 | 'dist', 76 | 'extras', 77 | ] 78 | }; 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | build 3 | .vscode 4 | dist 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Pascal Heitz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Formik Undo 2 | 3 | Provide the ability to undo / redo modifications in a Formik form. 4 | 5 | Uses Typescript and React hooks. 6 | 7 | 8 | ![screenshot](https://github.com/OoDeLally/formik-undo/blob/master/extras/formik-undo.gif) 9 | 10 | 11 | Online Demo: https://codesandbox.io/s/github/OoDeLally/formik-undo-demo 12 | 13 | 14 | ## Setup 15 | 16 | ```bash 17 | npm install --save formik-undo 18 | ``` 19 | 20 | 21 | ```tsx 22 | import { FormikUndo } from 'formik-undo'; 23 | 24 | const autoSaveOptions = { 25 | throttleDelay: 10000, 26 | }; 27 | 28 | const App = () => { 29 | return ( 30 | 34 | 35 | 36 | 37 | 38 | ); 39 | }; 40 | ``` 41 | 42 | Provider's props are as follow: 43 | 44 | | Name | Type | Default | Description | 45 | | -----------------------------|--------------------------------|---------|--------------------------------------------------------------------------------| 46 | | `autoSave` | `boolean \| { ...options }` | `true` | If `false`, does not autosave.
If `true`, autosave with the default options.
If `object` autosave with the provided options. | 47 | | `autoSave.throttleDelay` | `number` | 2000 | Frequency of autosaving in millisecond.
If `0`, save at every modification. | 48 | | `autoSave.saveOnFieldChange` | `boolean` | `true` | If ``true``, save a checkpoint everytime the modified field is different from the previously modified. This is useful to save the final value of a input after the user moves to another input.
If `false`, only the whole formik `values` object is considered and different fields changes may be aggregated from one checkpoint to another. | 49 | | `autoSave.preventWordCutting`| `boolean` | `true` | If ``true``, when editing a string value, don't save in the middle of a word (experimental). | 50 | 51 | If you need a finer save trigger, write your own hook using the methods provided by `useFormikContext()` and `useFormikUndo()`. 52 | 53 | 54 | 55 | ## Usage 56 | 57 | ```tsx 58 | import { useFormikUndo } from 'formik-undo'; 59 | 60 | const MyComponent = () => { 61 | const { reset, undo, redo, saveCheckpoint } = useFormikUndo(); 62 | // Do stuff 63 | return ( 64 |
65 | ... 66 |
67 | ); 68 | }; 69 | ``` 70 | 71 | | Name | Type | Description | 72 | | --------------------------- |-------------------------------|----------------------------------------------------------------| 73 | | `reset` | `() => void` | Reset the form to the initial values. | 74 | | `undo` | `() => void` | Undo to previous checkpoint. | 75 | | `redo` | `() => void` | Redo to next checkpoint. | 76 | | `saveCheckpoint` | `() => void` | Manually save a checkpoint to the history. | 77 | | `addCheckpointEquivalent` | `(targetValue: Values, equivalentValue: Values) => void` | Declare that a certain value is equivalent to another, and therefore does not constitute a change worth saving (advanced). | 78 | | `undoableCount` | `number` | Number of possible undo actions. | 79 | | `redoableCount` | `number` | Number of possible redo actions. | 80 | | `didCreateCurrentValues` | `boolean` | Whether the latest form's values were set by us (advanced). | 81 | 82 | 83 | 84 | ## Control bar 85 | 86 | A control bar with default buttons is provided (as seen on the screenshot above). 87 | 88 | ```tsx 89 | import { FormikUndoControlBar } from 'formik-undo'; 90 | 91 | const MyForm = () => { 92 | return ( 93 |
94 | 95 | 96 | 97 | 98 | ) 99 | }; 100 | ``` 101 | 102 | `` accepts props: 103 | 104 | | Name | Type | Default | Description | 105 | | ---------------------------|---------------------------------------------------------|-------------------------------|-----------------------------------------------| 106 | | `showReset` | `boolean` | `true` | Show the `reset` button. | 107 | | `showRedo` | `boolean` | `true` | Show the `redo` button. | 108 | | `disabled` | `boolean` | `false` | Disable every control. | 109 | | `className` | `string` | | Add extra classes to the control container. | 110 | | `buttonTitles` | `Record<('reset' \| 'undo' \| 'redo'), string \| null>` | `"Reset"`, `"Undo"`, `"Redo"` | Override the `title` attribute of the button. | 111 | | `buttonClasses` | `Record<('reset' \| 'undo' \| 'redo'), string>` | `{}` | Add extra classes to some of the buttons. | 112 | | `buttonIcons` | `Record<('reset' \| 'undo' \| 'redo'), ComponentType>` | `"↺"`, `"↶"`, `"↷"` | Override the buttons' content. | 113 | | `buttonComponent` | `ComponentType` |