Skip to content

Commit

Permalink
feat: add children function, add wrap method, rename effects to actio…
Browse files Browse the repository at this point in the history
…nCreators, fix bugs, subAction

BREAKING CHANGE: effects method was renamed to actionCreators
  • Loading branch information
megazazik committed Aug 11, 2019
1 parent d6ca401 commit 7295961
Show file tree
Hide file tree
Showing 17 changed files with 1,797 additions and 367 deletions.
939 changes: 652 additions & 287 deletions README.md

Large diffs are not rendered by default.

764 changes: 764 additions & 0 deletions README_RUS.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/bindActionCreators.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isEffect, IActionCreators, isActionCreatorFactory } from './controller';
import { isCustomActionCreator, IActionCreators, isActionCreatorFactory } from './controller';

function bindActionCreator (actionCreator, dispatch) {
return function() {
Expand Down Expand Up @@ -28,7 +28,7 @@ export function bindActionCreators<P extends IActionCreators>(
const wrappedActions: any = Object.keys(actionCreators).reduce(
(result, actionKey) => {
if (typeof actionCreators[actionKey] === 'function') {
if (isEffect(actionCreators[actionKey])) {
if (isCustomActionCreator(actionCreators[actionKey])) {
if (isActionCreatorFactory(actionCreators[actionKey] as any)) {
const getBoundActionCreators = (...args) =>
bindActionCreators((actionCreators[actionKey] as any)(...args), dispatch);
Expand Down
7 changes: 7 additions & 0 deletions src/children.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { build, Dictionary, IBuilder, IModel } from './controller';

export function children<AS extends Dictionary<IModel | IBuilder>> (
childrenModels: AS
) {
return build().children(childrenModels);
}
125 changes: 81 additions & 44 deletions src/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface IModel<Actions extends IActionCreators = {}, State = {}> {
readonly reducer: Reducer<State>;
}

/** @todo доработать, чтобы правильно определялся тип у типов действий, не совпадающих с именем actionCreator */
type AdditionalActionCreators<Creators, BaseCreators = Creators> = {
[K in keyof Creators]?: Creators[K] extends IActionCreator<infer U>
? ((action: IAction<U>, actions: BaseCreators) => Action)
Expand Down Expand Up @@ -104,33 +105,39 @@ export interface IBuilder<
* Оборачивает функции, создающие действия
* @returns новый строитель
*/
subActions(
subActions<A extends AdditionalActionCreators<Actions>>(
/** ассоциативный массив функций, создающих дополнительные действия */
wrapers: AdditionalActionCreators<Actions>
wrapers: A
): IBuilder<Actions, State>;

/**
* Позволяет создавать любые действия, не только простые объекты
* @returns новый строитель
*/
effect<K extends string, P extends any[], A>(
actionCreator<K extends string, P extends any[], A>(
/** тип действия */
key: K,
/** Функция, которая создает действия не в виде простых объектов */
effect: (actions: Actions, select: (state) => State) => (...args: P) => A
getActionCreator: (actions: Actions, select: (state) => State) => (...args: P) => A
): IBuilder<Actions & { [F in K]: (...args: P) => A }, State>;

/**
* Позволяет создавать любые действия, не только простые объекты
* @returns новый строитель
*/
effects<EF extends Dictionary<(actions: Actions, select: (state) => State) => (...args: any[]) => any>>(
actionCreators<EF extends Dictionary<(actions: Actions, select: (state) => State) => (...args: any[]) => any>>(
/** ассоциативный массив дочерних моделей */
effects: EF
getActionCreators: EF
): IBuilder<
Actions & { [C in keyof EF]: ReturnType<EF[C]> },
State
>;

/**
* Вызывает функцию, которая расширяет функционал текущей модели
* @returns новый строитель
*/
wrap<NewM extends IModel<any, any>>(wrapper: (builder: IBuilder<Actions, State>) => NewM): NewM;
}

/**
Expand All @@ -142,20 +149,13 @@ class Builder<
Actions extends IActionCreators = {},
State = {}
> implements IBuilder<Actions, State> {
constructor(private _model: IModel<Actions, State>) {
Object.keys(this._model.actions).forEach((key) => {
if (typeof this._model.actions[key] === 'function' && !this._model.actions[key].hasOwnProperty('toString') && !isEffect(this._model.actions[key])) {
const originActionCreator = this._model.actions[key] as Function;
(this._model.actions as any)[key] = function() {
return originActionCreator.apply(this, arguments)
};
Object.assign(
(this._model.actions as any)[key],
originActionCreator,
{toString: () => key}
);
}
});
private _model: IModel<Actions, State>

constructor(model: IModel<Actions, State>) {
this._model = {
actions: addToStringToActionCreators(model.actions),
reducer: model.reducer,
}
}

initState<NewState extends State>(f: (s: State) => NewState): IBuilder<Actions, NewState> {
Expand Down Expand Up @@ -214,7 +214,7 @@ class Builder<
...this._model.actions as any,
[childKey]: wrapActionsCreatorsWithKey(
childKey,
model.actions,
addToStringToActionCreators(model.actions),
() => (state) => state ? state[childKey] : undefined
)
},
Expand Down Expand Up @@ -242,24 +242,24 @@ class Builder<
);
}

subActions(wrappers: AdditionalActionCreators<Actions>): IBuilder<Actions, State> {
subActions<A extends AdditionalActionCreators<Actions>>(wrappers: A): IBuilder<Actions, State> {
return new Builder<Actions, State>({
...this.model,
actions: addSubActions(this._model.actions, wrappers) as any
});
}

effect<K extends string, P extends any[], A>(
/** тип действия */
actionCreator<K extends string, P extends any[], A>(
/** Имя генератора действия */
key: K,
/** Функция, которая создает действия не в виде простых объектов */
effect: (actions: Actions, select: (state) => State) => (...args: P) => A
getActionCreator: (actions: Actions, select: (state) => State) => (...args: P) => A
): IBuilder<Actions & { [F in K]: (...args: P) => A }, State> {
return new Builder({
...this.model,
actions: {
...this.model.actions as any,
[key]: createEffect(effect, () => this.model.actions, () => (state) => state)
[key]: createCustomActionCreator(getActionCreator, () => this.model.actions, () => (state) => state)
}
});
}
Expand All @@ -268,16 +268,16 @@ class Builder<
* Позволяет создавать любые действия, не только простые объекты
* @returns новый строитель
*/
effects<EF extends Dictionary<(actions: Actions, select: (state) => State) => (...args: any[]) => any>>(
actionCreators<EF extends Dictionary<(actions: Actions, select: (state) => State) => (...args: any[]) => any>>(
/** ассоциативный массив дочерних моделей */
effects: EF
getActionCreators: EF
): IBuilder<
Actions & { [C in keyof EF]: ReturnType<EF[C]> },
State
> {
/** @todo оптимизировать */
return Object.keys(effects).reduce(
(newBuilder, key) => newBuilder.effect(key, effects[key]),
return Object.keys(getActionCreators).reduce(
(newBuilder, key) => newBuilder.actionCreator(key, getActionCreators[key]),
this as any
);
}
Expand All @@ -293,6 +293,31 @@ class Builder<
get reducer(): Reducer<State> {
return this.model.reducer;
}

wrap<NewM extends IModel<any, any>>(wrapper: (builder: IBuilder<Actions, State>) => NewM) {
return new Builder(wrapper(this));
}
}

export function addToStringToActionCreators<A extends IActionCreators>(actionCreators: A): A {
const newActions = {} as any;
Object.keys(actionCreators).forEach((key) => {
if (typeof actionCreators[key] === 'function' && !actionCreators[key].hasOwnProperty('toString') && !isCustomActionCreator(actionCreators[key])) {
const originActionCreator = actionCreators[key] as Function;
newActions[key] = function() {
return originActionCreator.apply(this, arguments)
};
Object.assign(
newActions[key],
originActionCreator,
{toString: () => key}
);
} else {
newActions[key] = actionCreators[key];
}
});

return newActions;
}

function subActionsReducer<T = any>(reducer: Reducer<T>): Reducer<T> {
Expand Down Expand Up @@ -328,7 +353,7 @@ export function wrapChildActionCreators(
const wrappedActions = Object.keys(actions).reduce(
(result, actionKey) => {
if (typeof actions[actionKey] === 'function') {
if (isEffect(actions[actionKey])) {
if (isCustomActionCreator(actions[actionKey])) {
return ({
...result,

Expand Down Expand Up @@ -385,21 +410,33 @@ export function addSubActions<T extends IActionCreators>(
actions: T,
wrappers: AdditionalActionCreators<T>
): T {
const wrappersList = decomposeKeys(wrappers);
return wrapChildActionCreators(
function wrapAction(action: IAction<any>) {
let getWrapper;

if (typeof wrappers === 'function') {
getWrapper = () => wrappers;
} else {
const wrappersList = decomposeKeys(wrappers);
getWrapper = (type: string) => {
const wrapperKey = Object.keys(wrappersList).find(
(key) => ((action.type || '').indexOf(key + '.') === 0 || action.type === key)
(key) => ((type || '').indexOf(key + '.') === 0 || type === key)
);

return wrapperKey ? wrappersList[wrapperKey] : null;
}
}

return wrapChildActionCreators(
function wrapAction(action: IAction<any>) {
const wrapper = getWrapper(action.type);

const subActinos = (action.actions || []).map(wrapAction);

if (subActinos.length === 0 && !wrapperKey) {
if (subActinos.length === 0 && !wrapper) {
return action;
}

if (wrapperKey) {
const newSubActions = wrappersList[wrapperKey](action, actions) || [];
if (wrapper) {
const newSubActions = wrapper(action, actions) || [];
const newActions = [...subActinos, ...(Array.isArray(newSubActions) ? newSubActions : [newSubActions])];
if (newActions.length > 0) {
return {
Expand Down Expand Up @@ -433,11 +470,11 @@ export function decomposeKeys(list: object, parentKey = ''): { [key: string]: an
);
}

const CheckEffectField = '__Encaps.ActionCreatorsGetter__';
const CheckCustorActionCreatorField = '__Encaps.ActionCreatorsGetter__';
const ActionCreatorFactoryField = '__Encaps.ActionCreatorFactory__';
const GetEffectParamsValue = '__Encaps.GetEffectParamsValue__';

export function createEffect(
export function createCustomActionCreator(
effect: (actions, select) => any,
getActions: (...agrs) => any,
select: (...agrs) => (state) => any,
Expand All @@ -454,22 +491,22 @@ export function createEffect(
if (isActionCreatorFactory) {
newEffect[ActionCreatorFactoryField] = true;
}
newEffect[CheckEffectField] = true;
newEffect[CheckCustorActionCreatorField] = true;
return newEffect;
}

export function wrapEffect(effect, wrapActions, select) {
const [originEffect, getActions, originSelect, isActionCreatorFactory] = effect(GetEffectParamsValue);
return createEffect(
return createCustomActionCreator(
originEffect,
(...args) => wrapActions(getActions(...args)),
(...args) => (state) => originSelect(...args)(select(...args)(state)),
isActionCreatorFactory
);
}

export function isEffect(getter) {
return !!getter[CheckEffectField];
export function isCustomActionCreator(getter) {
return !!getter[CheckCustorActionCreatorField];
}

export function isActionCreatorFactory(getter) {
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { IAction, IActionCreator, Reducer, ModelActions, ModelState } from "./types";
export { build, unwrapAction, createEffect, IModel } from "./controller";
export { build, unwrapAction, createCustomActionCreator, IModel, isCustomActionCreator } from "./controller";
export { createList } from "./list";
export { createMap } from "./map";
export { bindActionCreators } from './bindActionCreators';
export { children } from './children';
7 changes: 4 additions & 3 deletions src/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,19 @@ import {
unwrapAction,
IModel,
IActionCreators,
createEffect
createCustomActionCreator,
addToStringToActionCreators,
} from './controller';
import { IAction } from './types';

export function createList<Actions extends IActionCreators = {}, State = {}>(model: IModel<Actions, State>) {
const list = build<{item: (index: number) => Actions}, {items: State[]}>({
actions: {
item: createEffect(
item: createCustomActionCreator(
(actions) => () => actions,
(index) => wrapActionsCreatorsWithKey(
joinKeys('item', index),
model.actions,
addToStringToActionCreators(model.actions),
() => (state) => state && state.items ? state.items[index] : undefined
),
(index) => (state) => state.items[index],
Expand Down
7 changes: 4 additions & 3 deletions src/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,19 @@ import {
unwrapAction,
IModel,
IActionCreators,
createEffect,
createCustomActionCreator,
addToStringToActionCreators,
} from './controller';
import { IAction } from './types';

export function createMap<Actions extends IActionCreators = {}, State = {}>(model: IModel<Actions, State>) {
const map = build<{item: (key: string) => Actions}, {items: {[key: string]: State}}>({
actions: {
item: createEffect(
item: createCustomActionCreator(
(actions, select) => (...args) => actions,
(index) => wrapActionsCreatorsWithKey(
joinKeys('item', index),
model.actions,
addToStringToActionCreators(model.actions),
() => (state) => state && state.items ? state.items[index] : undefined
),
(index) => (state) => state.items[index],
Expand Down
4 changes: 2 additions & 2 deletions test/bindActionCreators.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ test('bindActionCreators. effects', (t) => {
const model = build()
.initState(() => ({p1: 0}))
.handlers({setP1: 'p1'})
.effects({
.actionCreators({
effect1: (actions, select) => (payload) => ({myEffect: '123', payload}),
effect2: (actions, select) => () => effect2Result
});
Expand Down Expand Up @@ -198,7 +198,7 @@ test('bindActionCreators. complex', (t) => {
const child = build()
.initState(() => ({p1: 0}))
.handlers({a1: 'p1'})
.effects({
.actionCreators({
f1: (actions, select) => (value: number) => ({actions, select, value})
});

Expand Down
26 changes: 25 additions & 1 deletion test/controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import test from 'tape';
import { build, getSubActions, decomposeKeys } from '../src/controller';
import { build, getSubActions, decomposeKeys, IBuilder, IActionCreators } from '../src/controller';
import { IAction } from '../src/types';
import { spy } from 'sinon';

Expand Down Expand Up @@ -275,5 +275,29 @@ test("Child state by children", (t) => {
}
);

t.end();
});

test("Wrap builder", (t) => {
const wrap = <A extends IActionCreators, S>(builder: IBuilder<A, S>) => builder
.initState((state) => ({...state, newField: 'nv'}))
.handlers({
setValue: 'newField'
});

const model = build()
.initState(() => ({gcValue: false}))
.wrap(wrap);

t.deepEqual(
model.reducer(undefined, {type: 'init'}),
{gcValue: false, newField: 'nv'}
);

t.deepEqual(
model.actions.setValue('set'),
{type: 'setValue', payload: 'set'}
);

t.end();
});
Loading

0 comments on commit 7295961

Please sign in to comment.