/* * License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt */ import {useContext, useState, useEffect, useRef} from 'react'; import PropTypes from 'prop-types'; import {get,omit} from 'lodash'; import {dispatchMountComponent,dispatchValueChange} from '../fieldGroup/FieldGroupCntlr.js'; import FieldGroupUtils, {getFieldGroupState} from '../fieldGroup/FieldGroupUtils.js'; import {flux} from '../Firefly.js'; import {GroupKeyCtx} from './FieldGroup'; import {isDefined} from '../util/WebUtil'; const defaultConfirmValue= (v) => v; const defValidatorFunc= () => ({valid:true,message:''}); const STORE_OMIT_LIST= ['fieldKey', 'groupKey', 'initialState', 'fireReducer', 'confirmValue', 'confirmValueOnInit', 'forceReinit', 'mounted']; function buildViewProps(fieldState,props,fieldKey,groupKey,value='') { const {message= '', valid= true, visible= true, value:ignoreValue, displayValue= '', tooltip= '', validator= defValidatorFunc, ...rest}= fieldState; const propsClean= Object.keys(props).reduce( (obj,k)=> { if (isDefined(props[k]) && k!=='value') obj[k]=props[k]; return obj; },{} ); const tmpProps= Object.assign({ message, valid, visible, value, displayValue, tooltip, validator, key:`${groupKey}-${fieldKey}`}, rest, propsClean); return omit(tmpProps, STORE_OMIT_LIST); } function showValueWarning(groupKey, fieldKey, value, ignoring) { const extra= ignoring ? 'value property is being ignored, since initialState.value is defined' : ''; console.warn( `useFieldGroupConnector: fieldKey: ${fieldKey}, groupKey: ${groupKey}: value should not be passed in props\n`, `Only initial value should be passed as in the initialState property, ${extra}\n`, `i.e. initialState= {value: '${value}'} `); } function validateKeys(fieldKey, groupKey) { if (!fieldKey) throw Error('useFieldGroupConnector: fieldKey is required for useFieldGroupConnector.'); if (!groupKey) throw Error('useFieldGroupConnector: groupKey is required for useFieldGroupConnector, groupKey is usually passed though context but may be a prop.'); } function isInit(infoRef,fieldKey,groupKey) { const {prevFieldKey, prevGroupKey}= infoRef.current; return (prevFieldKey!==fieldKey || prevGroupKey !== groupKey); } function hasNewKeys(infoRef,fieldKey,groupKey) { const {prevFieldKey, prevGroupKey}= infoRef.current; return isInit(infoRef,fieldKey,groupKey) && prevFieldKey && prevGroupKey; } function doGetInitialState(groupKey, initialState, props, confirmValueOnInit= defaultConfirmValue) { const {fieldKey, forceReinit}= props; const storeField= get(FieldGroupUtils.getGroupFields(groupKey), [fieldKey]); const initS= forceReinit ? (initialState || storeField || {}) : (storeField || initialState || {}); return {...initS, value: confirmValueOnInit(initS.value,props,initialState)}; } /** * @global * @public * @typedef {Object} ConnectorInterface * * @summary Return value of useFieldGroupConnector. This object contains the view components properties (viewProps) and the * fireValueChange. Simple pass the viewProps to the component and call fireValueChange when the view component has a * value change call. * * @prop {function(payload:Object)} fireValueChange- Call with the value of the view changes. The parameter is an object * the should contains as least a value property plus anything else that is appropriate to pass * @prop {Object} viewProps - all the properties passed to useFieldGroupConnector with the connection properties * removed. This object should contain all the properties to pass on the the view component * @prop {String} fieldKey - the passed fieldKey, you don't usually need to access this value * @prop {String} groupKey - the passed groupKey, you don't usually need to access this value */ /** * useFieldGroupConnector parameters expressed as propTypes */ export const fgConnectPropsTypes= { fieldKey: PropTypes.string.isRequired, groupKey: PropTypes.string, // normally passed in context forceReinit: PropTypes.bool, initialState: PropTypes.shape({ // not all fields use everything in initialState, most of it is optional value: PropTypes.any, // this is the most common one, it is the initial value for the field. message: PropTypes.string, validator: PropTypes.func, displayValue: PropTypes.string, tooltip: PropTypes.string, label: PropTypes.string, }), confirmValueOnInit: PropTypes.func, confirmValue: PropTypes.func }; /** * Minimal set of properties ot use for useFieldGroupConnector */ export const fgMinPropTypes= { fieldKey: PropTypes.string.isRequired, groupKey: PropTypes.string, // normally passed in context }; /** * @name ConfirmValueFunc * Give a value, props, and state, return the same or a updated value. It must return a value. This is most often * used with a radio box type component when the value must be one in the list. * @function * @param {*} value * @param {Object} props * @param {Object} state * @returns * */ /** * * Hook to connect a field to the FieldGroup Store. Pass the props object, make sure it includes the required props * to connect to the store (fieldKey is the only requirement, see below). The hooks returns an object with the new * props that you should be able to pass directly to the view. * * the props object parameter can contain any that should be kept in the store. The parameters below are special. * fieldKey is required. * * @param {Object} props * @param {string} props.fieldKey - required, a unique id for this field (unique within group) * @param {string} [props.groupKey] - optional - a unique group id, normally this is not use because it is passed in the context * @param {string} [props.initialState] - optional - the initial state object, anything in the initialState, * should be thought of as state data, it can change over the lifetime of the component and is not controlled by props. * Typically only items like value (and possibly displayValue) are in the state but other items can be manage there as well. * @param {ConfirmValueFunc} [props.confirmValueOnInit] - optional - If defined it will be call only on init * @param {ConfirmValueFunc} [props.confirmValue] - optional - If defined it called every update or init. * @param {boolean} [props.forceReinit] - optional - if true, this field will be reinited from the properties and not from the field group, * it is almost always unnecessary, only use if you know what you are doing and even then make sure. * @return {ConnectorInterface} * */ export const useFieldGroupConnector= (props) => { const infoRef = useRef({prevFieldKey:undefined, prevGroupKey:undefined}); const context= useContext(GroupKeyCtx); const {fieldKey,confirmValue,confirmValueOnInit}= props; let {initialState}= props; const groupKey= props.groupKey || context.groupKey; const doingInit= isInit(infoRef,fieldKey,groupKey); if (doingInit) {// validation checks validateKeys(fieldKey,groupKey); if (isDefined(props.value)) { showValueWarning(groupKey, fieldKey, props.value, (initialState && isDefined(initialState.value))); initialState= {value:props.value, ...initialState}; } } const getInitialState= () => doingInit ? doGetInitialState(groupKey, initialState, props, (confirmValueOnInit||confirmValue)) : undefined; const [fieldState, setFieldState] = useState(getInitialState()); const fireValueChange= (payload) => dispatchValueChange({...payload, fieldKey,groupKey}); const value= confirmValue ? confirmValue(fieldState.value,props,fieldState) : fieldState.value; const effectChangeAry= [fieldKey, groupKey, fieldState]; if (confirmValue) effectChangeAry.push(value); // only need to watch value in this case useEffect(() => { if (doingInit) { // called the first time or when fieldKey or groupKey change let value= fieldState.value; if (hasNewKeys(infoRef,fieldKey,groupKey)) { // if field and group key changed, whole thing must reinit const {prevFieldKey, prevGroupKey}= infoRef.current; dispatchMountComponent( prevGroupKey, prevFieldKey, false ); const initFieldState= getInitialState(); setFieldState(initFieldState); value= initFieldState.value; } dispatchMountComponent( groupKey, fieldKey, true, value, initialState ); infoRef.current={prevFieldKey: fieldKey, prevGroupKey: groupKey}; } else if (confirmValue) {// in this case the value might have been updated during the render if (fieldState.value!==value) fireValueChange({value}); // put value back in sync } return flux.addListener(()=> { const gState = getFieldGroupState(groupKey); if (!gState || !gState.mounted || !get(gState,['fields',fieldKey])) return; if (fieldState !== gState.fields[fieldKey]) setFieldState(gState.fields[fieldKey]); }); }, effectChangeAry); return { fireValueChange, viewProps: buildViewProps(fieldState,props,fieldKey,groupKey, value), fieldKey, groupKey }; };