UNPKG

6.81 kBPlain TextView Raw
1import * as PropTypes from "prop-types";
2import {connect, MapStateToProps, Options} from "react-redux";
3import {Dispatch} from "redux";
4import {BaseComponent, BaseComponentConstructor, StatefulBaseComponentConstructor} from "../react/stateless-component";
5import {IReduxActionConstructor} from "./action";
6import {IState} from "./preload-state";
7import {IVirtualStoreConstructor} from "./virtual-store";
8
9export interface TDispatchProps {
10 _dispatch?: Dispatch<any>;
11 renderCounts?: number;
12}
13
14export 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
25export type WrapFunction<In, Out> = (state: In) => Out;
26export type MapperObject<State extends IState, Props> = {
27 [K in keyof Props]?: WrapFunction<State, Props[K]>;
28 };
29export type MapperFunction<State extends IState, Props> = WrapFunction<State, Partial<Props>>;
30export type Mapper<State extends IState, Props> = MapperFunction<State, Props>|MapperObject<State, Props>;
31
32// MapperObject<Props, State>;
33
34export 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, // TODO
83 undefined, // TODO
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 // RetClass.displayName = `ReactReduxConnector(${reactClass.displayName || reactClass.name})`;
95
96 return RetClass;
97 }
98}
99
100function 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/** @deprecated */
109export function connectToStore<State extends IState, Props extends TDispatchProps>
110(mapper0: WrapFunction<State, Props>);
111/** @deprecated */
112export function connectToStore<State extends IState, Props extends TDispatchProps>
113(...mappers: Mapper<State, Props>[]);
114/** @deprecated */
115export 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
124function 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 // let mapper: MapStateToPropsParam<Props, void>;
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
154export type triggerFn<IData> = (this: BaseComponent<TDispatchProps>, args: IData) => void;
155
156export function ActionDispatcher<IData>
157(Act: IReduxActionConstructor<IData>): PropertyDecorator;
158export function ActionDispatcher<IData, VI>
159(Act: IReduxActionConstructor<IData, VI>, Sto: IVirtualStoreConstructor<VI>): PropertyDecorator;
160export 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
172export type triggerMtd<IData> = (this: BaseComponent<TDispatchProps>, ...args: any[]) => IData;
173export type TypedMethodDecorator<T> = (target: Object,
174 propertyKey: string|symbol,
175 descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T>;
176export type TriggerDecorator<IData> = TypedMethodDecorator<triggerMtd<IData>>;
177
178export const CANCEL_TRIGGER = null;
179
180export function ActionTrigger<IData>
181(Act: IReduxActionConstructor<IData>): TriggerDecorator<IData>;
182export function ActionTrigger<IData, VI>
183(Act: IReduxActionConstructor<IData, VI>,
184 Sto: IVirtualStoreConstructor<VI>): TriggerDecorator<IData>;
185export 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}