on 9b85bb1 help sign in
171 lines 4.5 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 );
38 // track focused states
39 const [focused, _setFocused] = useState(() =>
40 defaultsFor(configs, config => config.autoFocus && !config.disabled)
41 );
42 const focuses = useMemo(
43 () => configs.map(config => focused[config.name] || false), //
44 [configs, focused]
45 );
47 const setFocused = useCallback(
48 (name, value) => _setFocused(values => ({ ...values, [name]: value })),
49 [_setFocused]
50 );
52 // track whether or not input has been focused
53 const [hasBeenFocused, _setHasBeenFocused] = useState({});
54 const setHasBeenFocused = useCallback(
55 (name, value) =>
56 _setHasBeenFocused(values => ({ ...values, [name]: value })),
57 [_setHasBeenFocused]
58 );
60 const transform = useCallback(
61 (name, value) => compose(...get(byName, [name, 'transformers'], []))(value),
62 [byName]
63 );
64 const validate = useCallback(
65 (name, value) =>
66 compose(
67 ...get(byName, [name, 'validators'], []),
68 kDefaultValidator
69 )(value),
70 [byName]
71 );
73 const onChange = useCallback(
74 name => e => setValue(name, transform(name, e.target.value)), //
75 [setValue, transform]
76 );
77 const onFocus = useCallback(
78 name => e => setFocused(name, true), //
79 [setFocused]
80 );
81 const onBlur = useCallback(
82 name => e => {
83 setFocused(name, false);
84 setHasBeenFocused(name, true);
85 },
86 [setFocused, setHasBeenFocused]
87 );
89 // memo-compute whether or not the current value is valid
90 const validations = useMemo(
91 () => configs.map(config => validate(config.name, values[config.name])), //
92 [configs, validate, values]
93 );
95 const datas = useMemo(() => validations.map(v => v.data), [validations]);
96 const passes = useMemo(() => validations.map(v => v.pass), [validations]);
98 // the input has errored if it
99 // 1) did not pass validation
100 // 2) has an error text
101 // 3) has been focused before
102 // 4) is not currently focused
103 // returning the error text at the end
104 const errors = useMemo(
105 () =>
106 configs.map(
107 (config, i) =>
108 !validations[i].pass &&
109 hasBeenFocused[config.name] &&
110 !focused[config.name] &&
111 validations[i].error
112 ),
113 [configs, validations, hasBeenFocused, focused]
114 );
116 // visibly tell the user that their input has passed if
117 // 1) it has passed
118 // 2) they are or have interacted with the input before
119 const visiblePasses = useMemo(
120 () =>
121 configs.map(
122 (config, i) =>
123 validations[i].pass &&
124 (hasBeenFocused[config.name] || focused[config.name])
125 ),
126 [configs, validations, hasBeenFocused, focused]
127 );
129 const inputs = useMemo(
130 () =>
131 configs.map((config, i) => ({
132 // Input props
133 data: datas[i],
134 pass: passes[i],
135 visiblyPassed: visiblePasses[i],
136 error: errors[i],
137 focused: focuses[i],
138 ...config,
139 autoFocus: config.autoFocus && !config.disabled,
140 // dom properties below:
141 bind: {
142 value: values[config.name],
143 onChange: onChange(config.name),
144 onFocus: onFocus(config.name),
145 onBlur: onBlur(config.name),
146 autoFocus: config.autoFocus && !config.disabled,
147 },
148 })),
149 [
150 configs,
151 datas,
152 visiblePasses,
153 passes,
154 errors,
155 focuses,
156 values,
157 onBlur,
158 onChange,
159 onFocus,
160 ]
161 );
163 const pass = every(passes);
164 const error = some(errors);
166 return {
167 inputs,
168 pass,
169 error,
170 };