UNPKG

react-formal

Version:

Classy HTML form management for React

121 lines (118 loc) 4.8 kB
/* eslint-disable @typescript-eslint/no-empty-function */ import expr from 'property-expr'; import useUpdatedRef from '@restart/hooks/useUpdatedRef'; import React, { useCallback, useMemo, useContext, useRef, useState, useLayoutEffect } from 'react'; import updateIn from './utils/updateIn'; export const formGetter = (path, model) => path ? expr.getter(path, true)(model || {}) : model; export function formSetter(path, value, fieldValue) { return updateIn(value, path, fieldValue); } export const BindingContext = /*#__PURE__*/React.createContext({ getValue() {}, updateBindingValue() {} }); BindingContext.displayName = 'ReactFormalValueContext'; export const useBindingContext = () => { return useContext(BindingContext); }; const isEvent = e => typeof e == 'object' && e != null && 'target' in e; function parseValueFromEvent(target, fieldValue, fieldSchema) { const { type, value, checked, options, multiple, files } = target; if (type === 'file') return multiple ? files : files && files[0]; if (multiple) { var _fieldSchema$_subType; // @ts-ignore const innerType = fieldSchema == null ? void 0 : (_fieldSchema$_subType = fieldSchema._subType) == null ? void 0 : _fieldSchema$_subType._type; return Array.from(options).filter(opt => opt.selected).map(({ value: option }) => innerType == 'number' ? parseFloat(option) : option); } if (/number|range/.test(type)) { let parsed = parseFloat(value); return isNaN(parsed) ? null : parsed; } if (type === 'checkbox') { const isArray = Array.isArray(fieldValue); const isBool = !isArray && (fieldSchema == null ? void 0 : fieldSchema._type) === 'boolean'; if (isBool) return checked; const nextValue = isArray ? [...fieldValue] : []; const idx = nextValue.indexOf(value); if (checked) { if (idx === -1) nextValue.push(value); } else nextValue.splice(idx, 1); return nextValue; } return value; } function useFormBindingContext({ formValue, onChange, setter = formSetter, getter = formGetter, getSchemaForPath }) { // Why is this so complicated? // Well, calling onChange, from a binding multiple times would trigger // a change multiple times. Duh. This change, when controlled, might not flush // back through by the time the next change is called, leaving the updateBindingValue() // with a stale copy of `model`. React's setState avoids this with it's function // signature of useState, so we "queue" model changes locally in state, and // then "flush" them in an effect when the update is finished. let formValueRef = useUpdatedRef(formValue); let pendingChangeRef = useRef(false); let [pendingChange, setPendingChange] = useState([formValue, []]); // This assumes that we won't get an update until all the queued setState's fire, // then if there is a pending change we fire onChange with it and the consolidated // paths useLayoutEffect(() => { const [nextFormValue, paths] = pendingChange; if (pendingChangeRef.current) { pendingChangeRef.current = false; onChange(nextFormValue, paths); } }); const updateBindingValue = useCallback((mapValue, args) => { setPendingChange(pendingState => { let [nextModel, paths] = pendingState; // If there are no unflushed changes then use the current props model, assuming it // would be up to date. if (!pendingChangeRef.current) { pendingChangeRef.current = true; nextModel = formValueRef.current; paths = []; } Object.keys(mapValue).forEach(key => { let field = mapValue[key]; let value; if (typeof field === 'function') value = field(...args);else if (field === '.' || field == null || args[0] == null) value = args[0];else { value = expr.getter(field, true)(args[0]); } if (paths.indexOf(key) === -1) paths.push(key); if (isEvent(value)) value = parseValueFromEvent(value.target, formGetter(key, nextModel), getSchemaForPath(key, nextModel)); nextModel = setter(key, nextModel, value, formSetter); }); return [nextModel, paths]; }); }, [formValueRef, getSchemaForPath, setter]); const getValue = useCallback(pathOrAccessor => typeof pathOrAccessor === 'function' ? pathOrAccessor(formValue, getter) : getter(pathOrAccessor, formValue), [getter, formValue]); return useMemo(() => ({ getValue, updateBindingValue, getSchemaForPath: getSchemaForPath, updateFormValue: nextFormValue => { setPendingChange(() => { pendingChangeRef.current = true; return [nextFormValue, []]; }); }, formValue }), [getValue, updateBindingValue, setPendingChange, getSchemaForPath, formValue]); } export default useFormBindingContext;