From e6db8d51405752727961864d33072e6798adf766 Mon Sep 17 00:00:00 2001 From: Ilya Moroz <37909603+ilyamore88@users.noreply.github.com> Date: Thu, 5 May 2022 14:26:04 +0300 Subject: [PATCH] [Feature] Add state manager (#2018) * add state manager with demo file * remove initial state * move Store type to Store.ts * add new actions * change store schema * add docs * type -> interface, add deepCopy function * move types to the /types/ folder * rename types files, change state type (add blocks: key) * fix createStore.ts func * add documentation for reducer * use BlockMutationType instead of ActionType * add doc * deep copy of initial state * add doc for createStore * Apply suggestions from code review Co-authored-by: Peter Savchenko * rename `reducer` to `blocksReducer` * add a listener type, pass changed state to the listener Co-authored-by: Peter Savchenko --- src/components/store/blocksReducer.ts | 29 ++++++++++ src/components/store/createStore.ts | 67 +++++++++++++++++++++++ src/components/store/demo.ts | 79 +++++++++++++++++++++++++++ src/components/store/index.ts | 6 ++ src/components/utils.ts | 25 ++++++++- types/store/action.ts | 34 ++++++++++++ types/store/editorState.ts | 11 ++++ types/store/listener.ts | 9 +++ types/store/reducer.ts | 7 +++ types/store/store.ts | 29 ++++++++++ 10 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 src/components/store/blocksReducer.ts create mode 100644 src/components/store/createStore.ts create mode 100644 src/components/store/demo.ts create mode 100644 src/components/store/index.ts create mode 100644 types/store/action.ts create mode 100644 types/store/editorState.ts create mode 100644 types/store/listener.ts create mode 100644 types/store/reducer.ts create mode 100644 types/store/store.ts diff --git a/src/components/store/blocksReducer.ts b/src/components/store/blocksReducer.ts new file mode 100644 index 00000000..8a0c7ea8 --- /dev/null +++ b/src/components/store/blocksReducer.ts @@ -0,0 +1,29 @@ +import { EditorState } from '../../../types/store/editorState'; +import { Action } from '../../../types/store/action'; +import * as _ from '../utils'; +import { BlockMutationType } from '../../../types/events/block/mutation-type'; + +/** + * The reducer function for Editor.js state + * This function applies the passed action to the current state and returns the new state + * This reducer is especially for working with blocks data + * + * @param state - previous state to apply action + * @param action - information about the action in the previous state + */ +export default function blocksReducer(state: EditorState, action: Action): EditorState { + const stateCopy = _.deepCopy(state); + + switch (action.type) { + case BlockMutationType.Added: + case BlockMutationType.Changed: + stateCopy.blocks[action.data.id] = action.data; + break; + + case BlockMutationType.Removed: + delete stateCopy.blocks[action.blockId]; + break; + } + + return stateCopy; +} diff --git a/src/components/store/createStore.ts b/src/components/store/createStore.ts new file mode 100644 index 00000000..adfd0e38 --- /dev/null +++ b/src/components/store/createStore.ts @@ -0,0 +1,67 @@ +import { EditorState } from '../../../types/store/editorState'; +import { Reducer } from '../../../types/store/reducer'; +import { Action } from '../../../types/store/action'; +import { Store } from '../../../types/store/store'; +import * as _ from '../utils'; +import { Listener } from '../../../types/store/listener'; + +/** + * This function is an entry point to use the store in the editor + * It creates the store with an initial state + * + * It returns functions to use the store: + * subscribe - function for subscribing to each state change + * dispatch - function for applying actions to the store + * getState - function returns a current state of the store + * + * @param reducer - the function that applies the passed action to the current state and returns the new state. + * Passing a reducer function to the `createStore` function helps + * to add new logic to the Store without changing the main logic of the Store. + * + * @param initialState - initial state of the store + */ +function createStore(reducer: Reducer, initialState: EditorState = { blocks: {} }): Store { + const currentReducer = reducer; + let state = _.deepCopy(initialState); + const currentListeners = []; + + /** + * Function for subscribing on state changes + * + * @param listener - function, that will execute every state change + * + * @returns {() => void} unsubscribe function + */ + const subscribe = (listener: Listener): (() => void) => { + currentListeners.push(listener); + + return (): void => { + currentListeners.splice(currentListeners.indexOf(listener), 1); + }; + }; + + /** + * Dispatch action on the current state + * + * @param action - action that will be dispatched + */ + const dispatch = (action: Action): void => { + state = currentReducer(state, action); + currentListeners.forEach((listener) => { + listener(state); + }); + }; + + /** + * Function returns current state + */ + const getState = (): EditorState => state; + + return { + subscribe, + dispatch, + getState, + }; +} + +export default createStore; diff --git a/src/components/store/demo.ts b/src/components/store/demo.ts new file mode 100644 index 00000000..b62e0576 --- /dev/null +++ b/src/components/store/demo.ts @@ -0,0 +1,79 @@ +import store from './index'; +import { BlockMutationType } from '../../../types/events/block/mutation-type'; +import { EditorState } from '../../../types/store/editorState'; +import { Listener } from '../../../types/store/listener'; + +/** + * Handle changes with previous and current states + */ +const onDataChange = (): Listener => { + /** + * Initial state + */ + let currentState = store.getState(); + + /** + * onChange handler + * + * @param changedState - changed state after dispatching + */ + return (changedState: EditorState): void => { + const prevState = currentState; + + currentState = changedState; + + console.log('***'); + console.log('Previous state:', prevState); + console.log('Current state:', currentState); + console.log('***'); + }; +}; + +const unsubscribeOnDataChange = store.subscribe(onDataChange()); + +const block1 = { + id: '3JPEqh8_Wc', + type: 'header', + data: { + text: 'Editor.js', + level: 2, + }, +}; + +const block2 = { + id: 'AsbMKCuatV', + type: 'paragraph', + data: { + text: 'Hey. Meet the new Editor. On this page you can see it in action — try to edit this text.', + }, +}; + +const block2Changed = { + id: 'AsbMKCuatV', + type: 'paragraph', + data: { + text: 'Hey. Meet the new Editor. On this page you can see it in action — try to edit this text.', + }, +}; + +store.dispatch({ + type: BlockMutationType.Added, + data: block1, +}); + +store.dispatch({ + type: BlockMutationType.Added, + data: block2, +}); + +store.dispatch({ + type: BlockMutationType.Changed, + data: block2Changed, +}); + +store.dispatch({ + type: BlockMutationType.Removed, + blockId: block1.id, +}); + +unsubscribeOnDataChange(); diff --git a/src/components/store/index.ts b/src/components/store/index.ts new file mode 100644 index 00000000..e0227d1e --- /dev/null +++ b/src/components/store/index.ts @@ -0,0 +1,6 @@ +import blocksReducer from './blocksReducer'; +import createStore from './createStore'; + +const store = createStore(blocksReducer); + +export default store; diff --git a/src/components/utils.ts b/src/components/utils.ts index e1e756f6..dfd43176 100644 --- a/src/components/utils.ts +++ b/src/components/utils.ts @@ -761,4 +761,27 @@ export function cacheable>(target: T): T { + if (target === null) { + return target; + } + + if (typeof target === 'object' && target !== {}) { + const cp = { ...target }; + + Object.keys(cp).forEach(k => { + cp[k] = deepCopy(cp[k]); + }); + + return cp; + } + + return target; +} diff --git a/types/store/action.ts b/types/store/action.ts new file mode 100644 index 00000000..21b7948d --- /dev/null +++ b/types/store/action.ts @@ -0,0 +1,34 @@ +import { OutputBlockData } from '../index'; +import { BlockMutationType } from '../events/block/mutation-type'; + +/** + * Action for creating a new block in the editor + * This action will add the new block to the state + */ +interface CreateBlockAction { + type: BlockMutationType.Added; + data: OutputBlockData; +} + +/** + * Action for changing data of an existing block + * This action will change block data in the state by its id + */ +interface ChangeBlockDataAction { + type: BlockMutationType.Changed; + data: OutputBlockData; +} + +/** + * Action for removing a block from the editor + * This action will remove the block from the state by its id + */ +interface RemoveBlockAction { + type: BlockMutationType.Removed; + blockId: string; +} + +/** + * Available action types + */ +export type Action = CreateBlockAction | ChangeBlockDataAction | RemoveBlockAction; diff --git a/types/store/editorState.ts b/types/store/editorState.ts new file mode 100644 index 00000000..21960cfe --- /dev/null +++ b/types/store/editorState.ts @@ -0,0 +1,11 @@ +import { OutputBlockData } from '../index'; + +/** + * Type of the state object + */ +export interface EditorState { + /** + * Data of blocks in the editor + */ + blocks: Record; +} diff --git a/types/store/listener.ts b/types/store/listener.ts new file mode 100644 index 00000000..b7936d12 --- /dev/null +++ b/types/store/listener.ts @@ -0,0 +1,9 @@ +import { EditorState } from './editorState'; + +/** + * Listener function type + * This function uses subscribing to state changes + * + * @param changedState - changed state after dispatch action on the state + */ +export type Listener = (changedState: EditorState) => void; diff --git a/types/store/reducer.ts b/types/store/reducer.ts new file mode 100644 index 00000000..9466d73f --- /dev/null +++ b/types/store/reducer.ts @@ -0,0 +1,7 @@ +import { EditorState } from './editorState'; +import { Action } from './action'; + +/** + * Type of the function that applies the passed action to the current state and returns the new state + */ +export type Reducer = (state: EditorState, action: Action) => EditorState diff --git a/types/store/store.ts b/types/store/store.ts new file mode 100644 index 00000000..620f62f4 --- /dev/null +++ b/types/store/store.ts @@ -0,0 +1,29 @@ +import { Action } from './action'; +import { EditorState } from './editorState'; +import { Listener } from './listener'; + +/** + * Store type contains functions for use it + */ +export interface Store { + /** + * Function for subscribing on state changes + * + * @param listener - function, that will execute every state change + * + * @returns {() => void} unsubscribe function + */ + subscribe: (listener: Listener) => (() => void); + + /** + * Dispatch action on the current state + * + * @param action - action that will be dispatched + */ + dispatch: (action: Action) => void; + + /** + * Function returns current state + */ + getState: () => EditorState; +}