import * as React from 'react'; import * as PropTypes from 'prop-types'; import { Dispatch } from '../flux/store'; import { ApplicationStore, IApplicationStoreState } from '../store/applicationStore'; interface IApplicationStoreContext { applicationStore: ApplicationStore; } export interface IDispatcher { dispatch: Dispatch; } interface IApplicationStoreProps { state: IApplicationStoreState; } interface IDispatchProps { dispatch: Dispatch; } type FuncInfer = { ([...args]: any): T; }; type FunctionResult = T extends FuncInfer ? U : never; type ComponentDecoratorInfer = { (wrappedComponent: React.ComponentType): React.ComponentClass; }; export type Connect = (TMapProps extends undefined ? IApplicationStoreProps : FunctionResult) & (TMapDispatch extends undefined ? IDispatchProps : FunctionResult); export function connect(): ComponentDecoratorInfer; export function connect( mapStateToProps: (state: IApplicationStoreState) => TStateProps ): ComponentDecoratorInfer; export function connect( mapStateToProps: (state: IApplicationStoreState) => TStateProps, mapDispatchToProps: (dispatcher: IDispatcher) => TDispatchProps ): ComponentDecoratorInfer; export function connect( mapStateToProps: undefined, mapDispatchToProps: (dispatcher: IDispatcher) => TDispatchProps ): ComponentDecoratorInfer; export function connect( mapStateToProps?: ((state: IApplicationStoreState) => TStateProps), mapDispatchToProps?: ((dispatcher: IDispatcher) => TDispatchProps) ) : ((WrappedComponent: React.ComponentType) => React.ComponentType) { const injectApplicationStore = (WrappedComponent: React.ComponentType): React.ComponentType => { class StoreAdapter extends React.Component { public static contextTypes = { ...WrappedComponent.contextTypes, applicationStore: PropTypes.object.isRequired }; context: IApplicationStoreContext; render(): JSX.Element { if (isWrappedComponentIsVersion1(WrappedComponent)) { const element = React.createElement(WrappedComponent, { ...(this.props as any), state: this.store.state, dispatch: this.store.dispatch.bind(this.store) }); return element; } else if (mapStateToProps && isWrappedComponentIsVersion2(WrappedComponent)) { const element = React.createElement(WrappedComponent, { ...(this.props as any), ...(mapStateToProps(this.store.state) as any), dispatch: this.store.dispatch.bind(this.store) }); return element; } else if (mapStateToProps && mapDispatchToProps && isWrappedComponentIsVersion3(WrappedComponent)) { const element = React.createElement(WrappedComponent, { ...(this.props as any), ...(mapStateToProps(this.store.state) as any), ...(mapDispatchToProps({ dispatch: this.store.dispatch.bind(this.store)}) as any) }); return element; } else if (!mapStateToProps && mapDispatchToProps && isWrappedComponentIsVersion4(WrappedComponent)) { const element = React.createElement(WrappedComponent, { ...(this.props as any), state: this.store.state, ...(mapDispatchToProps({ dispatch: this.store.dispatch.bind(this.store)}) as any) }); return element; } throw new Error("Invalid arguments in connect."); } componentDidMount(): void { this.store && this.store.changed.addHandler(this.handleStoreChanged); } componentWillUnmount(): void { this.store && this.store.changed.removeHandler(this.handleStoreChanged); } private get store(): ApplicationStore { return this.context.applicationStore; } private handleStoreChanged = () => { this.forceUpdate(); } } return StoreAdapter; } return injectApplicationStore; /* inline methods */ function isWrappedComponentIsVersion1(wrappedComponent: any): wrappedComponent is React.ComponentType { return !mapStateToProps && !mapDispatchToProps; } function isWrappedComponentIsVersion2(wrappedComponent: any): wrappedComponent is React.ComponentType { return !!mapStateToProps && !mapDispatchToProps; } function isWrappedComponentIsVersion3(wrappedComponent: any): wrappedComponent is React.ComponentType { return !!mapStateToProps && !!mapDispatchToProps; } function isWrappedComponentIsVersion4(wrappedComponent: any): wrappedComponent is React.ComponentType { return !mapStateToProps && !!mapDispatchToProps; } } interface ApplicationStoreProviderProps extends React.Props { applicationStore: ApplicationStore; } export class ApplicationStoreProvider extends React.Component implements /* React.ComponentLifecycle, */ React.ChildContextProvider { public static childContextTypes = { applicationStore: PropTypes.object.isRequired }; getChildContext(): IApplicationStoreContext { return { applicationStore: this.props.applicationStore }; } render(): JSX.Element { return React.Children.only(this.props.children); } } export default connect;