on f130223 help sign in
176 lines 4.9 kB history raw
1import React, { useCallback, useEffect, useMemo, useState } from 'react';
2import cn from 'classnames';
4import Grid from 'components/Grid';
6import { compose } from 'lib/lib';
7import { kDefaultValidator } from 'lib/validators';
9function useInput({
10 validators = [],
11 transformers = [],
12 initialValue = '',
13 autofocus = false,
14}) {
15 const [value, setValue] = useState(initialValue);
16 const [focused, setFocused] = useState(autofocus);
17 const [hasBeenFocused, setHasBeenFocused] = useState(false);
19 // NB(shrugs)
20 // TL;DR: This is what we want, the exhaustive deps rule is being too nosy.
21 //
22 // Here we memo-construct the `validate` and `transform` functions
23 // based on the prop arguments. We explicitly disable the exhausive deps rule
24 // because eslint wants an inline array specifying our dependencies.
25 //
26 // If we were to place the array of validators or transformers into
27 // the dependency array like `[validators]`, we would end up creating a new
28 // `validate` function every render, since deps are shallow-compared with
29 // `Object.is`. Because the array of validators are constructed as part of
30 // a component's render() function like:
31 //
32 // export function MnemonicInput(props) {
33 // return (
34 // <Input
35 // type="text"
36 // autoComplete="off"
37 // placeholder={`e.g. ${kExampleMnemonic}`}
38 // validators={[validateMnemonic, validateNotEmpty]}
39 // {...props}
40 // />
41 // );
42 // }
43 //
44 // we create a new array, which shallowly compares as not-equal
45 // between renders.
46 //
47 // So, in this specific case, we disable the rule and pass the
48 // array of validators (and transformers) themselves as dependencies.
49 // This means that validators and transformers can change between renders
50 // and we will re-create the composed `validate` and `transform` functions
51 // as expected.
52 //
53 // For context, an alternative solution I tried was defining the array of
54 // validators as a constant array outside of the render method, but:
55 // 1. It wasn't as clean or useful as specifying them inline,
56 // 2. It would mean that validators could not be updated between renders,
57 // 3. And the situation we have now feels naturally more 'correct'.
58 //
59 /* eslint-disable react-hooks/exhaustive-deps */
60 const transform = useMemo(() => compose(...transformers), transformers);
61 const validate = useMemo(
62 () =>
63 compose(
64 ...validators,
65 kDefaultValidator
66 ),
67 validators
68 );
69 /* eslint-enable react-hooks/exhaustive-deps */
71 const onChange = useCallback(
72 e => setValue(transform(e.target.value)), //
73 [transform]
74 );
75 const onFocus = useCallback(
76 e => setFocused(true), //
77 [setFocused]
78 );
79 const onBlur = useCallback(
80 e => {
81 setFocused(false);
82 setHasBeenFocused(true);
83 },
84 [setFocused, setHasBeenFocused]
85 );
87 // memo-compute whether or not the current value is valid
88 const { pass, error: _error, data } = useMemo(
89 () => validate(value), //
90 [validate, value]
91 );
93 // only set error if it should be visible
94 const error = !pass && hasBeenFocused && !focused && _error;
95 // whether or not the 'pass' effect should be shown
96 const visiblePass = pass && hasBeenFocused && !focused;
98 return {
99 pass,
100 // TODO: need a better name for `visiblePass`
101 visiblePass,
102 error,
103 data,
104 focused,
105 bind: {
106 value,
107 onChange,
108 focused: focused ? 'true' : 'false',
109 // ^ dom wants string value for focused
110 onFocus,
111 onBlur,
112 },
113 };
116export default function Input({
117 name,
118 label,
119 initialValue = '',
120 className = '',
121 validators = [],
122 onSuccess,
123 onFailure,
124 disabled = false,
125 mono = false,
126 autofocus = false,
127 ...rest
128}) {
129 const { focused, pass, visiblePass, error, data, bind } = useInput({
130 validators,
131 initialValue,
132 autofocus,
133 });
135 useEffect(() => {
136 onSuccess && onSuccess(data);
137 onFailure && onFailure(error);
138 }, [pass, error, data, onSuccess, onFailure]);
140 return (
141 <Grid>
142 <Grid.Item full>
143 <label className="f6 lh-tall" htmlFor={name}>
144 {label}
145 </label>
146 </Grid.Item>
147 <Grid.Item full>
148 <input
149 {...rest}
150 className={cn(
151 'b b1 p3',
152 { mono },
153 {
154 'bg-white': !disabled,
155 'bg-gray1': disabled,
156 },
157 {
158 gray4: !focused,
159 black: focused,
160 },
161 {
162 'b-black': focused,
163 'b-gray3': !focused && !error && !visiblePass,
164 'b-yellow3': !focused && error && !visiblePass,
165 'b-green3': !focused && !error && visiblePass,
166 },
167 className
168 )}
169 id={name}
170 name={name}
171 {...bind}
172 />
173 </Grid.Item>
174 </Grid>
175 );