[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:
Ilya Moroz 2022-05-05 14:26:04 +03:00 committed by GitHub
parent 771437ed04
commit e6db8d5140
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 295 additions and 1 deletions

View 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;
}

View 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;

View 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();

View file

@ -0,0 +1,6 @@
import blocksReducer from './blocksReducer';
import createStore from './createStore';
const store = createStore(blocksReducer);
export default store;

View file

@ -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
View 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;

View 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
View 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
View 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
View 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;
}