1import { useCallback, useMemo, useState } from 'react';
2import { isEqual, keyBy, get, every, some } from 'lodash';
4import { compose } from 'lib/lib';
5import { kDefaultValidator } from 'lib/validators';
6import useSetState from 'lib/useSetState';
7import usePreviousValue from 'lib/usePreviousValue';
9// interface InputConfig {
11// validators: Function[];
12// transformers: Function[];
13// initialValue: string;
19const defaultsFor = (configs, mapper) =>
21 (memo, config) => ({ ...memo, [config.name]: mapper(config) }),
26 * useForm manages a set of inputs for rendering them in a loop
28export default function useForm(inputConfigs = []) {
29 // track the old value of the config set
30 const previousConfigs = usePreviousValue(inputConfigs);
31 // then compare equality
32 const configsAreEqual = isEqual(inputConfigs, previousConfigs);
33 // if equality changes, give `configs` a new identity
34 // eslint-disable-next-line react-hooks/exhaustive-deps
35 const configs = useMemo(() => inputConfigs, [configsAreEqual]);
37 const byName = useMemo(() => keyBy(configs, 'name'), [configs]);
40 const [values, _setValues] = useState(() =>
41 defaultsFor(configs, config => config.initialValue || '')
43 // ^ NB(shrugs): because we're not syncing the initialValue of _additional_
44 // configs that are added, we won't be able to set an initialValue for a
45 // dynamic form that then triggers validation. for initial configs,
46 // initialValue can be a validated string and everything will be happy,
47 // but for dynamically added input configs, we won't get validation, etc
48 // until there's a traditional state update.
50 // track focused states
51 const [focused, setFocused] = useSetState(() =>
52 defaultsFor(configs, config => config.autoFocus && !config.disabled)
54 const focuses = useMemo(
55 () => configs.map(config => focused[config.name] || false), //
59 // track whether or not input has been focused
60 const [hasBeenFocused, setHasBeenFocused] = useSetState({});
62 // build fn that transforms a value by input name
63 const transform = useCallback(
64 (name, value) => compose(...get(byName, [name, 'transformers'], []))(value),
68 // set a value (and transform it)
69 const setValue = useCallback(
71 _setValues(values => ({ ...values, [name]: transform(name, value) })),
72 [_setValues, transform]
75 // build fn that validates a value by input name
76 const validate = useCallback(
79 ...get(byName, [name, 'validators'], []),
85 const getValue = useCallback(
87 if (byName[name].type === 'checkbox') {
88 return e.target.checked;
91 return e.target.value;
96 // on change, transform and set value
97 const onChange = useCallback(
98 name => e => setValue(name, getValue(name, e)), //
102 // on focus, update focus
103 const onFocus = useCallback(
104 name => e => setFocused({ [name]: true }), //
108 // on blur, defocus and set has been focused
109 const onBlur = useCallback(
111 setFocused({ [name]: false });
112 setHasBeenFocused({ [name]: true });
114 [setFocused, setHasBeenFocused]
117 // memo-compute validations of curent values
118 const validations = useMemo(
119 () => configs.map(config => validate(config.name, values[config.name])), //
120 [configs, validate, values]
123 // memo-compute the set of (perhaps changed by validation) data
124 const datas = useMemo(() => validations.map(v => v.data), [validations]);
126 // the input has errored if it
127 // 1) has been focused before
129 // a) did not pass validation and has an error text
130 // b) or has a specific error from parent state
131 // (and we implicitly return the error text at the end)
132 const errors = useMemo(
136 hasBeenFocused[config.name] && //
137 (validations[i].error || config.error)
139 [configs, validations, hasBeenFocused]
142 // this input has passed if
143 // 1) it has passed local validation
144 // 2) there are no errors
145 const passes = useMemo(
146 () => validations.map((v, i) => v.pass && !errors[i]),
147 [validations, errors]
150 // visibly tell the user that their input has passed if
151 // 1) they are or have interacted with the input before
152 // 2) and it has passed as defined above
153 const visiblePasses = useMemo(
157 (focused[config.name] || hasBeenFocused[config.name]) && //
160 [configs, hasBeenFocused, focused, passes]
163 // generate the list of input states
164 const inputs = useMemo(
167 ({ name, error, autoFocus, disabled, initialValue, ...rest }, i) => ({
172 visiblyPassed: visiblePasses[i],
175 autoFocus: autoFocus && !disabled,
178 // dom properties below:
180 value: values[name] || initialValue || '',
181 checked: !!values[name],
182 onChange: onChange(name),
183 onFocus: onFocus(name),
184 onBlur: onBlur(name),
185 autoFocus: autoFocus && !disabled,
203 // did all of the inputs pass validation?
204 const pass = useMemo(() => every(passes), [passes]);
205 // did any of the inputs error?
206 const error = useMemo(() => some(errors), [errors]);
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list