on b465a33 help sign in
184 lines 5.1 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';
7// interface InputConfig {
8// name: string;
9// validators: Function[];
10// transformers: Function[];
11// initialValue: string;
12// autoFocus: boolean;
13// disabled: boolean;
14// ...extra,
15// }
17const defaultsFor = (configs, mapper) =>
18 configs.reduce(
19 (memo, config) => ({ ...memo, [config.name]: mapper(config) }),
20 {}
21 );
23/**
24 * useForm managest a set of inputs for rendering them in a loop
25 */
26export default function useForm(configs = []) {
27 const byName = useMemo(() => keyBy(configs, 'name'), [configs]);
29 // track values
30 const [values, _setValues] = useState(() =>
31 defaultsFor(configs, config => config.initialValue)
32 );
33 const setValue = useCallback(
34 (name, value) => _setValues(values => ({ ...values, [name]: value })),
35 [_setValues]
36 );
37 // ^ NB(shrugs): because we're not syncing the initialValue of _additional_
38 // configs that are added, we won't be able to set an initialValue for a
39 // dynamic form that then triggers validation. for initial configs,
40 // initialValue can be a validated string and everything will be happy,
41 // but for dynamically added input configs, we won't get validation, etc
42 // until there's a traditional state update.
44 // track focused states
45 const [focused, _setFocused] = useState(() =>
46 defaultsFor(configs, config => config.autoFocus && !config.disabled)
47 );
48 const focuses = useMemo(
49 () => configs.map(config => focused[config.name] || false), //
50 [configs, focused]
51 );
53 const setFocused = useCallback(
54 (name, value) => _setFocused(values => ({ ...values, [name]: value })),
55 [_setFocused]
56 );
58 // track whether or not input has been focused
59 const [hasBeenFocused, _setHasBeenFocused] = useState({});
60 const setHasBeenFocused = useCallback(
61 (name, value) =>
62 _setHasBeenFocused(values => ({ ...values, [name]: value })),
63 [_setHasBeenFocused]
64 );
66 const transform = useCallback(
67 (name, value) => compose(...get(byName, [name, 'transformers'], []))(value),
68 [byName]
69 );
70 const validate = useCallback(
71 (name, value) =>
72 compose(
73 ...get(byName, [name, 'validators'], []),
74 kDefaultValidator
75 )(value),
76 [byName]
77 );
79 const onChange = useCallback(
80 name => e => setValue(name, transform(name, e.target.value)), //
81 [setValue, transform]
82 );
83 const onFocus = useCallback(
84 name => e => setFocused(name, true), //
85 [setFocused]
86 );
87 const onBlur = useCallback(
88 name => e => {
89 setFocused(name, false);
90 setHasBeenFocused(name, true);
91 },
92 [setFocused, setHasBeenFocused]
93 );
95 // memo-compute whether or not the current value is valid
96 const validations = useMemo(
97 () => configs.map(config => validate(config.name, values[config.name])), //
98 [configs, validate, values]
99 );
101 const datas = useMemo(() => validations.map(v => v.data), [validations]);
102 const passes = useMemo(() => validations.map(v => v.pass), [validations]);
104 // the input has errored if it
105 // 1) did not pass validation
106 // 2) has an error text
107 // 3) has been focused before
108 // 4) is not currently focused
109 // returning the error text at the end
110 const errors = useMemo(
111 () =>
112 configs.map(
113 (config, i) =>
114 config.error ||
115 (!validations[i].pass &&
116 hasBeenFocused[config.name] &&
117 !focused[config.name] &&
118 validations[i].error)
119 ),
120 [configs, validations, hasBeenFocused, focused]
121 );
123 // visibly tell the user that their input has passed if
124 // 1) it has passed
125 // 2) there are no errors
126 // 3) they are or have interacted with the input before
127 const visiblePasses = useMemo(
128 () =>
129 configs.map(
130 (config, i) =>
131 validations[i].pass &&
132 !errors[i] &&
133 (hasBeenFocused[config.name] || focused[config.name])
134 ),
135 [configs, validations, errors, hasBeenFocused, focused]
136 );
138 const inputs = useMemo(
139 () =>
140 configs.map(
141 ({ name, error, autoFocus, disabled, initialValue, ...rest }, i) => ({
142 // Input props
143 name,
144 data: datas[i],
145 pass: passes[i],
146 visiblyPassed: visiblePasses[i],
147 error: errors[i],
148 focused: focuses[i],
149 autoFocus: autoFocus && !disabled,
150 disabled,
151 ...rest,
152 // dom properties below:
153 bind: {
154 value: values[name] || initialValue,
155 onChange: onChange(name),
156 onFocus: onFocus(name),
157 onBlur: onBlur(name),
158 autoFocus: autoFocus && !disabled,
159 },
160 })
161 ),
162 [
163 configs,
164 datas,
165 visiblePasses,
166 passes,
167 errors,
168 focuses,
169 values,
170 onBlur,
171 onChange,
172 onFocus,
173 ]
174 );
176 const pass = every(passes);
177 const error = some(errors);
179 return {
180 inputs,
181 pass,
182 error,
183 };