on 30fffab help sign in
201 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 const setValue = useCallback(
35 (name, value) => _setValues(values => ({ ...values, [name]: value })),
36 [_setValues]
37 );
38 // ^ NB(shrugs): because we're not syncing the initialValue of _additional_
39 // configs that are added, we won't be able to set an initialValue for a
40 // dynamic form that then triggers validation. for initial configs,
41 // initialValue can be a validated string and everything will be happy,
42 // but for dynamically added input configs, we won't get validation, etc
43 // until there's a traditional state update.
45 // track focused states
46 const [focused, setFocused] = useSetState(() =>
47 defaultsFor(configs, config => config.autoFocus && !config.disabled)
48 );
49 const focuses = useMemo(
50 () => configs.map(config => focused[config.name] || false), //
51 [configs, focused]
52 );
54 // track whether or not input has been focused
55 const [hasBeenFocused, setHasBeenFocused] = useSetState({});
57 // build fn that transforms a value by input name
58 const transform = useCallback(
59 (name, value) => compose(...get(byName, [name, 'transformers'], []))(value),
60 [byName]
61 );
63 // build fn that validates a value by input name
64 const validate = useCallback(
65 (name, value) =>
66 compose(
67 ...get(byName, [name, 'validators'], []),
68 kDefaultValidator
69 )(value),
70 [byName]
71 );
73 const getValue = useCallback(
74 (name, e) => {
75 if (byName[name].type === 'checkbox') {
76 return e.target.checked;
77 }
79 return e.target.value;
80 },
81 [byName]
82 );
84 // on change, transform and set value
85 const onChange = useCallback(
86 name => e => setValue(name, transform(name, getValue(name, e))), //
87 [setValue, transform, getValue]
88 );
90 // on focus, update focus
91 const onFocus = useCallback(
92 name => e => setFocused({ [name]: true }), //
93 [setFocused]
94 );
96 // on blur, defocus and set has been focused
97 const onBlur = useCallback(
98 name => e => {
99 setFocused({ [name]: false });
100 setHasBeenFocused({ [name]: true });
101 },
102 [setFocused, setHasBeenFocused]
103 );
105 // memo-compute validations of curent values
106 const validations = useMemo(
107 () => configs.map(config => validate(config.name, values[config.name])), //
108 [configs, validate, values]
109 );
111 // memo-compute the set of (perhaps changed by validation) data
112 const datas = useMemo(() => validations.map(v => v.data), [validations]);
114 // the input has errored if it
115 // 1) has been focused before
116 // 2)
117 // a) did not pass validation and has an error text
118 // b) or has a specific error from parent state
119 // (and we implicitly return the error text at the end)
120 const errors = useMemo(
121 () =>
122 configs.map(
123 (config, i) =>
124 hasBeenFocused[config.name] && //
125 (validations[i].error || config.error)
126 ),
127 [configs, validations, hasBeenFocused]
128 );
130 // this input has passed if
131 // 1) it has passed local validation
132 // 2) there are no errors
133 const passes = useMemo(
134 () => validations.map((v, i) => v.pass && !errors[i]),
135 [validations, errors]
136 );
138 // visibly tell the user that their input has passed if
139 // 1) they are or have interacted with the input before
140 // 2) and it has passed as defined above
141 const visiblePasses = useMemo(
142 () =>
143 configs.map(
144 (config, i) =>
145 (focused[config.name] || hasBeenFocused[config.name]) && //
146 passes[i]
147 ),
148 [configs, hasBeenFocused, focused, passes]
149 );
151 // generate the list of input states
152 const inputs = useMemo(
153 () =>
154 configs.map(
155 ({ name, error, autoFocus, disabled, initialValue, ...rest }, i) => ({
156 // Input props
157 name,
158 data: datas[i],
159 pass: passes[i],
160 visiblyPassed: visiblePasses[i],
161 error: errors[i],
162 focused: focuses[i],
163 autoFocus: autoFocus && !disabled,
164 disabled,
165 ...rest,
166 // dom properties below:
167 bind: {
168 value: values[name] || initialValue,
169 checked: !!values[name],
170 onChange: onChange(name),
171 onFocus: onFocus(name),
172 onBlur: onBlur(name),
173 autoFocus: autoFocus && !disabled,
174 },
175 })
176 ),
177 [
178 configs,
179 datas,
180 visiblePasses,
181 passes,
182 errors,
183 focuses,
184 values,
185 onBlur,
186 onChange,
187 onFocus,
188 ]
189 );
191 // did all of the inputs pass validation?
192 const pass = useMemo(() => every(passes), [passes]);
193 // did any of the inputs error?
194 const error = useMemo(() => some(errors), [errors]);
196 return {
197 inputs,
198 pass,
199 error,
200 };