@@ -61,6 +61,9 @@ export default function useForm(inputConfigs = []) {
61 // track whether or not input has been focused
61 // track whether or not input has been focused
62 const [hasBeenFocused, setHasBeenFocused] = useSetState({});
62 const [hasBeenFocused, setHasBeenFocused] = useSetState({});
64+ // track whether or not the input has been touched
65+ const [hasBeenTouched, setHasBeenTouched] = useSetState({});
64 // build fn that transforms a value by input name
67 // build fn that transforms a value by input name
65 const transform = useCallback(
68 const transform = useCallback(
66 (name, value) => compose(...get(byName, [name, 'transformers'], []))(value),
69 (name, value) => compose(...get(byName, [name, 'transformers'], []))(value),
@@ -97,8 +100,11 @@ export default function useForm(inputConfigs = []) {
98 // on change, transform and set value
101 // on change, transform and set value
99 const onChange = useCallback(
102 const onChange = useCallback(
100− name => e => setValue(name, getValue(name, e)), //
104+ setValue(name, getValue(name, e));
105+ setHasBeenTouched({ [name]: true });
101− [setValue, getValue]
107+ [setValue, getValue, setHasBeenTouched]
104 // on focus, update focus
110 // on focus, update focus
@@ -126,27 +132,45 @@ export default function useForm(inputConfigs = []) {
126 const datas = useMemo(() => validations.map(v => v.data), [validations]);
132 const datas = useMemo(() => validations.map(v => v.data), [validations]);
128 // the input has errored if it
134 // the input has errored if it
135+ // 1) did not pass validation and has an error text
136+ // 2) or has a specific error from parent state
137+ // and then we store that in errorTexts[i]
138+ // <string | falsy>[]
139+ const errorTexts = useMemo(
141+ configs.map((config, i) => validations[i].error || config.error || false),
142+ [configs, validations]
145+ // we should hint at an error if it
129 // 1) has been focused before
146 // 1) has been focused before
131− // a) did not pass validation and has an error text
147+ // 2) and has an error
132− // b) or has a specific error from parent state
148+ // boolean[]
133− // (and we implicitly return the error text at the end)
134− const errors = useMemo(
149+ const hintErrors = useMemo(
136 configs.map(
151 configs.map(
138− hasBeenFocused[config.name] && //
152+ (config, i) => hasBeenFocused[config.name] && !!errorTexts[i]
139− (validations[i].error || config.error)
141− [configs, validations, hasBeenFocused]
154+ [configs, errorTexts, hasBeenFocused]
157+ // we should block on error (with potential reflow) if it
158+ // 1) has been touched
159+ // 2) and has an error
160+ // consumers should use this value to show error text and block on operations
161+ // <string | falsy>[]
162+ const errors = useMemo(
164+ configs.map((config, i) => hasBeenTouched[config.name] && errorTexts[i]),
165+ [configs, errorTexts, hasBeenTouched]
144 // this input has passed if
168 // this input has passed if
145 // 1) it has passed local validation
169 // 1) it has passed local validation
146 // 2) there are no errors
170 // 2) there are no errors
147 const passes = useMemo(
171 const passes = useMemo(
148− () => validations.map((v, i) => v.pass && !errors[i]),
172+ () => validations.map((v, i) => v.pass && !errorTexts[i]),
149− [validations, errors]
173+ [validations, errorTexts]
152 // visibly tell the user that their input has passed if
176 // visibly tell the user that their input has passed if
@@ -176,6 +200,7 @@ export default function useForm(inputConfigs = []) {
176 pass: passes[i],
200 pass: passes[i],
177 visiblyPassed: visiblePasses[i],
201 visiblyPassed: visiblePasses[i],
178 error: errors[i],
202 error: errors[i],
203+ hintError: hintErrors[i],
179 focused: focuses[i],
204 focused: focuses[i],
180 autoFocus: autoFocus && !disabled,
205 autoFocus: autoFocus && !disabled,
181 disabled,
206 disabled,
@@ -198,6 +223,7 @@ export default function useForm(inputConfigs = []) {
198 visiblePasses,
223 visiblePasses,