UNPKG

@gongt/ts-stl-client

Version:
207 lines (178 loc) 6.81 kB
import * as PropTypes from "prop-types"; import {connect, MapStateToProps, Options} from "react-redux"; import {Dispatch} from "redux"; import {BaseComponent, BaseComponentConstructor, StatefulBaseComponentConstructor} from "../react/stateless-component"; import {IReduxActionConstructor} from "./action"; import {IState} from "./preload-state"; import {IVirtualStoreConstructor} from "./virtual-store"; export interface TDispatchProps { _dispatch?: Dispatch<any>; renderCounts?: number; } export function tDispatchMapper(disp: Dispatch<any>): TDispatchProps { const ret = {}; Object.defineProperty(ret, '_dispatch', { value: disp, configurable: false, enumerable: true, writable: false, }); return ret; } export type WrapFunction<In, Out> = (state: In) => Out; export type MapperObject<State extends IState, Props> = { [K in keyof Props]?: WrapFunction<State, Props[K]>; }; export type MapperFunction<State extends IState, Props> = WrapFunction<State, Partial<Props>>; export type Mapper<State extends IState, Props> = MapperFunction<State, Props>|MapperObject<State, Props>; // MapperObject<Props, State>; export class ReactReduxConnector<State extends IState, Props extends TDispatchProps> { protected mps: Mapper<State, Props>[] = []; protected advOpts: Options<any, any, any> = { renderCountProp: 'renderCounts', shouldHandleStateChanges: true, withRef: false, pure: true, }; constructor() { this.connect = this.connect.bind(this); } addMapper(obj: Mapper<State, Props>) { this.mps.push(obj); } isComponentUseContext(notPure: boolean = true) { this.advOpts.pure = !notPure; } isComponentUseDOM(storeRef: boolean = true) { this.advOpts.withRef = storeRef; } connect<T extends object, Class extends StatefulBaseComponentConstructor<Props, T>>(reactClass: Class): Class { if (reactClass['_redux_connect']) { throw new TypeError(`duplicate @ReduxConnector() on ${reactClass.displayName || reactClass.name}`); } prepareReactClass(reactClass); let mapper: MapStateToProps<Props, void, State>; if (this.mps.length === 0) { this.advOpts.shouldHandleStateChanges = false; mapper = undefined; } else { this.advOpts.shouldHandleStateChanges = true; mapper = <any> createMapper(this.mps); } this.advOpts.getDisplayName = (name) => { return `RRConnector(${name}) [pure=${this.advOpts.pure},withRef=${this.advOpts.withRef},handle=${this.advOpts.shouldHandleStateChanges}]`; }; const c: ClassDecorator = <any>connect<Props, TDispatchProps, void>( mapper, tDispatchMapper, // TODO undefined, // TODO this.advOpts, ); const RetClass = <any>c(reactClass); Object.defineProperty(RetClass, '_redux_connect', { enumerable: false, configurable: false, writable: false, value: true, }); // RetClass.displayName = `ReactReduxConnector(${reactClass.displayName || reactClass.name})`; return RetClass; } } function prepareReactClass<Props extends TDispatchProps>(reactClass: BaseComponentConstructor<Props>) { if (!reactClass.propTypes) { reactClass.propTypes = {}; } reactClass.propTypes.renderCounts = PropTypes.number; } /** @deprecated */ export function connectToStore<State extends IState, Props extends TDispatchProps> (mapper0: WrapFunction<State, Props>); /** @deprecated */ export function connectToStore<State extends IState, Props extends TDispatchProps> (...mappers: Mapper<State, Props>[]); /** @deprecated */ export function connectToStore<State extends IState, Props extends TDispatchProps> (...mappers: Mapper<State, Props>[]) { const conn = new ReactReduxConnector<State, Props>(); for (const mapper of mappers) { conn.addMapper(mapper); } return conn.connect.bind(conn); } function createMapper<State extends IState, Props>(mappers: Mapper<State, Props>[]): MapperFunction<State, Props> { if (mappers.length === 1 && typeof mappers[0] === 'function') { return <any>mappers[0]; } // let mapper: MapStateToPropsParam<Props, void>; const fns: MapperFunction<State, Props>[] = <any>mappers.filter((mapObject) => { return typeof mapObject === 'function'; }); const objects = Object.assign({}, ...mappers.filter((mapObject) => { return typeof mapObject !== 'function'; })); const keys: (keyof Props)[] = <any>Object.keys(objects); if (keys.length) { fns.push((state: State) => { const props: Partial<Props> = {}; for (const i of keys) { props[i] = objects[i](state); } return props; }); } return (data: State): Props => { const ret: Props = <any>{}; for (const fn of fns) { Object.assign(ret, fn(data)); } return ret; }; } export type triggerFn<IData> = (this: BaseComponent<TDispatchProps>, args: IData) => void; export function ActionDispatcher<IData> (Act: IReduxActionConstructor<IData>): PropertyDecorator; export function ActionDispatcher<IData, VI> (Act: IReduxActionConstructor<IData, VI>, Sto: IVirtualStoreConstructor<VI>): PropertyDecorator; export function ActionDispatcher<IData, VI=void> (Act: IReduxActionConstructor<IData, VI>, Sto?: IVirtualStoreConstructor<VI>): PropertyDecorator { return function (this: BaseComponent<any>, name: string) { if (this[name]) { throw new Error('can not use @ActionDispatch with property with value'); } this[name] = <triggerFn<IData>>function (this, value) { this.props._dispatch(new Act(value, Sto).toJSON()); }; }; } export type triggerMtd<IData> = (this: BaseComponent<TDispatchProps>, ...args: any[]) => IData; export type TypedMethodDecorator<T> = (target: Object, propertyKey: string|symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T>; export type TriggerDecorator<IData> = TypedMethodDecorator<triggerMtd<IData>>; export const CANCEL_TRIGGER = null; export function ActionTrigger<IData> (Act: IReduxActionConstructor<IData>): TriggerDecorator<IData>; export function ActionTrigger<IData, VI> (Act: IReduxActionConstructor<IData, VI>, Sto: IVirtualStoreConstructor<VI>): TriggerDecorator<IData>; export function ActionTrigger<IData, VI> (Act: IReduxActionConstructor<IData, VI>, Sto?: IVirtualStoreConstructor<VI>): TriggerDecorator<IData> { return (target: BaseComponentConstructor<any>, name, descriptor) => { if (!descriptor || !descriptor.value || typeof descriptor.value !== 'function') { throw new TypeError('ActionTrigger: only allow decorate method.'); } const original = descriptor.value; function trigger(this: BaseComponent<any>, ...args: any[]) { const ret = original.apply(this, args); if (ret !== undefined && ret !== CANCEL_TRIGGER) { this.props._dispatch(new Act(ret, Sto).toJSON()); } return ret; } return { value: <triggerMtd<IData>>trigger, }; }; }