Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Rendering Error Messages to the DOM by Default #6

Merged
merged 11 commits into from
Apr 21, 2024
Merged
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## 2023-04-21

### v0.8.0 (All Packages)

#### Features

- Support rendering error messages to the DOM by default. (This opens the door for using JS framework state to render error messages to the DOM. It also opens the door for safely using a framework's `render` function to render error messages to the DOM. See [this PR](https://github.com/enthusiastic-js/form-observer/pull/6) for additional details.)

#### Bug Fixes

- Fixed a bug in `@form-observer/react`'s `useFormValidityObserver` function where updates to the `defaultErrors` option would not cause the hook to update its memoized value.

## 2023-02-11

### v0.7.2 (Solid, Lit, Preact)
Expand Down
4 changes: 0 additions & 4 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
# TODO

## Priority

- [ ] Support some kind of `renderByDefault` option

## Documentation

- [ ] Add more detailed examples of how to use `Zod` with the `defaultErrors.validate` option.
Expand Down
2 changes: 1 addition & 1 deletion docs/extras/philosophy.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ The next natural step is to provide a way to validate an _entire form_. For that

If the developer opts out of accessible error messaging, the `setFieldError` and `clearFieldError` methods will fallback to `field.setCustomValidity()`, and `validateField({ focus: true })`/`validateFields({ focus: true })` will fallback to `field.reportValidity()`/`form.reportValidity()`.

As an added bonus, the `FormValidityObserver` exposes a [`configure`](../form-validity-observer/README.md#method-formvalidityobserverconfigureename-string-errormessages-validationerrorsm-e-void) method that enables developers to configure the error messages that should be displayed when a field fails validation. (Any unconfigured error messages will fallback to the `validationMessage` that the browser provides.) It also allows a custom validation function to be configured for the field.
As an added bonus, the `FormValidityObserver` exposes a [`configure`](../form-validity-observer/README.md#method-formvalidityobserverconfigureename-string-errormessages-validationerrorsm-e-r-void) method that enables developers to configure the error messages that should be displayed when a field fails validation. (Any unconfigured error messages will fallback to the `validationMessage` that the browser provides.) It also allows a custom validation function to be configured for the field.

Seeing the big picture here? The `FormValidityObserver` is basically a wrapper for the browser's native features when accessible error messages aren't being used. When accessible error messages are needed, it functions as an _enhancement_ (not a replacement) of the browser's features to satisfy that need. As a bonus, it includes configurable scrolling/rendering functionality as well.

Expand Down
40 changes: 26 additions & 14 deletions docs/form-validity-observer/README.md

Large diffs are not rendered by default.

85 changes: 84 additions & 1 deletion docs/form-validity-observer/guides.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Here you'll find helpful tips on how to use the `FormValidityObserver` effective
- [Enabling/Disabling Accessible Error Messages](#enabling-accessible-error-messages-during-form-submissions)
- [Keeping Track of Visited/Dirty Fields](#keeping-track-of-visiteddirty-fields)
- [Getting the Most out of the `defaultErrors` option](#getting-the-most-out-of-the-defaulterrors-option)
- [Managing Form Errors with State](#managing-form-errors-with-state)
- [Keeping Track of Form Data](#keeping-track-of-form-data)
- [Recommendations for Conditionally Rendered Fields](#recommendations-for-conditionally-rendered-fields)
- [Recommendations for Styling Form Fields and Their Error Messages](#recommendations-for-styling-form-fields-and-their-error-messages)
Expand All @@ -13,7 +14,8 @@ Here you'll find helpful tips on how to use the `FormValidityObserver` effective
<!--
TODO: Some `Guides` that could be helpful:

1) MAYBE something on how to work with accessible error messages? (Should we also mention `aria-errormessage` vs. `aria-describedby` too? As well as the lack of support for `aria-errormessage`? Or does that belong somewhere else in the docs?)
1) Reconciling error messages sent by the server on the client side.
2) MAYBE something on how to work with accessible error messages? (Should we also mention `aria-errormessage` vs. `aria-describedby` too? As well as the lack of support for `aria-errormessage`? Or does that belong somewhere else in the docs?)
-->

## Enabling Accessible Error Messages during Form Submissions
Expand Down Expand Up @@ -274,6 +276,87 @@ const observer = new FormValidityObserver("focusout", {
});
```

## Managing Form Errors with State

Typically, the `FormValidityObserver` renders error messages directly to the DOM when an [accessible error container](https://www.w3.org/WAI/WCAG21/Techniques/aria/ARIA21#example-2-identifying-errors-in-data-format) is present. But if you prefer to render your error messages in a different way (or to a different location), then you can leverage the observer's [`renderer`](./README.md#form-validity-observer-options-renderer) option to do so.

For example, you might want to rely on React State (or another JS framework's state) to display error messages to your users. In that case, you can pass a `renderer` function to the `FormValidityObserver` that updates your local error state instead of updating the DOM. From there, you can let your framework do the work of updating the UI.

> Note: We generally recommend keeping your forms stateless when possible, but sometimes your use case might require you to use state.

<details open>
<summary><strong>Svelte Example</strong></summary>

```svelte
<form use:autoObserve>
<label for="username">Username</label>
<input id="username" name="username" type="text" required aria-describedby="username-error" />
<div id="username-error" role="alert">{errors["username-error"] ?? ""}</div>

<label for="email">Email</label>
<input id="email" name="email" type="email" required aria-describedby="email-error" />
<div id="email-error" role="alert">{errors["email-error"] ?? ""}</div>
</form>

<script lang="ts">
import { createFormValidityObserver } from "@form-observer/svelte";

let errors: Record<string, string | null> = {};
const { autoObserve } = createFormValidityObserver("input", {
renderByDefault: true,
renderer(errorContainer, errorMessage: (typeof errors)[string]) {
errors[errorContainer.id] = errorMessage;
},
});
</script>
```

</details>

<details open>
<summary><strong>React Example</strong></summary>

```tsx
import { useState, useMemo } from "react";
import { createFormValidityObserver } from "@form-observer/react";

export default function MyForm() {
const [errors, setErrors] = useState<Record<string, string | null>>({});
const { autoObserve } = useMemo(() => {
return createFormValidityObserver("input", {
renderByDefault: true,
renderer(errorContainer, errorMessage: (typeof errors)[string]) {
setErrors((e) => ({ ...e, [errorContainer.id]: errorMessage }));
},
});
}, []);

return (
<form ref={useMemo(autoObserve, [autoObserve])}>
<label htmlFor="username">Username</label>
<input id="username" name="username" type="text" required aria-describedby="username-error" />
<div id="username-error" role="alert">
{errors["username-error"]}
</div>

<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" required aria-describedby="email-error" />
<div id="email-error" role="alert">
{errors["email-error"]}
</div>
</form>
);
}
```

</details>

With this approach, our error messages are "rendered" to our stateful `errors` object instead of being rendered to the DOM directly. Then, we let the JavaScript framework take responsibility for displaying any error messages that are present.

Notice that we also supplied the [`renderByDefault: true`](./README.md#form-validity-observer-options-render-by-default) option to the `FormValidityObserver`. This option is important because it causes all error messages that are pure strings to be sent through our `renderer` function by default -- including the browser's default error messages. In other words, this option guarantees that all error messages which are generated for our form fields will be properly assigned to our stateful error object.

You can find more detailed examples of using stateful error objects on [StackBlitz](https://stackblitz.com/@ITenthusiasm/collections/form-observer-examples).

## Keeping Track of Form Data

Many form libraries offer stateful solutions for managing the data in your forms as JSON. But there are a few disadvantages to this approach:
Expand Down
Loading