Skip to content

Commit 8e9bbbc

Browse files
committed
feat: Support Default Error Messages for Observed Forms
This enables developer to remove redundant/repetitive code from their applications. It also empower developers to have a central, more ergonomic way to use validation tools like Zod.
1 parent 6999255 commit 8e9bbbc

19 files changed

+316
-39
lines changed

TODO.md

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Documentation
44

5+
- [ ] Add more detailed examples of how to use `Zod` with the `defaultErrors.validate` option.
56
- [ ] Figure out a Logo for the `enthusiastic-js` Organization and maybe the Form Observer package?
67
- [ ] In the interest of time, we're probably going to have to do the bare minimum when it comes to the documentation. Make the API clear, give some helpful examples, etc. After we've release the first draft of the project, we can start thinking about how to "perfect" the docs. But for now, don't get too paranoid about the wording.
78
- [ ] Adding demos somewhere in this repo (or in something like a CodeSandbox) would likely be helpful for developers. **Edit**: We now have examples for the `FormValidityObserver`. Would examples for the `FormObserver` or the `FormStorageObserver` also be helpful?

docs/form-validity-observer/README.md

+15-4
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,17 @@ The `FormValidityObserver()` constructor creates a new observer and configures i
116116
The <code>renderer</code> defaults to a function that accepts error messages of type <code>string</code> and renders them to the DOM as raw HTML.
117117
</p>
118118
</dd>
119+
<dt id="form-validity-observer-options-default-errors"><code>defaultErrors: ValidationErrors&lt;M, E&gt;</code></dt>
120+
<dd>
121+
<p>
122+
Configures the default error messages to display for the validation constraints. (See the <a href="#method-formvalidityobserverconfigureename-string-errormessages-validationerrorsm-e-void"><code>configure</code></a> method for more details about error message configuration, and refer to the <a href="./types.md#validationerrorsm-e"><code>ValidationErrors</code></a> type for more details about validation constraints.)
123+
</p>
124+
<p>
125+
<blockquote>
126+
<strong>Note: The <code>defaultErrors.validate</code> option will provide a default custom validation function for <em>all</em> fields in your form.</strong> This is primarily useful if you have a reusable validation function that you want to apply to all of your form's fields (for example, if you are using <a href="https://zod.dev">Zod</a>). See <a href="./guides.md#getting-the-most-out-of-the-defaulterrors-option"><i>Getting the Most out of the <code>defaultErrors</code></i> Option</a> for examples on how to use this option effectively.
127+
</blockquote>
128+
</p>
129+
</dd>
119130
</dl>
120131
</dd>
121132
</dl>
@@ -204,11 +215,11 @@ form1.elements[0].dispatchEvent(new FocusEvent("focusout")); // Does nothing
204215

205216
### Method: `FormValidityObserver.configure<E>(name: string, errorMessages: `[`ValidationErrors<M, E>`](./types.md#validationerrorsm-e)`): void`
206217

207-
Configures the error messages that will be displayed for a form field's validation constraints. If an error message is not configured for a validation constraint, then the field's [`validationMessage`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectElement/validationMessage) will be used instead. For [native form fields](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/elements), the browser automatically supplies a default `validationMessage` depending on the broken constraint.
218+
Configures the error messages that will be displayed for a form field's validation constraints. If an error message is not configured for a validation constraint and there is no corresponding [default configuration](#form-validity-observer-options-default-errors), then the field's [`validationMessage`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectElement/validationMessage) will be used instead. For [native form fields](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/elements), the browser automatically supplies a default `validationMessage` depending on the broken constraint.
208219

209-
> Note: If the field is _only_ using the browser's default error messages, it does _not_ need to be `configure`d.
220+
> Note: If the field is _only_ using the configured [`defaultErrors`](#form-validity-observer-options-default-errors) and/or the browser's default error messages, it _does not_ need to be `configure`d.
210221
211-
The Form Element Type, `E`, represents the form field being configured. This type is inferred from the `errorMessages` configuration and defaults to a general [`ValidatableField`](./types.md#validatablefield).
222+
The Field Element Type, `E`, represents the form field being configured. This type is inferred from the `errorMessages` configuration and defaults to a general [`ValidatableField`](./types.md#validatablefield).
212223

213224
#### Parameters
214225

@@ -297,7 +308,7 @@ When the `focus` option is `false`, you can consider `validateField()` to be an
297308

298309
Marks the form field having the specified `name` as invalid (via the [`[aria-invalid="true"]`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-invalid) attribute) and applies the provided error `message` to it. Typically, you shouldn't need to call this method manually; but in rare situations it might be helpful.
299310

300-
The Form Element Type, `E`, represents the invalid form field. This type is inferred from the error `message` if it is a function. Otherwise, `E` defaults to a general [`ValidatableField`](./types.md#validatablefield).
311+
The Field Element Type, `E`, represents the invalid form field. This type is inferred from the error `message` if it is a function. Otherwise, `E` defaults to a general [`ValidatableField`](./types.md#validatablefield).
301312

302313
#### Parameters
303314

docs/form-validity-observer/guides.md

+145
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Here you'll find helpful tips on how to use the `FormValidityObserver` effective
44

55
- [Enabling/Disabling Accessible Error Messages](#enabling-accessible-error-messages-during-form-submissions)
66
- [Keeping Track of Visited/Dirty Fields](#keeping-track-of-visiteddirty-fields)
7+
- [Getting the Most out of the `defaultErrors` option](#getting-the-most-out-of-the-defaulterrors-option)
78
- [Keeping Track of Form Data](#keeping-track-of-form-data)
89
- [Recommendations for Conditionally Rendered Fields](#recommendations-for-conditionally-rendered-fields)
910
- [Recommendations for Styling Form Fields and Their Error Messages](#recommendations-for-styling-form-fields-and-their-error-messages)
@@ -129,6 +130,150 @@ To get an idea of how these event listeners would function, you can play around
129130

130131
You can learn more about what can be done with forms using pure JS on our [Philosophy](../extras/philosophy.md#avoid-unnecessary-overhead-and-reinventing-the-wheel) page.
131132

133+
## Getting the Most out of the `defaultErrors` Option
134+
135+
Typically, we want the error messages in our application to be consistent. Unfortunately, this can sometimes cause us to write the same error messages over and over again. For example, consider a message that might be displayed for the `required` constraint:
136+
137+
```html
138+
<form>
139+
<label for="first-name">First Name</label>
140+
<input id="first-name" type="text" required aria-describedby="first-name-error" />
141+
<div id="first-name-error" role="alert"></div>
142+
143+
<label for="last-name">Last Name</label>
144+
<input id="last-name" type="text" required aria-describedby="last-name-error" />
145+
<div id="last-name-error" role="alert"></div>
146+
147+
<label for="email">Email</label>
148+
<input id="email" type="email" required aria-describedby="email-error" />
149+
<div id="email-error" role="alert"></div>
150+
151+
<!-- Other Fields ... -->
152+
</div>
153+
```
154+
155+
We might configure our error messages like so
156+
157+
```js
158+
const observer = new FormValidityObserver("focusout");
159+
observer.configure("first-name", { required: "First Name is required." });
160+
observer.configure("last-name", { required: "Last Name is required." });
161+
observer.configure("email", { required: "Email is required." });
162+
// Configurations for other fields ...
163+
```
164+
165+
But this is redundant (and consequently, error-prone). Since all of our error messages for the `required` constraint follow the same format (`"<FIELD_NAME> is required"`), it would be better for us to use the [`defaultErrors`](./README.md#form-validity-observer-options-default-errors) configuration option instead.
166+
167+
```js
168+
const observer = new FormValidityObserver("focusout", {
169+
defaultErrors: {
170+
required: (field) => `${field.labels?.[0].textContent ?? "This field"} is required.`;
171+
},
172+
});
173+
```
174+
175+
This gives us one consistent way to define the `required` error message for _all_ of our fields. Of course, it's possible that not all of your form controls will be labeled by a `<label>` element. For instance, a `radiogroup` is typically labeled by a `<legend>` instead. In this case, you may choose to make the error message more generic
176+
177+
```js
178+
const observer = new FormValidityObserver("focusout", {
179+
defaultErrors: { required: "This field is required" },
180+
});
181+
```
182+
183+
Or you may choose to make the error message more flexible
184+
185+
```js
186+
const observer = new FormValidityObserver("focusout", {
187+
defaultErrors: {
188+
required(field) {
189+
if (field instanceof HTMLInputElement && field.type === "radio") {
190+
const radiogroup = input.closest("fieldset[role='radiogroup']");
191+
const legend = radiogroup.firstElementChild.matches("legend") ? radiogroup.firstElementChild : null;
192+
return `${legend?.textContent ?? "This field"} is required.`;
193+
}
194+
195+
return `${field.labels?.[0].textContent ?? "This field"} is required.`;
196+
},
197+
},
198+
});
199+
```
200+
201+
And if you ever need a _unique_ error message for a specific field, you can still configure it explicitly.
202+
203+
```js
204+
const observer = new FormValidityObserver("focusout", {
205+
defaultErrors: { required: "This field is required" },
206+
});
207+
208+
observer.configure("my-unique-field", { required: "This field has a unique `required` error!" });
209+
```
210+
211+
### Default Validation Functions
212+
213+
The `validate` option in the `defaultErrors` object provides a default custom validation function for _all_ of the fields in your form. This can be helpful if you have a reusable validation function that you want to apply to all of your form's fields. For example, if you're using [`Zod`](https://zod.dev) to validate your form data, you could do something like this:
214+
215+
```js
216+
const schema = z.object({
217+
"first-name": z.string(),
218+
"last-name": z.string(),
219+
email: z.string().email(),
220+
});
221+
222+
const observer = new FormValidityObserver("focusout", {
223+
defaultErrors: {
224+
validate(field) {
225+
const result = schema.shape[field.name].safeParse(field.value);
226+
// Extract field's error message from `result`
227+
return errorMessage;
228+
},
229+
},
230+
});
231+
```
232+
233+
By leveraging `defaultErrors.validate`, you can easily use Zod (or any other validation tool) on your frontend. If you're using an SSR framework, you can use the exact same tool on your backend. It's the best of both worlds!
234+
235+
### Zod Validation with Nested Fields
236+
237+
For more complex form structures (e.g., "Nested Fields" as objects or arrays), you will need to write some advanced logic to make sure that you access the correct `safeParse` function. For example, if you have a field named `address.name.first`, then you'll need to recursively follow the path from `address` to `first` to access the correct `safeParse` function. The [`shape`](https://zod.dev/?id=shape) property (for objects) and the [`element`](https://zod.dev/?id=element) property (for arrays) in Zod will help you accomplish this. Alternatively, you can flatten your object structure entirely:
238+
239+
```js
240+
const schema = z.object({
241+
"address.name.first": z.string(),
242+
"address.name.last": z.string(),
243+
"address.city": z.string(),
244+
// Other fields...
245+
});
246+
```
247+
248+
This enables you to use the approach that we showed above without having to write any recursive logic. It's arguably more performant than defining and walking through nested objects, but it requires you to be doubly sure that you're spelling all of your fields' names correctly. Also note that the logic for handling arrays in this example would still take a little effort and may require some recursion. However, this logic shouldn't be too difficult to write.
249+
250+
If there's sufficient interest from the community, then we may add some Zod helper functions to our packages to take this burden off of developers.
251+
252+
### Zod Validation Using Existing Libraries
253+
254+
Another option is to use an existing library that validates forms with Zod (e.g., `@conform-to/zod`) and to extract the error messages from that tool. For example, you might do something like the following:
255+
256+
```js
257+
import { FormValidityObserver } from "@form-observer";
258+
import { parseWithZod } from "@conform-to/zod";
259+
import { z } from "zod";
260+
261+
const schema = z.object({
262+
email: z.string().email(),
263+
password: z.string(),
264+
});
265+
266+
const observer = new FormValidityObserver("focusout", {
267+
defaultErrors: {
268+
validate(field) {
269+
const results = parseWithZod(new FormData(field.form), schema);
270+
// Grab the correct error message from `results` object by using `field.name`.
271+
return errorMessage;
272+
},
273+
},
274+
});
275+
```
276+
132277
## Keeping Track of Form Data
133278
134279
Many form libraries offer stateful solutions for managing the data in your forms as JSON. But there are a few disadvantages to this approach:

docs/form-validity-observer/integrations/preact.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ Remember that `autoObserve` is simply a convenience utility for calling `observe
8181

8282
An enhanced version of [`FormValidityObserver.configure`](../README.md#method-formvalidityobserverconfigureename-string-errormessages-validationerrorsm-e-void) for `Preact`. In addition to configuring a field's error messages, it generates the props that should be applied to the field based on the provided arguments.
8383

84-
> Note: If the field is _only_ using the browser's default error messages, it does _not_ need to be `configure`d.
84+
> Note: If the field is _only_ using the configured [`defaultErrors`](../README.md#form-validity-observer-options-default-errors) and/or the browser's default error messages, it _does not_ need to be `configure`d.
8585
8686
The `PreactValidationErrors<M, E>` type is an enhanced version of the core [`ValidationErrors<M, E>`](../types.md#validationerrorsm-e) type. Here is how `PreactValidationErrors` compares to `ValidationErrors`.
8787

docs/form-validity-observer/integrations/react.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ Remember that `autoObserve` is simply a convenience utility for calling `observe
9191

9292
An enhanced version of [`FormValidityObserver.configure`](../README.md#method-formvalidityobserverconfigureename-string-errormessages-validationerrorsm-e-void) for `React`. In addition to configuring a field's error messages, it generates the props that should be applied to the field based on the provided arguments.
9393

94-
> Note: If the field is _only_ using the browser's default error messages, it does _not_ need to be `configure`d.
94+
> Note: If the field is _only_ using the configured [`defaultErrors`](../README.md#form-validity-observer-options-default-errors) and/or the browser's default error messages, it _does not_ need to be `configure`d.
9595
9696
The `ReactValidationErrors<M, E>` type is an enhanced version of the core [`ValidationErrors<M, E>`](../types.md#validationerrorsm-e) type. Here is how `ReactValidationErrors` compares to `ValidationErrors`.
9797

docs/form-validity-observer/integrations/solid.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ Remember that `autoObserve` is simply a convenience utility for calling `observe
4949

5050
An enhanced version of [`FormValidityObserver.configure`](../README.md#method-formvalidityobserverconfigureename-string-errormessages-validationerrorsm-e-void) for `Solid`. In addition to configuring a field's error messages, it generates the props that should be applied to the field based on the provided arguments.
5151

52-
> Note: If the field is _only_ using the browser's default error messages, it does _not_ need to be `configure`d.
52+
> Note: If the field is _only_ using the configured [`defaultErrors`](../README.md#form-validity-observer-options-default-errors) and/or the browser's default error messages, it _does not_ need to be `configure`d.
5353
5454
The `SolidValidationErrors<M, E>` type is an enhanced version of the core [`ValidationErrors<M, E>`](../types.md#validationerrorsm-e) type. Here is how `SolidValidationErrors` compares to `ValidationErrors`.
5555

docs/form-validity-observer/integrations/svelte.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ Remember that `autoObserve` is simply a convenience utility for calling `observe
4949

5050
An enhanced version of [`FormValidityObserver.configure`](../README.md#method-formvalidityobserverconfigureename-string-errormessages-validationerrorsm-e-void) for `Svelte`. In addition to configuring a field's error messages, it generates the props that should be applied to the field based on the provided arguments.
5151

52-
> Note: If the field is _only_ using the browser's default error messages, it does _not_ need to be `configure`d.
52+
> Note: If the field is _only_ using the configured [`defaultErrors`](../README.md#form-validity-observer-options-default-errors) and/or the browser's default error messages, it _does not_ need to be `configure`d.
5353
5454
The `SvelteValidationErrors<M, E>` type is an enhanced version of the core [`ValidationErrors<M, E>`](../types.md#validationerrorsm-e) type. Here is how `SvelteValidationErrors` compares to `ValidationErrors`.
5555

docs/form-validity-observer/integrations/vue.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ Remember that `autoObserve` is simply a convenience utility for calling `observe
5151

5252
An enhanced version of [`FormValidityObserver.configure`](../README.md#method-formvalidityobserverconfigureename-string-errormessages-validationerrorsm-e-void) for `Vue`. In addition to configuring a field's error messages, it generates the props that should be applied to the field based on the provided arguments.
5353

54-
> Note: If the field is _only_ using the browser's default error messages, it does _not_ need to be `configure`d.
54+
> Note: If the field is _only_ using the configured [`defaultErrors`](../README.md#form-validity-observer-options-default-errors) and/or the browser's default error messages, it _does not_ need to be `configure`d.
5555
5656
The `VueValidationErrors<M, E>` type is an enhanced version of the core [`ValidationErrors<M, E>`](../types.md#validationerrorsm-e) type. Here is how `VueValidationErrors` compares to `ValidationErrors`.
5757

docs/form-validity-observer/types.md

+1
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ Remember that each instance of the `FormValidityObserver` determines its `M` typ
121121
### Primary Uses
122122

123123
- [`FormValidityObserver.configure`](./README.md#method-formvalidityobserverconfigureename-string-errormessages-validationerrorsm-e-void)
124+
- [`FormValidityObserverOptions.defaultErrors`](./README.md#form-validity-observer-options-default-errors)
124125

125126
## `ErrorDetails<M, E>`
126127

0 commit comments

Comments
 (0)