react-formal
Version:
Classy HTML form management for React
138 lines (132 loc) • 4.06 kB
JavaScript
/* eslint-disable @typescript-eslint/no-shadow */
import { useRef, useMemo } from 'react';
import { useFieldMeta } from './useField';
import { move, remove, shift, unshift } from './Errors';
import { BITS, useFormContext } from './Contexts';
/**
* Retrieve the values at a given path as well as a set of array helpers
* for manipulating list values.
*
* ```jsx
* function ContactList() {
* const [values, arrayHelpers, meta] = useFieldArray("contacts")
*
* return (
* <ul>
* {values.map((value, idx) => (
* <li key={value.id}>
* <Form.Field name={`contacts[${idx}].name`} />
*
* <button onClick={() => arrayHelpers.remove(value)}>
* Remove
* </button>
* </li>
* )}
* </ul>
* )
* }
* ```
*
* @param name A field path, should point to an array value in the form data
*/
/**
* Retrieve the values at a given path as well as a set of array helpers
* for manipulating list values.
*
* ```jsx
* function ContactList() {
* const [values, arrayHelpers, meta] = useFieldArray({
* name: 'contacts',
* validates: 'otherField'
* })
*
* return (
* <ul>
* {values.map((value, idx) => (
* <li key={value.id}>
* <Form.Field name={`contacts[${idx}].name`} />
*
* <button onClick={() => arrayHelpers.remove(value)}>
* Remove
* </button>
* </li>
* )}
* </ul>
* )
* }
* ```
*
* @param name A field path, should point to an array value in the form data
*/
function useFieldArray(optionsOrName) {
var _options$exclusive;
let options = typeof optionsOrName === 'string' ? {
name: optionsOrName,
exclusive: true
} : optionsOrName;
let {
name
} = options;
const {
actions
} = useFormContext(BITS.actions);
// TODO: doesn't shallow validate validates
const fieldsToValidate = useMemo(() => [{
path: name,
shallow: true
}], [name]);
const meta = useFieldMeta(Object.assign({}, options, {
exclusive: (_options$exclusive = options.exclusive) != null ? _options$exclusive : true,
validates: fieldsToValidate
}));
const {
errors,
onError,
value,
onChange,
update
} = meta;
const sendErrors = fn => {
onError(fn(errors || {}, options.name));
};
const helpers = {
unshift: item => helpers.insert(item, 0),
add: item => helpers.push(item),
push: item => helpers.insert(item, value ? value.length : 0),
insert: (item, index) => {
const newValue = value == null ? [] : [...value];
newValue.splice(index, 0, item);
onChange(newValue);
sendErrors((errors, name) => unshift(errors, name, index));
},
move: (item, toIndex) => {
const fromIndex = value.indexOf(item);
const newValue = value == null ? [] : [...value];
if (fromIndex === -1) throw new Error('`onMove` must be called with an item in the array');
newValue.splice(toIndex, 0, ...newValue.splice(fromIndex, 1));
// FIXME: doesn't handle syncing error state. , { action: 'move', toIndex, fromIndex }
onChange(newValue);
sendErrors((errors, name) => move(errors, name, fromIndex, toIndex));
},
remove: item => {
if (value == null) return;
const index = value.indexOf(item);
onChange(value.filter(v => v !== item));
sendErrors((errors, name) => shift(errors, name, index));
},
onItemError: (name, errors) => {
sendErrors(fieldErrors => Object.assign({}, remove(fieldErrors, name), errors));
},
update: (updatedItem, oldItem) => {
const index = value.indexOf(oldItem);
const newValue = value == null ? [] : [...value];
newValue.splice(index, 1, updatedItem);
update(newValue);
// @ts-ignore
if (options.noValidate) return;
actions == null ? void 0 : actions.onValidate([`${name}[${index}]`], 'onChange', []);
}
};
return [value, Object.assign(useRef({}).current, helpers), meta];
}
export default useFieldArray;