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; renderCounts?: number; } export function tDispatchMapper(disp: Dispatch): TDispatchProps { const ret = {}; Object.defineProperty(ret, '_dispatch', { value: disp, configurable: false, enumerable: true, writable: false, }); return ret; } export type WrapFunction = (state: In) => Out; export type MapperObject = { [K in keyof Props]?: WrapFunction; }; export type MapperFunction = WrapFunction>; export type Mapper = MapperFunction|MapperObject; // MapperObject; export class ReactReduxConnector { protected mps: Mapper[] = []; protected advOpts: Options = { renderCountProp: 'renderCounts', shouldHandleStateChanges: true, withRef: false, pure: true, }; constructor() { this.connect = this.connect.bind(this); } addMapper(obj: Mapper) { this.mps.push(obj); } isComponentUseContext(notPure: boolean = true) { this.advOpts.pure = !notPure; } isComponentUseDOM(storeRef: boolean = true) { this.advOpts.withRef = storeRef; } connect>(reactClass: Class): Class { if (reactClass['_redux_connect']) { throw new TypeError(`duplicate @ReduxConnector() on ${reactClass.displayName || reactClass.name}`); } prepareReactClass(reactClass); let mapper: MapStateToProps; if (this.mps.length === 0) { this.advOpts.shouldHandleStateChanges = false; mapper = undefined; } else { this.advOpts.shouldHandleStateChanges = true; mapper = 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 = connect( mapper, tDispatchMapper, // TODO undefined, // TODO this.advOpts, ); const RetClass = 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(reactClass: BaseComponentConstructor) { if (!reactClass.propTypes) { reactClass.propTypes = {}; } reactClass.propTypes.renderCounts = PropTypes.number; } /** @deprecated */ export function connectToStore (mapper0: WrapFunction); /** @deprecated */ export function connectToStore (...mappers: Mapper[]); /** @deprecated */ export function connectToStore (...mappers: Mapper[]) { const conn = new ReactReduxConnector(); for (const mapper of mappers) { conn.addMapper(mapper); } return conn.connect.bind(conn); } function createMapper(mappers: Mapper[]): MapperFunction { if (mappers.length === 1 && typeof mappers[0] === 'function') { return mappers[0]; } // let mapper: MapStateToPropsParam; const fns: MapperFunction[] = mappers.filter((mapObject) => { return typeof mapObject === 'function'; }); const objects = Object.assign({}, ...mappers.filter((mapObject) => { return typeof mapObject !== 'function'; })); const keys: (keyof Props)[] = Object.keys(objects); if (keys.length) { fns.push((state: State) => { const props: Partial = {}; for (const i of keys) { props[i] = objects[i](state); } return props; }); } return (data: State): Props => { const ret: Props = {}; for (const fn of fns) { Object.assign(ret, fn(data)); } return ret; }; } export type triggerFn = (this: BaseComponent, args: IData) => void; export function ActionDispatcher (Act: IReduxActionConstructor): PropertyDecorator; export function ActionDispatcher (Act: IReduxActionConstructor, Sto: IVirtualStoreConstructor): PropertyDecorator; export function ActionDispatcher (Act: IReduxActionConstructor, Sto?: IVirtualStoreConstructor): PropertyDecorator { return function (this: BaseComponent, name: string) { if (this[name]) { throw new Error('can not use @ActionDispatch with property with value'); } this[name] = >function (this, value) { this.props._dispatch(new Act(value, Sto).toJSON()); }; }; } export type triggerMtd = (this: BaseComponent, ...args: any[]) => IData; export type TypedMethodDecorator = (target: Object, propertyKey: string|symbol, descriptor: TypedPropertyDescriptor) => TypedPropertyDescriptor; export type TriggerDecorator = TypedMethodDecorator>; export const CANCEL_TRIGGER = null; export function ActionTrigger (Act: IReduxActionConstructor): TriggerDecorator; export function ActionTrigger (Act: IReduxActionConstructor, Sto: IVirtualStoreConstructor): TriggerDecorator; export function ActionTrigger (Act: IReduxActionConstructor, Sto?: IVirtualStoreConstructor): TriggerDecorator { return (target: BaseComponentConstructor, 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, ...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: >trigger, }; }; }