react-formal
Version:
Classy HTML form management for React
288 lines (282 loc) • 9.3 kB
JavaScript
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;