From f6aff5bdd4062f995af059493d7244e4ab430731 Mon Sep 17 00:00:00 2001 From: Lebedev Mikhail Date: Sun, 12 Apr 2020 19:59:40 +0300 Subject: [PATCH] readme in progress --- README.md | 334 ++++++++++++++++++++++++++++++++++++++- src/index.ts | 14 +- src/tmp/assistant.ts | 4 +- src/tmp/index.ts | 4 +- src/tmp/module3/index.ts | 6 +- 5 files changed, 347 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index adadfb4..d42753e 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,336 @@ [![npm version](https://badge.fury.io/js/reducer-assistant.svg)](https://badge.fury.io/js/reducer-assistant) -TODO: write README +Library to manage side effects and async logic in applications using `redux` for state management. If you use `redux` but you don't like a complexity of sush solutions as `redux-saga` or `redux-observable`, you can manage your side effects with classes now. + +## Usage example + +```typescript +import { Assistant } from 'reducer-assistant'; + +class FetchDataAssistant extends Assistant { + /** + * onInit runs when an assistant starts + * + * You can configure required listeners or dispatch some actions here + */ + onInit() { + /** add 'FETCH_START' action listener */ + this.afterAction('FETCH_START', this.startFetch); + } + + startFetch = async (action) => { + try { + const requestResult = await fetch(action.url, { + /** you have access to the state from any method of assistant */ + ...this.state.someData, + }); + + /** you can dispatch actions from any method of assistant */ + this.dispatch({ type: 'FETCH_SUCCESS', payload: requestResult }); + } catch (e) { + this.dispatch({ type: 'FETCH_ERROR', payload: e }); + } + }; +} + +class IntervalAssistant extends Assistant { + /** you can use any fields as in usual classes */ + intervalId = null; + + onInit() { + /** add listener for 'INCREMENT_START' action */ + this.afterAction('INCREMENT_START', (action) => { + this.intervalId = setInterval(this.incrementValue, action.timeout); + }); + + this.afterAction('INCREMENT_STOP', () => { + clearInterval(this.intervalId); + }); + } + + incrementValue = () => { + /** increase value */ + this.dispatch({ type: 'SET_VALUE', value: this.state.value + 1 }); + }; +} +``` + +## Configure redux store + +The `createAssistantEnhancer` function is used to setup store to work with assistants. It create a store enhancer with an `applyAssistants` method which receives an array of assistant consctructors or `AssistanConfig` objects (see [Assistant config](#assistant-config) section). + +```typescript +import { createStore } from 'redux'; +import { createAssistantEnhancer } from 'reducer-assistant/redux'; +import { assistants } from './assistants'; + +const assistantEnhancer = createAssistantEnhancer(); + +const store = createStore(reducer, assistantEnhancer); + +assistantEnhancer.applyAssistants(assistants); +``` + +You can invoke the `applyAssistants` method many times. All previous assistants will be destroyed. + +## Assistant API + +To create assistant you should create new class which extends base `Assistant` class. + +```typescript +import { Assistant } from 'reducer-assistant'; + +class MyAssistant extends Assistant {} +``` + +`Assistant`'s constructor has no parameters. If you need any parameter you can create them. + +```typescript +import { Assistant } from 'reducer-assistant'; + +class MyAssistant extends Assistant { + constructor(params) { + super(); + + this.someValue = params.someValue; + } +} +``` + +To find out how to pass parameters to assistant's constructor see [Assistant config](#assistant-config) section. + +You CAN'T use any inherited methods or properties of base class in constructor. Use them inside `onInit` or after it. + +The base `Assistant` class has the following method and properties: + +- [state](#state) +- [dispatch](#dispatch) +- [onInit](#oninit) +- [onChange](#onchange) +- [afterAction](#afteraction) +- [beforeAction](#beforeaction) +- [createAssistant](#createAssistant) +- [destroy](#destroy) +- [onDestroy](#ondestroy) + +### state + +The `state` getter returns a current state. + +### dispatch + +The `dispatch` dispatches an action. + +### onInit + +You can override this method to add listeners, dispatch action, start fetching data, start timers, create child assistants etc. + +```typescript +class MyAssistant extends Assistant { + onInit() { + this.afterAction('SOME_ACTION', (action) => { + console.log(action); + console.log(state); + }); + } +} +``` + +### onChange + +Listeners passed to `onChange` will be invoked when the state has changed after any action. An assistant can watch to changes of the whole state or some part of state. To see how to watch to a part of state see [Assistant config](#assistant-config) section. + +```typescript +class MyAssistant extends Assistant { + onInit() { + this.onChange(() => { + console.log('new state action', this.state); + }); + } +} +``` + +The `onChange` method return a function to unsubscribe events. + +```typescript +class MyAssistant extends Assistant { + unsubscribe = null; + + onInit() { + /** add 'onChange' listener */ + this.afterAction('ACTION1', (action) => { + this.unsubscribe = this.onChange(() => { + console.log('new state action', this.state); + }); + }); + /** remove listener */ + this.afterAction('ACTION2', (action) => { + this.unsubscribe(); + }); + } +} +``` + +### afterAction + +The `afterAction` listeners will be invoked when an action has been dispatched and the state has been changed. You can add listener to some specific action or to any action. + +```typescript +class MyAssistant extends Assistant { + onInit() { + /** runs after any action */ + this.afterAction((action) => { + /*...*/ + }); + /** runs after the 'ACTION1' action */ + this.afterAction('ACTION1', (action) => { + /*...*/ + }); + } +} +``` + +An `afterAction` listener receives an action as a parameter. + +The `afterAction` method return a function to unsubscribe events. + +### beforeAction + +The `beforeAction` method works as the `afterAction`. But its listeners will be invoked before state changes. + +### createAssistant + +You can run assistants dynamically from other assistants in any time inside and after `onInit` and before `onDestroy`. The `createAssistant` method receives an `AssistantConfig` and returns a created assistant instance. + +```typescript +class MyAssistant extends Assistant { + onInit() { + this.createAssistant(ChildAssistant); + } +} +``` + +### destroy + +Via the `destroy` method you can stop an current assistant themself or a child assistant. + +```typescript +class MyAssistant extends Assistant { + childAssistant = null; + + onInit() { + this.childAssistant = this.createAssistant(ChildAssistant); + + this.afterAction('DESTROY', () => { + this.destroy(); + }); + + this.afterAction('DESTROY_CHILD', () => { + this.childAssistant.destroy(); + }); + } +} +``` + +### onDestroy + +The `onDestroy` function is invoked before an assistant is destroyed. For example, if you add listeners to any events of DOM objects you can remove them in this method. + +```typescript +class ClickAssistant extends Assistant { + onInit() { + document + .getElementById('myDIV') + .addEventListener('click', this.onClick); + } + + onDestroy() { + document + .getElementById('myDIV') + .removeEventListener('click', this.onClick); + } + + onClick = () => { + this.dispatch({ type: 'CLICK' }); + }; +} +``` + +There is no need to remove listeners of the base assistant events such as `onChange` or `afterAction` or destroy child assistants. They will be removed automatically. + +## Assistant config + +To create assistants you can use the `applyAssistants` method of a store enhancer or the `createAssistant` method of an assistant. They receives `AssistantConfigs` values. + +The simplified `AssistantConfigs` type has the following form: + +```ts +type AssistantConfig = + | { new (): Assistant } + | { + Constructor: { new (): Assistant }; + select: (fullstate: any) => any; + } + | { + create: () => Assistant; + select: (fullstate: any) => any; + }; +``` + +### Constructor + +The simplest version of `AssistantConfigs` is an assistant's constructor. + +```ts +class MyAssistant extends Assustant {} + +enhancer.applyAssistants([MyAssistant]); +``` + +### Constructor with select + +Sometimes you may need to create an assistant which manages some part of state. Such assistants can be reusable and independent of any other part of state. + +```ts +type TimerState = { + value: number; +}; + +class TimerAssistant extends Assistant { + private intervalId; + + onInit() { + this.intervalId = setInterval(() => { + this.dispatch({ type: 'INCREMENT', payload: this.state.value + 1 }); + }); + } + + onDestroy() { + clearInterval(this.intervalId); + } +} +``` + +The `TimerAssistant` class expects the `state` property returns a value of type `{value: number}`. But state of a page reducer can be different and can contain the timer's value in any field; + +```ts +type PageState = { + timer: {value: number}; + .... +} +``` + +When you create an assistant you can specify a part of state which will be managed by the assistant via select function of `AssistantConfig`. + +```typescript +enhancer.applyAssistants([{ + Constructor: TimerAssistant; + /** select part of the PageState for TimerAssistant */ + select: (fullstate) => fullstate.timer; +}]); // instead of enhancer.applyAssistants([TimerAssistant]) +``` + +Now the `state` property of a `TimerAssistant`'s instance will return the `timer` field value of the page state. + +// дочерние редьюсеры по умолчанию наследуют селект родителя diff --git a/src/index.ts b/src/index.ts index d11249b..ee1cb62 100644 --- a/src/index.ts +++ b/src/index.ts @@ -154,15 +154,15 @@ export abstract class Assistant { export type Configs = Array, S>>; -export function addSelect>( +export function ofStatePart>( select: K, config: { new (): A } ): AssistantConfig }>; -export function addSelect>( +export function ofStatePart>( select: (s: NewS) => StateOfAssistant, config: { new (): A } ): AssistantConfig; -export function addSelect< +export function ofStatePart< K extends string, A extends Assistant, S = StateOfAssistant @@ -170,7 +170,7 @@ export function addSelect< select: K, config: ConstructorAssistantConfig | CreateAssistantConfig ): AssistantConfig; -export function addSelect< +export function ofStatePart< NewS, A extends Assistant, S = StateOfAssistant @@ -178,16 +178,16 @@ export function addSelect< select: (s: NewS) => S, config: ConstructorAssistantConfig | CreateAssistantConfig ): AssistantConfig; -export function addSelect( +export function ofStatePart( select: K, configs: Configs ): Configs<{ [P in K]: S }>; -export function addSelect( +export function ofStatePart( select: (s: NewS) => S, configs: Configs ): Configs; /** Implementation */ -export function addSelect( +export function ofStatePart( select: string | ((s: any) => any), config: AssistantConfig | Array> ): AssistantConfig | Array> { diff --git a/src/tmp/assistant.ts b/src/tmp/assistant.ts index a9571b8..8f10a2e 100644 --- a/src/tmp/assistant.ts +++ b/src/tmp/assistant.ts @@ -1,5 +1,5 @@ import { model } from './model'; -import { Assistant, addSelect } from '..'; +import { Assistant, ofStatePart } from '..'; export class ModelAssistant extends Assistant< ReturnType @@ -9,7 +9,7 @@ export class ModelAssistant extends Assistant< onInit() { this.afterAction('mytest', () => { // this.dispatch({ type: 'model3.model1.setValue', payload: 876 }); - this.tmp = this.createAssistant(addSelect('model3', Temp)); + this.tmp = this.createAssistant(ofStatePart('model3', Temp)); }); this.afterAction('remove', () => { diff --git a/src/tmp/index.ts b/src/tmp/index.ts index 1589329..fdcdf42 100644 --- a/src/tmp/index.ts +++ b/src/tmp/index.ts @@ -1,5 +1,5 @@ import { createStore } from 'redux'; -import { addSelect } from '..'; +import { ofStatePart } from '..'; import { createAssistantEnhancer } from '../redux'; import { assistants } from './module3'; import { model } from './model'; @@ -15,7 +15,7 @@ const store = createStore(model.reducer, assistantEnhancer); assistantEnhancer.applyAssistants([ ModelAssistant, - ...addSelect('model3', assistants), + ...ofStatePart('model3', assistants), ]); console.log('state', store.getState()); diff --git a/src/tmp/module3/index.ts b/src/tmp/module3/index.ts index df78fc2..c481c66 100644 --- a/src/tmp/module3/index.ts +++ b/src/tmp/module3/index.ts @@ -1,4 +1,4 @@ -import { addSelect, Configs, AssistantConfig } from '../..'; +import { ofStatePart, Configs, AssistantConfig } from '../..'; import { model3, State3 } from './model'; import { Model3Assistant } from './assistant'; import { Model1Assistant } from '../module1'; @@ -8,6 +8,6 @@ export { model3 }; export const assistants: Configs = [ Model3Assistant, - addSelect('model1', Model1Assistant), - addSelect('model2', Model2Assistant), + ofStatePart('model1', Model1Assistant), + ofStatePart('model2', Model2Assistant), ];