Skip to content

Commit cbe6c5e

Browse files
committed
Draft in progress
1 parent b3a3128 commit cbe6c5e

File tree

1 file changed

+285
-0
lines changed

1 file changed

+285
-0
lines changed
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
## Thinking of validation as a linear process
2+
3+
We can think of validation as mapping from events to errors.
4+
5+
We have two events: `blur` and `submit`.
6+
7+
When a field is blurred, we validate whether its value is valid or not.
8+
9+
When a whole form is submitted, we validate all of its fields at once.
10+
11+
It would be great to write the same code to validate either a single field (when it is blurred) or a whole bunch of fields (when their form is submitted).
12+
13+
If we were to sketch this using TypeScript:
14+
15+
```ts
16+
function validate(event: Event): Map<string, string> {
17+
// Get the field(s) matching this event.
18+
// Get the values from the fields.
19+
// Validate each value.
20+
// Return a key-value map for each error, with the key identifying the field, and the value holding the error message.
21+
}
22+
```
23+
24+
So how do we do each of these steps?
25+
26+
## Get the field(s) matching this event
27+
28+
Each event that happens from a user interacting with some UI control has that control as part of the event. These can be accessed via the `.current` property on the event.
29+
30+
For a `blur` event on an `<input>`, the `.current` property will refer to the input’s DOM element. This is an instance of [`HTMLInputElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement), which includes convenient properties like reading the current `.value`.
31+
32+
For a `submit` event on a `<form>`, the `.current` property will refer to the form’s DOM element. This is an instance of [`HTMLFormElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement).
33+
34+
Here’s an example form written in React demonstrating reading from the event:
35+
36+
```tsx
37+
<form onSubmit={(event) => {
38+
const form = event.target;
39+
console.log(form instanceof HTMLFormElement); // true
40+
}}>
41+
<label for="f">First name</label>
42+
<input id="f" onBlur={(event) => {
43+
const input = event.target;
44+
console.log(input instanceof HTMLInputElement); // true
45+
}} />
46+
</form>
47+
```
48+
49+
## Get the values from the fields
50+
51+
So we’ve successfully been able to get a DOM element corresponding to the event. Why is this so powerful? Because we can read the current state of the form without having to store that state in React.
52+
53+
That is, instead of adding an `onChange` handler to the form’s inputs and using that to update some React state, we can use these two events `blur` and `submit` as synchronization points to read from the DOM that the user is actively interacting with. Instead of listening to and controlling everything about the form, we let the browser do some of the work.
54+
55+
Here are the two events we care about and the validation work we do for each.
56+
57+
- `blur` event -> HTMLInputElement -> input value -> validate that single value.
58+
- `submit` event -> HTMLFormElement -> all input values -> validate every value.
59+
60+
How we get all input values from a form? There’s a number of approaches, including using the [`.elements` property](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/elements) to get a list of child DOM elements. My favorite approach is to use [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) which has been added to browsers.
61+
62+
If you have a HTMLFormElement, you can quickly read every single value from the form by creating a new `FormData` passing the form:
63+
64+
```tsx
65+
const values = new FormData(form);
66+
values.get("firstField"); // "Some string"
67+
values.get("secondField"); // "Another string value"
68+
69+
// For the given form:
70+
<form>
71+
<label for="f1">First</label>
72+
<input id="f1" name="firstField" value="Some string" />
73+
74+
<label for="f2">Second</label>
75+
<input id="f2" name="secondField" value="Another string value" />
76+
</form>
77+
```
78+
79+
Note the keys like `firstField` are provided by setting the `name` attribute of each `<input>`.
80+
81+
If we wanted to create an empty `FormData` and add values to it, we can also do that:
82+
83+
```ts
84+
const values = FormData();
85+
values.get("firstField"); // null
86+
values.set("firstField", "The value for this field");
87+
values.get("firstField"); // "The value for this field"
88+
```
89+
90+
Let’s see that with our React form:
91+
92+
```tsx
93+
function validate(values) {
94+
// TODO
95+
}
96+
97+
<form onSubmit={(event) => {
98+
const form = event.target;
99+
const values = new FormData(form);
100+
validate(values);
101+
}}>
102+
<label for="f1">First name</label>
103+
<input id="f1" name="firstName" onBlur={(event) => {
104+
const input = event.target;
105+
const values = new FormData();
106+
values.set("firstName", input.value);
107+
validate(values);
108+
}} />
109+
</form>
110+
```
111+
112+
You can see for both events we get the associated DOM element and their relevant values and turn it into a `FormData`. I like this pattern of turning two different types of input into a consistent output, as now the code that follows can think about things in just one way, instead of requiring two branches say with an `if` statement.
113+
114+
Now, you might think “there’s only one field here, so I’m going to have to duplicate the `onBlur` handler for every field”.
115+
116+
Say if we added _last name_ and _email_ fields, our code now looks like this:
117+
118+
```tsx
119+
function validate(values) {
120+
// TODO
121+
}
122+
123+
<form onSubmit={(event) => {
124+
const form = event.target;
125+
const values = new FormData(form);
126+
validate(values);
127+
}}>
128+
<label for="f1">First name</label>
129+
<input id="f1" name="firstName" onBlur={(event) => {
130+
const input = event.target;
131+
const values = new FormData();
132+
values.set("firstName", input.value);
133+
validate(values);
134+
}} />
135+
136+
<label for="f2">Last name</label>
137+
<input id="f2" name="lastName" onBlur={(event) => {
138+
const input = event.target;
139+
const values = new FormData();
140+
values.set("lastName", input.value);
141+
validate(values);
142+
}} />
143+
144+
<label for="f3">Email</label>
145+
<input id="f3" name="email" type="email" onBlur={(event) => {
146+
const input = event.target;
147+
const values = new FormData();
148+
values.set("email", input.value);
149+
validate(values);
150+
}} />
151+
</form>
152+
```
153+
154+
Ugghh that’s a lot of repetition. Wouldn’† it be great it we could just have one `onBlur` handler? Turns out we can:
155+
156+
```tsx
157+
function validate(values) {
158+
// TODO
159+
}
160+
161+
<form
162+
onBlur={(event) => {
163+
const input = event.target;
164+
const values = new FormData();
165+
values.set(input.name, input.value);
166+
validate(values);
167+
}}
168+
onSubmit={(event) => {
169+
const form = event.target;
170+
const values = new FormData(form);
171+
validate(values);
172+
}}
173+
>
174+
<label for="f1">First name</label>
175+
<input id="f1" name="firstName" />
176+
177+
<label for="f2">Last name</label>
178+
<input id="f2" name="lastName" />
179+
180+
<label for="f3">Email</label>
181+
<input id="f3" name="email" type="email" />
182+
</form>
183+
```
184+
185+
This is a feature of JavaScript, not React. Events like `blur` bubble up, so if they aren’t handled by an event listener on the input element itself, then they bubble to its parent and then its parent, right up to the `<body>`.
186+
187+
Since we want to handle all `blur` events within the form in the same way, it makes sense to add the `blur` event handler to the form itself.
188+
189+
Plus we can use the same `name` attribute that `new FormData(form)` uses to identify the field’s value in our `onBlur` handler.
190+
191+
## Validate each value
192+
193+
So given we have a `FormData` object, how can we validate each value?
194+
195+
For now, we’ll just validate that the fields were filled in. If the user didn’t type anything in, or only entered whitespace, we’ll flag it as an error. Otherwise, we’ll say the field is valid and therefore has no error.
196+
197+
We’ll store our errors in a `Map`, which is similar to `FormData` with `.get()` and `.set()` methods, but we can use to store any key-value pairs.
198+
199+
```ts
200+
const errors = new Map();
201+
for (const [name, value] of formDataFromEvent(event)) {
202+
errors.delete(name);
203+
204+
// TODO: add more advanced validation here
205+
if (value.trim() === "") {
206+
errors.set(name, "Required");
207+
}
208+
}
209+
```
210+
211+
## Return a key-value map for each error
212+
213+
## Making it repeatable with a reducer
214+
215+
```tsx
216+
function formDataFromEvent(event: Event) {
217+
if (event.target instanceof HTMLFormElement) {
218+
return new FormData(event.target);
219+
}
220+
221+
const formData = new FormData();
222+
if (event.target instanceof HTMLInputElement) {
223+
formData.set(event.target.name, event.target.value);
224+
}
225+
return formData;
226+
}
227+
228+
function reducer(state, event) {
229+
if (event.type === "submit") {
230+
event.preventDefault();
231+
}
232+
233+
const errors = new Map(state.errors);
234+
for (const [name, value] of formDataFromEvent(event)) {
235+
errors.delete(name);
236+
237+
// TODO: add more advanced validation here
238+
if (value.trim() === "") {
239+
errors.set(name, "Required");
240+
}
241+
}
242+
243+
return { ...state, errors };
244+
}
245+
246+
function Field({ name, label, error, type = "text" }) {
247+
const id = useId();
248+
return (
249+
<div class="flex items-center gap-2">
250+
<label for={id}>{label}</label>
251+
<input id={id} name={name} type={type} />
252+
<span class="italic">{error}</span>
253+
</div>
254+
);
255+
}
256+
257+
export default function App() {
258+
const [state, dispatch] = useReducer(reducer, { errors: new Map<string, string>() });
259+
260+
return (
261+
<form onBlur={dispatch} onSubmit={dispatch} class="flex flex-col items-start gap-4">
262+
<p class="italic">Fields will individually validate on blur, or every field will validate on submit.</p>
263+
<fieldset class="flex flex-col gap-2">
264+
<Field
265+
name="firstName"
266+
label="First name"
267+
error={state.errors.get("firstName")}
268+
/>
269+
<Field
270+
name="lastName"
271+
label="Last name"
272+
error={state.errors.get("lastName")}
273+
/>
274+
<Field
275+
name="email"
276+
label="Email"
277+
type="email"
278+
error={state.errors.get("email")}
279+
/>
280+
</fieldset>
281+
<button class="px-3 py-1 bg-blue-300 rounded">Save</button>
282+
</form>
283+
);
284+
}
285+
```

0 commit comments

Comments
 (0)