Skip to content

Commit c2ca263

Browse files
committed
feat: Support Form Field Revalidation
This feature will be especially helpful for developers who only want to validate their fields `oninput` _after_ their form has already been submitted.
1 parent fe3d39a commit c2ca263

File tree

4 files changed

+272
-25
lines changed

4 files changed

+272
-25
lines changed

docs/form-validity-observer/README.md

+25
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,15 @@ The `FormValidityObserver()` constructor creates a new observer and configures i
109109
<dd>
110110
The function used to scroll a field (or radiogroup) that has failed validation into view. Defaults to a function that calls <a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView"><code>scrollIntoView()</code></a> on the field (or radiogroup) that failed validation.
111111
</dd>
112+
<dt id="form-validity-observer-options-revalidate-on"><code>revalidateOn: EventType</code></dt>
113+
<dd>
114+
<p>
115+
The type of event that will cause a form field to be revalidated. (Revalidation for a form field is enabled after it is validated at least once -- whether manually or automatically.)
116+
</p>
117+
<p>
118+
This can be helpful, for example, if you want to validate your fields <code>oninput</code>, but only after the user has visited them. In that case, you could write <code>new FormValidityObserver("focusout", { revalidateOn: "input" })</code>. Similarly, you might only want to validate your fields <code>oninput</code> after your form has been submitted. In that case, you could write <code>new FormValidityObserver(null, { revalidateOn: "input" })</code>.
119+
</p>
120+
</dd>
112121
<dt id="form-validity-observer-options-renderer"><code>renderer: (errorContainer: HTMLElement, errorMessage: M | null) => void</code></dt>
113122
<dd>
114123
<p>
@@ -292,10 +301,18 @@ Validates all of the observed form's fields, returning `true` if _all_ of the va
292301
<dd>
293302
<p>Indicates that the <em>first</em> field in the DOM that fails validation should be focused. Defaults to <code>false</code>.</p>
294303
</dd>
304+
<dt><code>enableRevalidation</code></dt>
305+
<dd>
306+
<p>
307+
Enables revalidation for <strong>all</strong> of the form's fields. Defaults to <code>true</code>. (This option is only relevant if a value was provided for the observer's <a href="#form-validity-observer-options-revalidate-on"><code>revalidateOn</code></a> option.)
308+
</p>
309+
</dd>
295310
</dl>
296311

297312
When the `focus` option is `false`, you can consider `validateFields()` to be an enhanced version of `form.checkValidity()`. When the `focus` option is `true`, you can consider `validateFields()` to be an enhanced version of [`form.reportValidity()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/reportValidity).
298313

314+
Note that the `enableRevalidation` option can prevent field revalidation from being turned on, but it cannot be used to _turn off_ revalidation.
315+
299316
### Method: `FormValidityObserver.validateField(name: string, options?: ValidateFieldOptions): boolean | Promise<boolean>`
300317

301318
Validates the form field with the specified `name`, returning `true` if the field passes validation and `false` otherwise. The `boolean` that `validateField()` returns will be wrapped in a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) if the field's [`validate` constraint](./types.md#validationerrorsm-e-r) runs asynchronously. This promise will `resolve` after the asynchronous validation function `resolves`. Unlike the [`validateFields()`](#method-formvalidityobservervalidatefieldsoptions-validatefieldsoptions-boolean--promiseboolean) method, this promise will also `reject` if the asynchronous validation function `rejects`.
@@ -314,12 +331,20 @@ Validates the form field with the specified `name`, returning `true` if the fiel
314331
<dl>
315332
<dt><code>focus</code></dt>
316333
<dd>Indicates that the field should be focused if it fails validation. Defaults to <code>false</code>.</dd>
334+
<dt><code>enableRevalidation</code></dt>
335+
<dd>
336+
<p>
337+
Enables revalidation for the validated field. Defaults to <code>true</code>. (This option is only relevant if a value was provided for the observer's <a href="#form-validity-observer-options-revalidate-on"><code>revalidateOn</code></a> option.)
338+
</p>
339+
</dd>
317340
</dl>
318341
</dd>
319342
</dl>
320343

321344
When the `focus` option is `false`, you can consider `validateField()` to be an enhanced version of [`field.checkValidity()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/checkValidity). When the `focus` option is `true`, you can consider `validateField()` to be an enhanced version of [`field.reportValidity()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/reportValidity).
322345

346+
Note that the `enableRevalidation` option can prevent field revalidation from being turned on, but it cannot be used to _turn off_ revalidation.
347+
323348
### Method: `FormValidityObserver.setFieldError<E>(name: string, message: `[`ErrorMessage<string, E>`](./types.md#errormessagem-e)`|`[`ErrorMessage<M, E>`](./types.md#errormessagem-e)`, render?: boolean): void`
324349

325350
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.

packages/core/FormValidityObserver.d.ts

+16
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ export interface FormValidityObserverOptions<
5757
*/
5858
scroller?(fieldOrRadiogroup: ValidatableField): void;
5959

60+
/**
61+
* The type of event that will cause a form field to be revalidated. (Revalidation for a form field
62+
* is enabled after it is validated at least once -- whether manually or automatically).
63+
*/
64+
revalidateOn?: EventType;
65+
6066
/**
6167
* The function used to render error messages to the DOM when a validation constraint's `render` option is `true`.
6268
* (It will be called with `null` when a field passes validation.) Defaults to a function that accepts a string
@@ -83,11 +89,21 @@ export interface FormValidityObserverOptions<
8389
export interface ValidateFieldOptions {
8490
/** Indicates that the field should be focused if it fails validation. Defaults to `false`. */
8591
focus?: boolean;
92+
/**
93+
* Enables revalidation for the validated field. Defaults to `true`.
94+
* (This option is only relevant if a value was provided for the observer's `revalidateOn` constructor option.)
95+
*/
96+
enableRevalidation?: boolean;
8697
}
8798

8899
export interface ValidateFieldsOptions {
89100
/** Indicates that the _first_ field in the DOM that fails validation should be focused. Defaults to `false`. */
90101
focus?: boolean;
102+
/**
103+
* Enables revalidation for **all** of the form's fields. Defaults to `true`.
104+
* (This option is only relevant if a value was provided for the observer's `revalidateOn` constructor option.)
105+
*/
106+
enableRevalidation?: boolean;
91107
}
92108

93109
interface FormValidityObserverConstructor {

packages/core/FormValidityObserver.js

+28-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import FormObserver from "./FormObserver.js";
22

33
const radiogroupSelector = "fieldset[role='radiogroup']";
4-
const attrs = Object.freeze({ "aria-describedby": "aria-describedby", "aria-invalid": "aria-invalid" });
4+
const attrs = Object.freeze({
5+
"aria-describedby": "aria-describedby",
6+
"aria-invalid": "aria-invalid",
7+
"data-fvo-revalidate": "data-fvo-revalidate",
8+
});
59

610
// NOTE: Generic `T` = Event TYPE. Generic `M` = Error MESSAGE. Generic `E` = ELEMENT. Generic `R` = RENDER by default.
711

@@ -68,6 +72,10 @@ const attrs = Object.freeze({ "aria-describedby": "aria-describedby", "aria-inva
6872
* scroll a `field` (or `radiogroup`) that has failed validation into view. Defaults to a function that calls
6973
* `fieldOrRadiogroup.scrollIntoView()`.
7074
*
75+
* @property {import("./types.d.ts").EventType} [revalidateOn] The type of event that will cause a form field to be
76+
* revalidated. (Revalidation for a form field is enabled after it is validated at least once -- whether manually or
77+
* automatically).
78+
*
7179
* @property {(errorContainer: HTMLElement, errorMessage: M | null) => void} [renderer] The function used to render
7280
* error messages to the DOM when a validation constraint's `render` option is `true`. (It will be called with `null`
7381
* when a field passes validation.) Defaults to a function that accepts a string and renders it to the DOM as raw HTML.
@@ -85,12 +93,20 @@ const attrs = Object.freeze({ "aria-describedby": "aria-describedby", "aria-inva
8593
/**
8694
* @typedef {Object} ValidateFieldOptions
8795
* @property {boolean} [focus] Indicates that the field should be focused if it fails validation. Defaults to `false`.
96+
*
97+
* @property {boolean} [enableRevalidation] Enables revalidation for the validated field. Defaults to `true`.
98+
* (This option is only relevant if a value was provided for the observer's
99+
* {@link FormValidityObserverOptions.revalidateOn `revalidateOn`} option.)
88100
*/
89101

90102
/**
91103
* @typedef {Object} ValidateFieldsOptions
92104
* @property {boolean} [focus] Indicates that the _first_ field in the DOM that fails validation should be focused.
93105
* Defaults to `false`.
106+
*
107+
* @property {boolean} [enableRevalidation] Enables revalidation for **all** of the form's fields. Defaults to `true`.
108+
* (This option is only relevant if a value was provided for the observer's
109+
* {@link FormValidityObserverOptions.revalidateOn `revalidateOn`} option.)
94110
*/
95111

96112
/** @template [M=string] @template {boolean} [R=false] */
@@ -138,7 +154,6 @@ class FormValidityObserver extends FormObserver {
138154
/** @type {import("./types.d.ts").EventType[]} */ const types = [];
139155
/** @type {((event: Event & {target: import("./types.d.ts").ValidatableField }) => void)[]} */ const listeners = [];
140156

141-
// NOTE: We know this looks like overkill for something so simple. It'll make sense when we support `revalidateOn`.
142157
if (typeof type === "string") {
143158
types.push(type);
144159
listeners.push((event) => {
@@ -147,6 +162,14 @@ class FormValidityObserver extends FormObserver {
147162
});
148163
}
149164

165+
if (typeof options?.revalidateOn === "string") {
166+
types.push(options.revalidateOn);
167+
listeners.push((event) => {
168+
const field = event.target;
169+
if (field.hasAttribute(attrs["data-fvo-revalidate"])) this.validateField(field.name);
170+
});
171+
}
172+
150173
super(types, listeners, { passive: true, capture: options?.useEventCapturing });
151174
this.#scrollTo = options?.scroller ?? defaultScroller;
152175
this.#renderError = /** @type {any} Necessary because of double `M`s */ (options?.renderer ?? defaultErrorRenderer);
@@ -214,6 +237,7 @@ class FormValidityObserver extends FormObserver {
214237
validateFields(options) {
215238
assertFormExists(this.#form);
216239
let syncValidationPassed = true;
240+
/** @type {ValidateFieldOptions} */ const validatorOptions = { enableRevalidation: options?.enableRevalidation };
217241

218242
/** @type {Promise<boolean>[] | undefined} */
219243
let pendingValidations;
@@ -238,7 +262,7 @@ class FormValidityObserver extends FormObserver {
238262
if (field.type === "radio") validatedRadiogroups.add(name);
239263

240264
// Validate Field and Update Internal State
241-
const result = this.validateField(name);
265+
const result = this.validateField(name, validatorOptions);
242266
if (result === true) continue;
243267
if (result === false) {
244268
syncValidationPassed = false;
@@ -313,6 +337,7 @@ class FormValidityObserver extends FormObserver {
313337
const field = this.#getTargetField(name);
314338
if (!field) return false; // TODO: should we give a warning that the field doesn't exist? Same for other methods.
315339
if (!field.willValidate) return true;
340+
if (options?.enableRevalidation ?? true) field.setAttribute(attrs["data-fvo-revalidate"], "");
316341

317342
field.setCustomValidity?.(""); // Reset the custom error message in case a default browser error is displayed next.
318343

0 commit comments

Comments
 (0)