mirror of
https://github.com/codex-team/editor.js
synced 2024-06-08 08:52:15 +02:00
[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 <specc.dev@gmail.com> * rename `reducer` to `blocksReducer` * add a listener type, pass changed state to the listener Co-authored-by: Peter Savchenko <specc.dev@gmail.com>
This commit is contained in:
parent
771437ed04
commit
e6db8d5140
29
src/components/store/blocksReducer.ts
Normal file
29
src/components/store/blocksReducer.ts
Normal file
|
@ -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;
|
||||
}
|
67
src/components/store/createStore.ts
Normal file
67
src/components/store/createStore.ts
Normal file
|
@ -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;
|
79
src/components/store/demo.ts
Normal file
79
src/components/store/demo.ts
Normal file
|
@ -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 <b>Editor</b>. 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();
|
6
src/components/store/index.ts
Normal file
6
src/components/store/index.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import blocksReducer from './blocksReducer';
|
||||
import createStore from './createStore';
|
||||
|
||||
const store = createStore(blocksReducer);
|
||||
|
||||
export default store;
|
|
@ -761,4 +761,27 @@ export function cacheable<Target, Value, Arguments extends unknown[] = unknown[]
|
|||
}
|
||||
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep copy function.
|
||||
*
|
||||
* @param target - Target value to be copied.
|
||||
*/
|
||||
export function deepCopy<T extends Record<keyof T, unknown>>(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;
|
||||
}
|
||||
|
|
34
types/store/action.ts
Normal file
34
types/store/action.ts
Normal file
|
@ -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;
|
11
types/store/editorState.ts
Normal file
11
types/store/editorState.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { OutputBlockData } from '../index';
|
||||
|
||||
/**
|
||||
* Type of the state object
|
||||
*/
|
||||
export interface EditorState {
|
||||
/**
|
||||
* Data of blocks in the editor
|
||||
*/
|
||||
blocks: Record<string, OutputBlockData>;
|
||||
}
|
9
types/store/listener.ts
Normal file
9
types/store/listener.ts
Normal file
|
@ -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;
|
7
types/store/reducer.ts
Normal file
7
types/store/reducer.ts
Normal file
|
@ -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
|
29
types/store/store.ts
Normal file
29
types/store/store.ts
Normal file
|
@ -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;
|
||||
}
|
Loading…
Reference in a new issue