@gongt/ts-stl-client
Version:
207 lines (178 loc) • 6.81 kB
text/typescript
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,
};
};
}