on 2144ac9 help sign in
202 lines 5.6 kB history raw
1import { useCallback, useMemo, useState } from 'react';
2import { keyBy, get, every, some } from 'lodash';
4import { compose } from 'lib/lib';
5import { kDefaultValidator } from 'lib/validators';
6import useSetState from 'lib/useSetState';
8// interface InputConfig {
9// name: string;
10// validators: Function[];
11// transformers: Function[];
12// initialValue: string;
13// autoFocus: boolean;
14// disabled: boolean;
15// ...extra,
16// }
18const defaultsFor = (configs, mapper) =>
19 configs.reduce(
20 (memo, config) => ({ ...memo, [config.name]: mapper(config) }),
21 {}
22 );
24/**
25 * useForm manages a set of inputs for rendering them in a loop
26 */
27export default function useForm(configs = []) {
28 const byName = useMemo(() => keyBy(configs, 'name'), [configs]);
30 // track values
31 const [values, _setValues] = useState(() =>
32 defaultsFor(configs, config => config.initialValue || '')
33 );
34 // ^ NB(shrugs): because we're not syncing the initialValue of _additional_
35 // configs that are added, we won't be able to set an initialValue for a
36 // dynamic form that then triggers validation. for initial configs,
37 // initialValue can be a validated string and everything will be happy,
38 // but for dynamically added input configs, we won't get validation, etc
39 // until there's a traditional state update.
41 const setValue = useCallback(
42 (name, value) => _setValues(values => ({ ...values, [name]: value })),
43 [_setValues]
44 );
46 // track focused states
47 const [focused, setFocused] = useSetState(() =>
48 defaultsFor(configs, config => config.autoFocus && !config.disabled)
49 );
50 const focuses = useMemo(
51 () => configs.map(config => focused[config.name] || false), //
52 [configs, focused]
53 );
55 // track whether or not input has been focused
56 const [hasBeenFocused, setHasBeenFocused] = useSetState({});
58 // build fn that transforms a value by input name
59 const transform = useCallback(
60 (name, value) => compose(...get(byName, [name, 'transformers'], []))(value),
61 [byName]
62 );
64 // build fn that validates a value by input name
65 const validate = useCallback(
66 (name, value) =>
67 compose(
68 ...get(byName, [name, 'validators'], []),
69 kDefaultValidator
70 )(value),
71 [byName]
72 );
74 const getValue = useCallback(
75 (name, e) => {
76 if (byName[name].type === 'checkbox') {
77 return e.target.checked;
78 }
80 return e.target.value;
81 },
82 [byName]
83 );
85 // on change, transform and set value
86 const onChange = useCallback(
87 name => e => setValue(name, transform(name, getValue(name, e))), //
88 [setValue, transform, getValue]
89 );
91 // on focus, update focus
92 const onFocus = useCallback(
93 name => e => setFocused({ [name]: true }), //
94 [setFocused]
95 );
97 // on blur, defocus and set has been focused
98 const onBlur = useCallback(
99 name => e => {
100 setFocused({ [name]: false });
101 setHasBeenFocused({ [name]: true });
102 },
103 [setFocused, setHasBeenFocused]
104 );
106 // memo-compute validations of curent values
107 const validations = useMemo(
108 () => configs.map(config => validate(config.name, values[config.name])), //
109 [configs, validate, values]
110 );
112 // memo-compute the set of (perhaps changed by validation) data
113 const datas = useMemo(() => validations.map(v => v.data), [validations]);
115 // the input has errored if it
116 // 1) has been focused before
117 // 2)
118 // a) did not pass validation and has an error text
119 // b) or has a specific error from parent state
120 // (and we implicitly return the error text at the end)
121 const errors = useMemo(
122 () =>
123 configs.map(
124 (config, i) =>
125 hasBeenFocused[config.name] && //
126 (validations[i].error || config.error)
127 ),
128 [configs, validations, hasBeenFocused]
129 );
131 // this input has passed if
132 // 1) it has passed local validation
133 // 2) there are no errors
134 const passes = useMemo(
135 () => validations.map((v, i) => v.pass && !errors[i]),
136 [validations, errors]
137 );
139 // visibly tell the user that their input has passed if
140 // 1) they are or have interacted with the input before
141 // 2) and it has passed as defined above
142 const visiblePasses = useMemo(
143 () =>
144 configs.map(
145 (config, i) =>
146 (focused[config.name] || hasBeenFocused[config.name]) && //
147 passes[i]
148 ),
149 [configs, hasBeenFocused, focused, passes]
150 );
152 // generate the list of input states
153 const inputs = useMemo(
154 () =>
155 configs.map(
156 ({ name, error, autoFocus, disabled, initialValue, ...rest }, i) => ({
157 // Input props
158 name,
159 data: datas[i],
160 pass: passes[i],
161 visiblyPassed: visiblePasses[i],
162 error: errors[i],
163 focused: focuses[i],
164 autoFocus: autoFocus && !disabled,
165 disabled,
166 ...rest,
167 // dom properties below:
168 bind: {
169 value: values[name] || initialValue || '',
170 checked: !!values[name],
171 onChange: onChange(name),
172 onFocus: onFocus(name),
173 onBlur: onBlur(name),
174 autoFocus: autoFocus && !disabled,
175 },
176 })
177 ),
178 [
179 configs,
180 datas,
181 visiblePasses,
182 passes,
183 errors,
184 focuses,
185 values,
186 onBlur,
187 onChange,
188 onFocus,
189 ]
190 );
192 // did all of the inputs pass validation?
193 const pass = useMemo(() => every(passes), [passes]);
194 // did any of the inputs error?
195 const error = useMemo(() => some(errors), [errors]);
197 return {
198 inputs,
199 pass,
200 error,
201 };