import React, { useCallback, useMemo } from 'react'; import { fromPairs } from 'lodash'; import { Epic } from 'redux-observable'; import { EMPTY } from 'rxjs'; import { Action } from 'wdk-client/Actions'; import { DispatchAction } from 'wdk-client/Core/CommonTypes'; import { EpicDependencies } from 'wdk-client/Core/Store'; import { State, QuestionState } from 'wdk-client/StoreModules/QuestionStoreModule'; import { Parameter, ParameterValues, QuestionWithParameters } from 'wdk-client/Utils/WdkModel'; import { Props as FormProps } from 'wdk-client/Views/Question/DefaultQuestionForm'; // Types // ----- // FIXME Add full question, paramUIState and groupUIState // OR TODO: change parameter to parameterName to reduce type constraints export type Context = { searchName: string; parameter: T; paramValues: ParameterValues; } export type Props = { ctx: Context; parameter: T; value: string; uiConfig?: Record; uiState: S; dispatch: DispatchAction; onParamValueChange: (value: string) => void; } export type ParamModule = { isType: (parameter: Parameter) => parameter is T; /** * Determine if the param value is valid. This can be used by form container * to determine if submit should be disabled. The Component is responsible * for providing details about the invalid state. */ isParamValueValid: (context: Context, state: S) => boolean; reduce: (state: S, action: Action) => S; Component: React.ComponentType>; observeParam: Epic; /** * React to submit events. The Question will not be submitted until this is complete. */ getValueFromState: (context: Context, state: QuestionState, services: EpicDependencies) => string | Promise; } type ParamModuleSpec = Partial> & Pick, 'isParamValueValid' | 'Component' | 'isType'> export function createParamModule(spec: ParamModuleSpec): ParamModule { return { ...spec, reduce: spec.reduce || defaultReduce, observeParam: spec.observeParam || defaultObserve, getValueFromState: spec.getValueFromState || defaultGetValueFromState } } function defaultReduce(state: S, action: Action): S { return state; } function defaultObserve() { return EMPTY; } function defaultGetValueFromState(context: Context) { return context.paramValues[context.parameter.name]; } // Type guards (see https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards) // ----------------------------------------------------------------------------------------------------------- export function isPropsType( props: Props, predicate: (parameter: Parameter) => parameter is T ): props is Props { return predicate(props.parameter); } export function isContextType( context: Context, predicate: (parameter: Parameter) => parameter is T ): context is Context { return predicate(context.parameter); } // Hook for making custom parameter-updating callbacks export const useChangeParamValue = (parameter: Parameter, state: QuestionState, updateParamValue: FormProps['eventHandlers']['updateParamValue']) => { const searchName = state.question.urlSegment; const paramValues = state.paramValues; const changeParamValue = useCallback((paramValue: string) => { updateParamValue({ searchName, parameter, paramValues, paramValue }); }, [ updateParamValue, searchName, parameter, paramValues ]); return changeParamValue; }; export function useDependentParamsAreUpdating( question: QuestionWithParameters, paramsUpdatingDependencies: Record ) { const paramDependenciesUpdating = useParamDependenciesUpdating( question, paramsUpdatingDependencies ); return useMemo( () => Object.values(paramDependenciesUpdating).some(x => x), [paramDependenciesUpdating] ); } export function useParamDependenciesUpdating( question: QuestionWithParameters, paramsUpdatingDependencies: Record ) { return useMemo( () => makeParamDependenciesUpdating( question, paramsUpdatingDependencies ), [question, paramsUpdatingDependencies] ); } export function makeParamDependenciesUpdating( question: QuestionWithParameters, paramsUpdatingDependencies: Record ): Record { return fromPairs( question.parameters.filter( parameter => paramsUpdatingDependencies[parameter.name] ).flatMap( parameter => parameter.dependentParams.map( pn => [pn, true] ) ) ); }