1 | import * as PropTypes from "prop-types";
|
2 | import {connect, MapStateToProps, Options} from "react-redux";
|
3 | import {Dispatch} from "redux";
|
4 | import {BaseComponent, BaseComponentConstructor, StatefulBaseComponentConstructor} from "../react/stateless-component";
|
5 | import {IReduxActionConstructor} from "./action";
|
6 | import {IState} from "./preload-state";
|
7 | import {IVirtualStoreConstructor} from "./virtual-store";
|
8 |
|
9 | export interface TDispatchProps {
|
10 | _dispatch?: Dispatch<any>;
|
11 | renderCounts?: number;
|
12 | }
|
13 |
|
14 | export function tDispatchMapper(disp: Dispatch<any>): TDispatchProps {
|
15 | const ret = {};
|
16 | Object.defineProperty(ret, '_dispatch', {
|
17 | value: disp,
|
18 | configurable: false,
|
19 | enumerable: true,
|
20 | writable: false,
|
21 | });
|
22 | return ret;
|
23 | }
|
24 |
|
25 | export type WrapFunction<In, Out> = (state: In) => Out;
|
26 | export type MapperObject<State extends IState, Props> = {
|
27 | [K in keyof Props]?: WrapFunction<State, Props[K]>;
|
28 | };
|
29 | export type MapperFunction<State extends IState, Props> = WrapFunction<State, Partial<Props>>;
|
30 | export type Mapper<State extends IState, Props> = MapperFunction<State, Props>|MapperObject<State, Props>;
|
31 |
|
32 |
|
33 |
|
34 | export class ReactReduxConnector<State extends IState, Props extends TDispatchProps> {
|
35 | protected mps: Mapper<State, Props>[] = [];
|
36 |
|
37 | protected advOpts: Options<any, any, any> = {
|
38 | renderCountProp: 'renderCounts',
|
39 | shouldHandleStateChanges: true,
|
40 | withRef: false,
|
41 | pure: true,
|
42 | };
|
43 |
|
44 | constructor() {
|
45 | this.connect = this.connect.bind(this);
|
46 | }
|
47 |
|
48 | addMapper(obj: Mapper<State, Props>) {
|
49 | this.mps.push(obj);
|
50 | }
|
51 |
|
52 | isComponentUseContext(notPure: boolean = true) {
|
53 | this.advOpts.pure = !notPure;
|
54 | }
|
55 |
|
56 | isComponentUseDOM(storeRef: boolean = true) {
|
57 | this.advOpts.withRef = storeRef;
|
58 | }
|
59 |
|
60 | connect<T extends object, Class extends StatefulBaseComponentConstructor<Props, T>>(reactClass: Class): Class {
|
61 | if (reactClass['_redux_connect']) {
|
62 | throw new TypeError(`duplicate @ReduxConnector() on ${reactClass.displayName || reactClass.name}`);
|
63 | }
|
64 |
|
65 | prepareReactClass(reactClass);
|
66 |
|
67 | let mapper: MapStateToProps<Props, void, State>;
|
68 | if (this.mps.length === 0) {
|
69 | this.advOpts.shouldHandleStateChanges = false;
|
70 | mapper = undefined;
|
71 | } else {
|
72 | this.advOpts.shouldHandleStateChanges = true;
|
73 | mapper = <any> createMapper(this.mps);
|
74 | }
|
75 |
|
76 | this.advOpts.getDisplayName = (name) => {
|
77 | return `RRConnector(${name}) [pure=${this.advOpts.pure},withRef=${this.advOpts.withRef},handle=${this.advOpts.shouldHandleStateChanges}]`;
|
78 | };
|
79 |
|
80 | const c: ClassDecorator = <any>connect<Props, TDispatchProps, void>(
|
81 | mapper,
|
82 | tDispatchMapper,
|
83 | undefined,
|
84 | this.advOpts,
|
85 | );
|
86 |
|
87 | const RetClass = <any>c(reactClass);
|
88 | Object.defineProperty(RetClass, '_redux_connect', {
|
89 | enumerable: false,
|
90 | configurable: false,
|
91 | writable: false,
|
92 | value: true,
|
93 | });
|
94 |
|
95 |
|
96 | return RetClass;
|
97 | }
|
98 | }
|
99 |
|
100 | function prepareReactClass<Props extends TDispatchProps>(reactClass: BaseComponentConstructor<Props>) {
|
101 | if (!reactClass.propTypes) {
|
102 | reactClass.propTypes = {};
|
103 | }
|
104 |
|
105 | reactClass.propTypes.renderCounts = PropTypes.number;
|
106 | }
|
107 |
|
108 |
|
109 | export function connectToStore<State extends IState, Props extends TDispatchProps>
|
110 | (mapper0: WrapFunction<State, Props>);
|
111 |
|
112 | export function connectToStore<State extends IState, Props extends TDispatchProps>
|
113 | (...mappers: Mapper<State, Props>[]);
|
114 |
|
115 | export function connectToStore<State extends IState, Props extends TDispatchProps>
|
116 | (...mappers: Mapper<State, Props>[]) {
|
117 | const conn = new ReactReduxConnector<State, Props>();
|
118 | for (const mapper of mappers) {
|
119 | conn.addMapper(mapper);
|
120 | }
|
121 | return conn.connect.bind(conn);
|
122 | }
|
123 |
|
124 | function createMapper<State extends IState, Props>(mappers: Mapper<State, Props>[]): MapperFunction<State, Props> {
|
125 | if (mappers.length === 1 && typeof mappers[0] === 'function') {
|
126 | return <any>mappers[0];
|
127 | }
|
128 |
|
129 | const fns: MapperFunction<State, Props>[] = <any>mappers.filter((mapObject) => {
|
130 | return typeof mapObject === 'function';
|
131 | });
|
132 | const objects = Object.assign({}, ...mappers.filter((mapObject) => {
|
133 | return typeof mapObject !== 'function';
|
134 | }));
|
135 | const keys: (keyof Props)[] = <any>Object.keys(objects);
|
136 | if (keys.length) {
|
137 | fns.push((state: State) => {
|
138 | const props: Partial<Props> = {};
|
139 | for (const i of keys) {
|
140 | props[i] = objects[i](state);
|
141 | }
|
142 | return props;
|
143 | });
|
144 | }
|
145 | return (data: State): Props => {
|
146 | const ret: Props = <any>{};
|
147 | for (const fn of fns) {
|
148 | Object.assign(ret, fn(data));
|
149 | }
|
150 | return ret;
|
151 | };
|
152 | }
|
153 |
|
154 | export type triggerFn<IData> = (this: BaseComponent<TDispatchProps>, args: IData) => void;
|
155 |
|
156 | export function ActionDispatcher<IData>
|
157 | (Act: IReduxActionConstructor<IData>): PropertyDecorator;
|
158 | export function ActionDispatcher<IData, VI>
|
159 | (Act: IReduxActionConstructor<IData, VI>, Sto: IVirtualStoreConstructor<VI>): PropertyDecorator;
|
160 | export function ActionDispatcher<IData, VI=void>
|
161 | (Act: IReduxActionConstructor<IData, VI>, Sto?: IVirtualStoreConstructor<VI>): PropertyDecorator {
|
162 | return function (this: BaseComponent<any>, name: string) {
|
163 | if (this[name]) {
|
164 | throw new Error('can not use @ActionDispatch with property with value');
|
165 | }
|
166 | this[name] = <triggerFn<IData>>function (this, value) {
|
167 | this.props._dispatch(new Act(value, Sto).toJSON());
|
168 | };
|
169 | };
|
170 | }
|
171 |
|
172 | export type triggerMtd<IData> = (this: BaseComponent<TDispatchProps>, ...args: any[]) => IData;
|
173 | export type TypedMethodDecorator<T> = (target: Object,
|
174 | propertyKey: string|symbol,
|
175 | descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T>;
|
176 | export type TriggerDecorator<IData> = TypedMethodDecorator<triggerMtd<IData>>;
|
177 |
|
178 | export const CANCEL_TRIGGER = null;
|
179 |
|
180 | export function ActionTrigger<IData>
|
181 | (Act: IReduxActionConstructor<IData>): TriggerDecorator<IData>;
|
182 | export function ActionTrigger<IData, VI>
|
183 | (Act: IReduxActionConstructor<IData, VI>,
|
184 | Sto: IVirtualStoreConstructor<VI>): TriggerDecorator<IData>;
|
185 | export function ActionTrigger<IData, VI>
|
186 | (Act: IReduxActionConstructor<IData, VI>,
|
187 | Sto?: IVirtualStoreConstructor<VI>): TriggerDecorator<IData> {
|
188 | return (target: BaseComponentConstructor<any>, name, descriptor) => {
|
189 | if (!descriptor || !descriptor.value || typeof descriptor.value !== 'function') {
|
190 | throw new TypeError('ActionTrigger: only allow decorate method.');
|
191 | }
|
192 | const original = descriptor.value;
|
193 |
|
194 | function trigger(this: BaseComponent<any>, ...args: any[]) {
|
195 | const ret = original.apply(this, args);
|
196 | if (ret !== undefined && ret !== CANCEL_TRIGGER) {
|
197 | this.props._dispatch(new Act(ret, Sto).toJSON());
|
198 | }
|
199 | return ret;
|
200 | }
|
201 |
|
202 | return {
|
203 | value: <triggerMtd<IData>>trigger,
|
204 | };
|
205 | };
|
206 | }
|