From eef6f37c9def6f1c57ad602b38bf446719e3483d Mon Sep 17 00:00:00 2001 From: "Sergey S. Volkov" Date: Thu, 14 May 2020 21:52:21 +0300 Subject: [PATCH] [new package] @reffect/undoable (#7) * feat: initial commit for reffect-undoable package, add simplest implementation undo redo * feat: create simple undo redo store extension package * chore: change build config (output plugins) * chore: remove build files * chore: add gitignore for reffect/undoable * chore: lock file update * feat: rewrite undoable to reffect store with couple effects (undo, redo) * chore: up size limits for reffect undoable package; chore: change keywords for undoable * docs: update readme for monorepo (add Install section); docs: add readme for undoable package --- README.md | 18 +++++ build/utils/createBuildConfig.js | 35 ++++++---- packages/undoable/.gitignore | 4 ++ packages/undoable/.size-limit.json | 17 +++++ packages/undoable/README.md | 106 +++++++++++++++++++++++++++++ packages/undoable/package.json | 50 ++++++++++++++ packages/undoable/src/index.ts | 68 ++++++++++++++++++ 7 files changed, 283 insertions(+), 15 deletions(-) create mode 100644 packages/undoable/.gitignore create mode 100644 packages/undoable/.size-limit.json create mode 100644 packages/undoable/README.md create mode 100644 packages/undoable/package.json create mode 100644 packages/undoable/src/index.ts diff --git a/README.md b/README.md index 226f3ad..5db8603 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,24 @@ Reffect — is a declarative and reactive multi-store state manager for JavaScri [![npm](https://img.shields.io/npm/v/@reffect/logger?style=flat-square&color=blue)](https://www.npmjs.com/package/@reffect/logger) [![npm bundle size](https://img.shields.io/bundlephobia/minzip/@reffect/logger?style=flat-square&color=blue)](https://bundlephobia.com/result?p=@reffect/logger) - [`@reffect/localstore`](https://github.com/acacode/reffect/tree/master/packages/localstore) - store middleware to synchronize store with local storage key [![npm](https://img.shields.io/npm/v/@reffect/localstore?style=flat-square&color=blue)](https://www.npmjs.com/package/@reffect/localstore) [![npm bundle size](https://img.shields.io/bundlephobia/minzip/@reffect/localstore?style=flat-square&color=blue)](https://bundlephobia.com/result?p=@reffect/localstore) +- [`@reffect/undoable`](https://github.com/acacode/reffect/tree/master/packages/undoable) - store extension which provides undo/redo effects and store history + [![npm](https://img.shields.io/npm/v/@reffect/undoable?style=flat-square&color=blue)](https://www.npmjs.com/package/@reffect/undoable) [![npm bundle size](https://img.shields.io/bundlephobia/minzip/@reffect/undoable?style=flat-square&color=blue)](https://bundlephobia.com/result?p=@reffect/undoable) + +## Install + +Before at all you need to install main package: + +```bash +$ npm i -S @reffect/core +# or using yarn +$ yarn install @reffect/core +``` + +If project is using [React](https://github.com/facebook/react) you need to install [@reffect/react](https://github.com/acacode/reffect/tree/master/packages/react) (pack of React hooks which simplify usage with React application) + +```bash +$ npm i -S @reffect/react +``` ## Examples diff --git a/build/utils/createBuildConfig.js b/build/utils/createBuildConfig.js index 3e172f7..9b1e2e7 100644 --- a/build/utils/createBuildConfig.js +++ b/build/utils/createBuildConfig.js @@ -32,25 +32,28 @@ const createBuildConfig = ({ output: { file: outputPathToFile, format: outputFormat, - sourcemap, + sourcemap: sourcemap, + plugins: [ + defaultPlugins && + terser({ + sourcemap: sourcemap, + compress: { + pure_getters: true, + unsafe: true, + unsafe_comps: true, + warnings: false, + arguments: true, + unsafe_Function: true, + module: true, + passes: 30 + } + }) + ].filter(Boolean), ...commonOutput }, external: id => externalDeps.includes(id), context: projectPath, plugins: [ - defaultPlugins && - terser({ - compress: { - pure_getters: true, - unsafe: true, - unsafe_comps: true, - warnings: false, - arguments: true, - unsafe_Function: true, - module: true, - passes: 15 - } - }), defaultPlugins && typescript({ allowJs: false, @@ -64,7 +67,9 @@ const createBuildConfig = ({ noImplicitReturns: true, noImplicitThis: true, noUnusedLocals: true, - sourceMap: false, + inlineSourceMap: true, + inlineSources: true, + sourceMap: true, strictNullChecks: true, suppressImplicitAnyIndexErrors: true, exclude: ["node_modules", "**/*.spec.ts", "**/*.test.ts"] diff --git a/packages/undoable/.gitignore b/packages/undoable/.gitignore new file mode 100644 index 0000000..3cb55ca --- /dev/null +++ b/packages/undoable/.gitignore @@ -0,0 +1,4 @@ +reffect-undoable.*.js +reffect-undoable.d.ts +reffect-undoable.*.js.map +package-lock.json \ No newline at end of file diff --git a/packages/undoable/.size-limit.json b/packages/undoable/.size-limit.json new file mode 100644 index 0000000..4b59c82 --- /dev/null +++ b/packages/undoable/.size-limit.json @@ -0,0 +1,17 @@ +[ + { + "path": "reffect-undoable.es.js", + "webpack": false, + "limit": "410 B" + }, + { + "path": "reffect-undoable.cjs.js", + "webpack": false, + "limit": "450 B" + }, + { + "path": "reffect-undoable.umd.js", + "webpack": false, + "limit": "550 B" + } +] diff --git a/packages/undoable/README.md b/packages/undoable/README.md new file mode 100644 index 0000000..110bd04 --- /dev/null +++ b/packages/undoable/README.md @@ -0,0 +1,106 @@ +
+ +[![reffect logo](https://raw.githubusercontent.com/acacode/reffect/master/assets/reffect.png)](https://github.com/acacode/reffect) +[![npm](https://img.shields.io/npm/v/@reffect/react?style=flat-square&color=blue)](https://www.npmjs.com/package/@reffect/react) +[![npm bundle size](https://img.shields.io/bundlephobia/minzip/@reffect/react?style=flat-square&color=blue)](https://bundlephobia.com/result?p=@reffect/react) +[![license](https://img.shields.io/github/license/acacode/reffect?style=flat-square&color=blue)](https://github.com/acacode/reffect) + +
+ +Reffect — is a declarative and reactive multi-store state manager for JavaScript/TypeScript applications inspired by [Reatom](https://github.com/artalar/reatom) and [Effector](https://github.com/zerobias/effector) + +# @reffect/undoable + +Extension for [`Reffect`](https://github.com/acacode/reffect) stores. +Provides undo/redo effects and store history. + +## How to use + +Import `undoable` function from package: + +```ts +import { undoable } from "@reffect/undoable"; +``` + +Call `undoable` and send to it your store at first argument: + +```ts +import { store } from "@reffect/core"; +const storeRef = store({ foo: "bar" }); +const { history, undo, redo } = undoable(storeRef, middlewares, limit); +// `middlewares` it is array of reffect store middlewares +// `limit` means limit for state history +``` + +### undo (`VoidFunction`) + +This function move state of wrapped store to the previous value + +```ts +undo(); +``` + +### redo (`VoidFunction`) + +This function move state of wrapped store to the next value (if `undo()` was called early) + +```ts +redo(); +``` + +### history (`{ past: Store[]; present: Store; future: Store[]; }`) + +It's reffect store which have state history of wrapped store + +```ts +import { manage } from "@reffect/core"; + +manage(history).subscribe((payload, prevState, currState) => console.log(payload, prevState, currState)); +``` + +## Examples + +```ts +import { store, effect } from "@reffect/core"; +import { undoable } from "@reffect/undoable"; + +const keyboards = store({ list: [] }); +const { history, undo, redo } = undoable(keyboards); + +const addKeyboard = effect(keyboards, name => ({ list: [...keyboards.list, name] })); + +addKeyboard("Das Keyboard 4Q"); +addKeyboard("Leopold FC900R"); +addKeyboard("Leopold FC750R"); + +console.log(keyboards.list); // ["Das Keyboard 4Q", "Leopold FC900R", "Leopold FC750R"] +undo(); +console.log(keyboards.list); // ["Das Keyboard 4Q", "Leopold FC900R"] +undo(); +console.log(keyboards.list); // ["Das Keyboard 4Q"] +redo(); +console.log(keyboards.list); // ["Das Keyboard 4Q", "Leopold FC900R"] +redo(); +console.log(keyboards.list); // ["Das Keyboard 4Q", "Leopold FC900R", "Leopold FC750R"] + +console.log(history); +/* + { + past: [ + { + list: [], + }, + { + list: ["Das Keyboard 4Q"], + }, + { + list: ["Das Keyboard 4Q", "Leopold FC900R"], + }, + ], + present: { + list: ["Das Keyboard 4Q", "Leopold FC900R", "Leopold FC750R"] + }, + future: [] + } +*/ +``` diff --git a/packages/undoable/package.json b/packages/undoable/package.json new file mode 100644 index 0000000..0058904 --- /dev/null +++ b/packages/undoable/package.json @@ -0,0 +1,50 @@ +{ + "name": "@reffect/undoable", + "version": "0.0.1", + "description": "Reffect stores extension", + "scripts": { + "build": "node ../../build/build.js", + "size": "size-limit" + }, + "author": "acacode", + "license": "MIT", + "readme": "README.md", + "private": false, + "sideEffects": false, + "main": "reffect-undoable.cjs.js", + "typings": "reffect-undoable.d.ts", + "umd:main": "reffect-undoable.umd.js", + "jsnext:main": "reffect-undoable.es.js", + "module": "reffect-undoable.es.js", + "peerDependencies": { "@reffect/core": "^1.5.0" }, + "devDependencies": { + "@reffect/core": "^1.5.0", + "@size-limit/preset-small-lib": "^4.2.1", + "size-limit": "^4.2.1" + }, + "files": [ + "LICENSE", + "reffect-undoable.d.ts", + "reffect-undoable.cjs.js", + "reffect-undoable.cjs.js.map", + "reffect-undoable.es.js", + "reffect-undoable.es.js.map", + "reffect-undoable.umd.js", + "reffect-undoable.umd.js.map" + ], + "keywords": [ + "redux", + "effector", + "mobx", + "state", + "store", + "flux", + "reactive", + "state-manager", + "state manager", + "undo", + "undoable", + "reffect" + ], + "gitHead": "7636d0e5d6b6ba2f17c0b415f0761b559ae2a20c" +} \ No newline at end of file diff --git a/packages/undoable/src/index.ts b/packages/undoable/src/index.ts new file mode 100644 index 0000000..f6e6623 --- /dev/null +++ b/packages/undoable/src/index.ts @@ -0,0 +1,68 @@ +import { StoreType, manage, store, effect, StoreMiddleware } from "@reffect/core"; + +type Undoable = { + history: StoreHistory; + undo: VoidFunction; + redo: VoidFunction; +}; + +export type StoreHistory = { + past: Store[]; + present: Store; + future: Store[]; +}; + +export const undoable = ( + storeRef: Store, + middlewares?: StoreMiddleware>[], + limit = 15, +): Undoable => { + let updateFromUndoable = false; + const storeRefManager = manage(storeRef); + const storeHistory = store>( + { + past: [], + present: storeRefManager.initialState as Store, + future: [], + }, + `${storeRefManager.name}-history`, + middlewares, + ); + + const undoRedo = (isRedo?: boolean): Partial> | void => { + const historyArray = storeHistory[isRedo ? "future" : "past"]; + if (historyArray.length) { + const sliced = historyArray.slice(); + const present = sliced[isRedo ? "shift" : "pop"](); + if (present) { + updateFromUndoable = true; + storeRefManager.partialUpdate(present); + return { + present, + future: isRedo ? sliced : [storeHistory.present, ...storeHistory.future], + past: isRedo ? [...storeHistory.past, storeHistory.present] : sliced, + }; + } + } + }; + + storeRefManager.subscribe((partialUpdate, prevState, currState) => { + if (updateFromUndoable) { + updateFromUndoable = false; + } else { + const pastLength = storeHistory.past.length; + + manage(storeHistory).partialUpdate({ + present: currState, + future: storeHistory.future.length ? [] : storeHistory.future, + past: [...storeHistory.past.slice(+(pastLength >= limit), pastLength), prevState], + }); + } + }); + + return { + undo: effect(storeHistory, () => undoRedo()), + redo: effect(storeHistory, () => undoRedo(true)), + history: storeHistory, + }; +};