UNPKG

react-formal

Version:

Classy HTML form management for React

288 lines (282 loc) 9.3 kB
function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } import { useCallback, useMemo, useRef } from 'react'; import useBinding from './useBinding'; import { BITS, useFormContext } from './Contexts'; import config from './config'; import isNativeType from './utils/isNativeType'; import { toArray } from './utils/paths'; import notify from './utils/notify'; import useErrors from './useErrors'; import useFieldSchema from './useFieldSchema'; export function splitFieldProps(_ref) { let { name, type, mapFromValue, mapToValue, validates, validateOn, exclusive, noValidate, errorClass, className, onChange, onBlur, value, as } = _ref, rest = _objectWithoutPropertiesLoose(_ref, ["name", "type", "mapFromValue", "mapToValue", "validates", "validateOn", "exclusive", "noValidate", "errorClass", "className", "onChange", "onBlur", "value", "as"]); return [{ name, type, as, mapFromValue, mapToValue, validates, validateOn, exclusive, noValidate, errorClass, className, onChange, onBlur, value }, rest]; } function useEvents(validateOn = config.validateOn, meta) { validateOn = validateOn === undefined ? config.validateOn : validateOn; validateOn = typeof validateOn === 'function' ? validateOn(meta) : validateOn; return (typeof validateOn === 'string' ? { [validateOn]: true } : validateOn) || {}; } function resolveNativeInputConfig(type, asProp) { let tagName = 'input'; if (type === 'boolean') type = 'checkbox'; if (type === 'array' || asProp === 'select') { tagName = 'select'; } if (asProp === 'textarea') tagName = 'textarea'; return tagName === 'input' ? { tagName, type: isNativeType(type) ? type : 'text' } : { tagName }; } const onChangeStrategy = { change: true }; const onBlurStrategy = { blur: true }; const onChangeAndBlurStrategy = { change: true, blur: true }; const onBlurThenChangeAndBlurStrategy = meta => ({ blur: true, change: !meta.valid }); export const ValidateStrategies = { Change: onChangeStrategy, Blur: onBlurStrategy, ChangeAndBlur: onChangeAndBlurStrategy, BlurThenChangeAndBlur: onBlurThenChangeAndBlurStrategy }; const passThrough = v => v; export function useFieldMeta(opts) { let { name, type, as: asProp, validates, exclusive, noValidate, mapToValue, mapFromValue = passThrough, errorClass = config.errorClass } = opts; const [value, onChange] = useBinding(mapToValue || name, mapFromValue); const warned = useRef(false); const { actions, touched, submits } = useFormContext(BITS.actions | BITS.touched | BITS.submits); const filteredErrors = useErrors(name, { inclusive: !exclusive }); let handleFieldError = errors => actions.onFieldError(name, errors); const schema = useFieldSchema(name); if (process.env.NODE_ENV !== 'production') { const shouldWarn = warned.current === false && !schema && !noValidate && (actions == null ? void 0 : actions.formHasValidation()); if (shouldWarn) { warned.current = true; console.warn(`There is no corresponding schema defined for this field: "${name}" ` + "Each Field's `name` prop must be a valid path defined by the parent Form schema"); } } const onValidate = actions == null ? void 0 : actions.onValidate; let meta = Object.assign({ schema, errorClass, context: actions == null ? void 0 : actions.yupContext, touched: touched[name], onError: handleFieldError, value, update: onChange, // Add an onChange handler to `meta` so that custom inputs // don't need to infer the events configured for a Field onChange: useCallback((...args) => { onChange(...args); if (noValidate || !onValidate) return; onValidate(validates, 'onChange', args); }, [onChange, validates, noValidate, onValidate]) }, submits); meta.errors = filteredErrors; meta.invalid = !!Object.keys(filteredErrors).length; meta.valid = !meta.invalid; // @ts-ignore let resolvedType = type || meta.schema && meta.schema._type; meta.resolvedType = resolvedType; let nativeConfig = resolveNativeInputConfig(resolvedType, asProp); meta.nativeType = nativeConfig.type; meta.nativeTagName = nativeConfig.tagName; return meta; } /** * Create a new form field for the provided name, takes the same options * as `Field` props. * * * ```jsx * function MyNameField(props) { * const [fieldProps, meta] = useField('firstName') * * return ( * <input * {...fieldProps} * className={meta.invalid ? 'field-error' : ''} * /> * ) * } * ``` * * @param {string} name The Field name, which should be path corresponding to a specific form `value` path. */ /** * Create a new form field for the provided name, takes the same options * as `Field` props. * * ```jsx * function MyNameField(props) { * const [fieldProps, meta] = useField({ name: 'firstName' }) * * return ( * <input * {...fieldProps} * className={meta.invalid ? 'field-error' : ''} * /> * ) * } * ``` * * @param options * @param {string} options.name The Field name, which should be path corresponding to a specific form `value` path. * @param {any=} options.value For checkbox/boolean fields override the HTML default value for checks from `'on'` * @param {MapToValue=} options.mapToValue A mapper from the form value to fieldProps.value` * @param {(string|MapFromValue)=} options.mapFromValue A mapper from the form value to fieldProps.value` * @param {(string|string[]|null)=} options.validates Triggers validation for additional field paths * @param {('change' | 'blur' | { blur?: boolean; change?: boolean } | (meta: FieldMeta) => { blur?: boolean; change?: boolean })=} options.validateOn configure which events trigger validation. */ function useField(optionsOrName) { let options = typeof optionsOrName === 'string' ? { name: optionsOrName } : optionsOrName; let { name, as: asProp, validates, noValidate, onChange, onBlur } = options; const fieldsToValidate = useMemo(() => validates != null ? toArray(validates) : [name], [name, validates]); const { actions } = useFormContext(BITS.actions); const meta = useFieldMeta(Object.assign({}, options, { validates: fieldsToValidate })); meta.validateOn = useEvents(options.validateOn, meta); const { blur, change } = meta.validateOn; const { update } = meta; const validate = actions == null ? void 0 : actions.onValidate; const fieldProps = { onChange: useCallback((...args) => { notify(onChange, args); notify(update, args); if (!change || noValidate || !validate) return; validate(fieldsToValidate, 'onChange', args); }, [change, fieldsToValidate, noValidate, onChange, update, validate]), onBlur: useCallback((...args) => { notify(onBlur, args); if (!blur || noValidate || !validate) return; validate(fieldsToValidate, 'onBlur', args); }, [blur, fieldsToValidate, noValidate, onBlur, validate]) }; // always include an onChange if (!fieldProps.onChange) { fieldProps.onChange = update; } fieldProps.name = name; fieldProps.value = meta.value; // lots of rigamorole here. We only want to be // clever with props if the field `as` is likely to be a native // input. So only do this if const elementType = asProp || meta.nativeTagName; const valueIsNull = fieldProps.value == null; if (elementType === 'input') { const type = meta.nativeType; fieldProps.type = type; if (type === 'checkbox' || type === 'radio') { if (options.value === undefined) { fieldProps.checked = !!fieldProps.value; } else { fieldProps.checked = type === 'radio' ? fieldProps.value === options.value : Array.isArray(fieldProps.value) ? fieldProps.value.includes(options.value) : // if the value is not an array IDK seems like bad config? !!fieldProps.value; } fieldProps.value = options.value; } else if (type === 'file') { fieldProps.value = ''; } else { // all other inputs, default to empty string fieldProps.value = valueIsNull ? '' : fieldProps.value; } } else if (elementType === 'textarea') { // default null to empty string fieldProps.value = valueIsNull ? '' : fieldProps.value; // } else if (elementType === 'select') { // default to empty array for multiple selects if (meta.resolvedType === 'array') { fieldProps.value = valueIsNull ? [] : fieldProps.value; fieldProps.multiple = true; } else if (valueIsNull) { fieldProps.value = ''; } } let className = options.className || ''; if (!noValidate && meta.invalid && meta.errorClass) { className = className + ' ' + meta.errorClass; } if (className) fieldProps.className = className; return [fieldProps, meta]; } export default useField;