import * as React from 'react'; import { Dispatch, Action } from 'redux'; import { createSelector, ParametricSelector } from 'reselect'; import { setUIState } from './actions'; ////////////////////////////////////////////////// // Id interfaces and functions /** * A string or a function accepting the props as an argument and returning a string */ export type IdFunction = (props: TProps) => string; export type Id = string | IdFunction; /** * A type guard guaranteeing id is a string; */ export const idIsString = (id: Id): id is string => typeof id === 'string'; /** * A type guard guaranteeing id is a function that accepts props and returns a string */ export const idIsFunction = (id: Id): id is IdFunction => typeof id === 'function'; /** * Returns a string or undefined from the union type of (string | function that returns string) */ export const getStringFromId = (id: Id, props: TProps): string | undefined => { if (idIsString(id)) { return id; } if (idIsFunction(id)) { return id(props); } return undefined; }; ////////////////////////////////////////////////// // Interfaces /** * The default shape of a store containing the uiState reducer */ export interface DefaultStoreState { // This is slightly gross and involves repetition of the value in DEFAULT_BRANCH_NAME, but // TypeScript doesn't currently allow computed property names in interfaces - [DEFAULT_BRANCH_NAME]: UIStateBranch; ui: UIStateBranch; } /** * The branch of the Redux store governed by reduxUIState's reducer */ export type UIStateBranch = Record; /** * The state props passed into a component wrapped by addReduxUIState */ export interface StateProps { uiState: TUIState; } /** * The dispatch props passed into a component wrapped by addReduxUIState */ export interface DispatchProps { setUIState: (state: Partial) => void; } /** * All the props props passed into a component wrapped by addReduxUIState */ export type Props = StateProps & DispatchProps; /** * The component passed into addReduxUIState */ export type AbstractWrappedComponent = React.StatelessComponent | React.ComponentClass; /** * A component being passed into addReduxUIState that accepts the default props */ export type WrappedComponentWithDefaultProps = AbstractWrappedComponent>; /** * A component being passed into addReduxUIState that accepts mapped props */ export type WrappedComponentWithMappedProps = AbstractWrappedComponent; // tslint:disable-line:max-line-length /** * A Selector that accepts the full application state and returns the Redux UI State branch */ // export type UIStateBranchSelector = (appState: TAppState) => UIStateBranch; // This looks weird, but the standard type definition (commented out above) causes a Reselect import error export interface UIStateBranchSelector extends ParametricSelector { (appState: TAppState): UIStateBranch; } /** * The props representing the key of the UI state data in the store */ export interface UIStateIdProps { uiStateId: Id; } /** * The function used to make changes to the state */ export type SetUIState = (state: Partial) => void; ////////////////////////////////////////////////// // Constants export const DEFAULT_BRANCH_NAME = 'ui'; export const UNDEFINED_KEY = 'REDUX_UI_STATE_UNDEFINED_KEY'; ////////////////////////////////////////////////// // Selectors export const stateSelector = (state: TAppState) => state; export const propsSelector = (_: any, props: TProps) => props; // tslint:disable-line:no-any /** * Selects the ui state branch from the default location in the Redux store */ export const defaultUIStateBranchSelector: UIStateBranchSelector = ( state: DefaultStoreState ): UIStateBranch => state[DEFAULT_BRANCH_NAME]; /** * The props containing the selector for the uiState branch. */ export interface UIStateBranchSelectorSelectorProps { uiStateBranchSelector?: UIStateBranchSelector; } /** * Selects the uiState branch selector, returning defaultUIStateBranchSelector if a custom is not found in the props */ export const uiStateBranchSelectorSelector = ( _: any, // tslint:disable-line:no-any props: UIStateBranchSelectorSelectorProps ) => { const uiStateBranchSelector = props && props.uiStateBranchSelector; if (!uiStateBranchSelector) { throw new Error( 'redux-ui-state Couldn\'t find uiStateBranchSelector in props - this is most likely because ' + 'createConnectUIState was called without an argument'); } return uiStateBranchSelector; }; export const uiStateBranchSelector = createSelector( uiStateBranchSelectorSelector, stateSelector, (selector, state) => { const branch = selector(state); if (!branch) { throw new Error( 'redux-ui-state Could not select UI state branch from the store - this is either because the reducer has not' + 'been composed properly or the selector is looking in the wrong location.' ); } return branch; } ); export const idSelector = ( _: any, // tslint:disable-line:no-any props: TProps & Partial> ) => { if (!props.uiStateId) { throw new Error( 'Couldn\'t find uiStateId prop for idSelector in Redux UI State - this usually occurs because the id passed ' + 'into connectUIState is undefined, or the uiStateId prop being passed into a component is undefined.' ); } return getStringFromId(props.uiStateId, props); }; export const UI_STATE_SELECTOR_WARNING_MESSAGE = 'redux-ui-state state key is undefined'; export const createUIStateSelectorWarningMessage = (id?: string) => ( `redux-ui-state uiStateSelector found undefined state for key: ${id}.\n` + `This is most often due to the value not being initialized in createReducer.` ); export const uiStateSelector = createSelector( uiStateBranchSelector, idSelector, (uiStateBranch, id) => { if (!id) { console.warn(UI_STATE_SELECTOR_WARNING_MESSAGE); } const uiState = uiStateBranch[id || UNDEFINED_KEY]; if (!uiState) { console.warn(createUIStateSelectorWarningMessage(id)); } // Since the branch in the store does not contain the types for each of the sub-branches // uiState is cast to any. This is recast to the UIState state generic in createUIState // connectUIState // tslint:disable-next-line:no-any return uiState as any; } ); export const SET_UI_STATE_SELECTOR_WARNING_MESSAGE = 'redux-ui-state - state key is undefined in setUIStateSelector\n' + 'This is due to an id not being passed via the props'; export const SET_UI_STATE_WARNING_MESSAGE = 'redux-ui-state - state key is undefined in setUIStateSelector\n' + 'This is due to an id not being passed via the props'; /** * Selects the setUIState function. * This differs from normal selectors in that it is a dispatch selector, meaning that it expects dispatch as the first * function and returns a function capable of dispatching through the Redux store. * @param dispatch The dispatch function of the Redux store * @param props The component's props */ export const setUIStateSelector = >( dispatch: Dispatch, props: TProps ) => { const id = idSelector(undefined, props); if (!id) { console.warn(SET_UI_STATE_SELECTOR_WARNING_MESSAGE); return (state: Partial) => { console.warn(SET_UI_STATE_WARNING_MESSAGE); }; } return (state: Partial) => { dispatch(setUIState>({ id, state })); }; };