Skip to content

Commit fe3d39a

Browse files
committed
feat: Enable the FormValidityObserver to Be Used in "Manual Only" Mode
This "Manual Mode" can be enabled by passing `null` to the `type` argument of the constructor. The main reason that this is useful is that it gives developers a way to only validate their forms `onsubmit` if they so please.
1 parent 95c5488 commit fe3d39a

11 files changed

+77
-45
lines changed

docs/form-validity-observer/README.md

+8-3
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,14 @@ As expected for any form validation library, we also support the following featu
8787
The `FormValidityObserver()` constructor creates a new observer and configures it with the `options` that you pass in. Because the `FormValidityObserver` only focuses on one task, it has a simple constructor with no overloads.
8888

8989
<dl>
90-
<dt id="form-validity-observer-parameters-types"><code>type: EventType</code></dt>
90+
<dt id="form-validity-observer-parameters-types"><code>type: EventType | null</code></dt>
9191
<dd>
92-
A string representing the type of event that should cause a form's field to be validated. As with the <code>FormObserver</code>, the string can be a <a href="https://developer.mozilla.org/en-US/docs/Web/Events">commonly recognized</a> event type <em>or</em> your own <a href="../form-observer/guides.md#supporting-custom-event-types">custom</a> event type. But in the case of the <code>FormValidityObserver</code>, only one event type may be specified.
92+
<p>
93+
A string representing the type of event that should cause a form's field to be validated. As with the <code>FormObserver</code>, the string can be a <a href="https://developer.mozilla.org/en-US/docs/Web/Events">commonly recognized</a> event type <em>or</em> your own <a href="../form-observer/guides.md#supporting-custom-event-types">custom</a> event type. But in the case of the <code>FormValidityObserver</code>, only one event type may be specified.
94+
</p>
95+
<p>
96+
If you <em>only</em> want to validate fields manually, you can specify <code>null</code> instead of an event type. This can be useful, for instance, if you only want to validate your fields <code>onsubmit</code>. (You would still need to call <a href="#method-formvalidityobservervalidatefieldsoptions-validatefieldsoptions-boolean--promiseboolean"><code>FormValidityObserver.validateFields()</code></a> manually in your <code>submit</code> handler in that scenario.)
97+
</p>
9398
</dd>
9499

95100
<dt id="form-validity-observer-parameters-options"><code>options</code> (Optional)</dt>
@@ -169,7 +174,7 @@ const observer = new FormValidityObserver("focusout", {
169174

170175
### Method: `FormValidityObserver.observe(form: HTMLFormElement): boolean`
171176

172-
Instructs the observer to validate any fields (belonging to the provided form) that a user interacts with, and registers the observer's validation methods with the provided form. Automatic field validation will only occur when a field belonging to the form emits an event matching one of the `types` that were specified during the observer's construction. Unlike the `FormObserver` and the `FormStorageObserver`, _the `FormValidityObserver` may only observe 1 form at a time_.
177+
Instructs the observer to validate any fields (belonging to the provided form) that a user interacts with, and registers the observer's validation methods with the provided form. Automatic field validation will only occur when a field belonging to the form emits an event matching the `type` that was specified during the observer's construction. Unlike the `FormObserver` and the `FormStorageObserver`, _the `FormValidityObserver` may only observe 1 form at a time_.
173178

174179
Note that the `name` attribute is what the observer uses to [identify fields](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormControlsCollection/namedItem) during manual form validation and error handling. Therefore, a valid `name` is required for all validated fields. **If a field does not have a `name`, then it _will not_ participate in form validation.** Since the [web specification](https://www.w3.org/TR/html401/interact/forms.html#successful-controls) does not allow nameless fields to participate in form submission, this is likely a requirement that your application already satisfies.
175180

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

+14-15
Original file line numberDiff line numberDiff line change
@@ -128,16 +128,16 @@ We'll walk you through the process by going step-by-step on how we made our `Sve
128128
The first step is easy. Just create a function that instantiates and returns a `FormValidityObserver`. Because this function will only be creating an augmented `FormValidityObserver`, it should accept the same arguments as the class's [constructor](../README.md#constructor-formvalidityobservertypes-options). The return type will be an `interface` that represents the enhanced observer, but we won't add anything to it yet.
129129
130130
```ts
131-
import type { EventType, OneOrMany, ValidatableField, FormValidityObserverOptions } from "@form-observer/core";
131+
import type { EventType, ValidatableField, FormValidityObserverOptions } from "@form-observer/core";
132132
import FormValidityObserver from "@form-observer/core/FormValidityObserver";
133133
134134
function createFormValidityObserver<
135-
T extends OneOrMany<EventType>,
135+
T extends EventType | null,
136136
M = string,
137137
E extends ValidatableField = ValidatableField,
138138
R extends boolean = false,
139-
>(types: T, options?: FormValidityObserverOptions<M, E, R>): SvelteFormValidityObserver<M, R> {
140-
const observer = new FormValidityObserver(types, options) as unknown as SvelteFormValidityObserver<M, R>;
139+
>(type: T, options?: FormValidityObserverOptions<M, E, R>): SvelteFormValidityObserver<M, R> {
140+
const observer = new FormValidityObserver(type, options) as unknown as SvelteFormValidityObserver<M, R>;
141141
return observer;
142142
}
143143
@@ -161,12 +161,12 @@ In order to ensure that all of the `FormValidityObserver`'s methods function pro
161161
// Imports ...
162162
163163
function createFormValidityObserver<
164-
T extends OneOrMany<EventType>,
164+
T extends EventType | null,
165165
M = string,
166166
E extends ValidatableField = ValidatableField,
167167
R extends boolean = false,
168-
>(types: T, options?: FormValidityObserverOptions<M, E, R>): SvelteFormValidityObserver<M, R> {
169-
const observer = new FormValidityObserver(types, options) as unknown as SvelteFormValidityObserver<M, R>;
168+
>(type: T, options?: FormValidityObserverOptions<M, E, R>): SvelteFormValidityObserver<M, R> {
169+
const observer = new FormValidityObserver(type, options) as unknown as SvelteFormValidityObserver<M, R>;
170170
171171
/* ---------- Bindings ---------- */
172172
// Form Observer Methods
@@ -199,17 +199,17 @@ In this step, we create a reusable utility function that will enable us to autom
199199
Most JS frameworks create a way for you to accomplish this easily with utility functions. In [`React`](https://react.dev/reference/react-dom/components/common#ref-callback) or [`Vue`](https://vuejs.org/api/built-in-special-attributes.html#ref), you would pass a `ref` callback to an `HTMLFormElement`. In `Svelte`, the idiomatic way to accomplish this is with [`actions`](https://learn.svelte.dev/tutorial/actions):
200200
201201
```ts
202-
import type { EventType, OneOrMany, ValidatableField, FormValidityObserverOptions } from "@form-observer/core";
202+
import type { EventType, ValidatableField, FormValidityObserverOptions } from "@form-observer/core";
203203
import FormValidityObserver from "@form-observer/core/FormValidityObserver";
204204
import type { ActionReturn } from "svelte/action";
205205
206206
function createFormValidityObserver<
207-
T extends OneOrMany<EventType>,
207+
T extends EventType | null,
208208
M = string,
209209
E extends ValidatableField = ValidatableField,
210210
R extends boolean = false,
211-
>(types: T, options?: FormValidityObserverOptions<M, E, R>): SvelteFormValidityObserver<M, R> {
212-
const observer = new FormValidityObserver(types, options) as unknown as SvelteFormValidityObserver<M, R>;
211+
>(type: T, options?: FormValidityObserverOptions<M, E, R>): SvelteFormValidityObserver<M, R> {
212+
const observer = new FormValidityObserver(type, options) as unknown as SvelteFormValidityObserver<M, R>;
213213
214214
/* ---------- Bindings ---------- */
215215
// Apply all bindings...
@@ -305,7 +305,6 @@ Now that we've specified all of the requirements, let's implement this functiona
305305
```ts
306306
import type {
307307
EventType,
308-
OneOrMany,
309308
ErrorMessage,
310309
ValidationErrors,
311310
ValidatableField,
@@ -421,12 +420,12 @@ The hardest part of this process is defining the TypeScript types. With that out
421420
// Imports ...
422421
423422
export default function createFormValidityObserver<
424-
T extends OneOrMany<EventType>,
423+
T extends EventType | null,
425424
M = string,
426425
E extends ValidatableField = ValidatableField,
427426
R extends boolean = false,
428-
>(types: T, options?: FormValidityObserverOptions<M, E, R>): SvelteFormValidityObserver<M, R> {
429-
const observer = new FormValidityObserver(types, options) as unknown as SvelteFormValidityObserver<M, R>;
427+
>(type: T, options?: FormValidityObserverOptions<M, E, R>): SvelteFormValidityObserver<M, R> {
428+
const observer = new FormValidityObserver(type, options) as unknown as SvelteFormValidityObserver<M, R>;
430429
431430
/* ---------- Bindings ---------- */
432431
// Apply bindings for exposed methods ...

packages/core/FormValidityObserver.d.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,15 @@ interface FormValidityObserverConstructor {
9595
* Provides a way to validate an `HTMLFormElement`'s fields (and to display _accessible_ errors for those fields)
9696
* in response to the events that the fields emit.
9797
*
98-
* @param type The type of event that triggers form field validation.
98+
* @param type The type of event that triggers form field validation. (If you _only_ want to validate fields manually,
99+
* you can specify `null` instead of an event type.)
99100
*/
100-
new <T extends EventType, M = string, E extends ValidatableField = ValidatableField, R extends boolean = false>(
101+
new <
102+
T extends EventType | null,
103+
M = string,
104+
E extends ValidatableField = ValidatableField,
105+
R extends boolean = false,
106+
>(
101107
type: T,
102108
options?: FormValidityObserverOptions<M, E, R>,
103109
): FormValidityObserver<M, R>;

packages/core/FormValidityObserver.js

+17-15
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ class FormValidityObserver extends FormObserver {
115115
* illegal generic constructors?
116116
*/
117117
/**
118-
* @template {import("./types.d.ts").EventType} T
118+
* @template {import("./types.d.ts").EventType | null} T
119119
* @template [M=string]
120120
* @template {import("./types.d.ts").ValidatableField} [E=import("./types.d.ts").ValidatableField]
121121
* @template {boolean} [R=false]
@@ -124,28 +124,30 @@ class FormValidityObserver extends FormObserver {
124124
* Provides a way to validate an `HTMLFormElement`'s fields (and to display _accessible_ errors for those fields)
125125
* in response to the events that the fields emit.
126126
*
127-
* @param {T} type The type of event that triggers form field validation.
127+
* @param {T} type The type of event that triggers form field validation. (If you _only_ want to validate fields
128+
* manually, you can specify `null` instead of an event type.)
128129
* @param {FormValidityObserverOptions<M, E, R>} [options]
129130
* @returns {FormValidityObserver<M, R>}
130131
*/
131132

132133
/**
133-
* @param {import("./types.d.ts").EventType} type
134+
* @param {import("./types.d.ts").EventType | null} type
134135
* @param {FormValidityObserverOptions<M, import("./types.d.ts").ValidatableField, R>} [options]
135136
*/
136137
constructor(type, options) {
137-
/**
138-
* Event listener used to validate form fields in response to user interactions
139-
*
140-
* @param {Event & { target: import("./types.d.ts").ValidatableField }} event
141-
* @returns {void}
142-
*/
143-
const eventListener = (event) => {
144-
const fieldName = event.target.name;
145-
if (fieldName) this.validateField(fieldName);
146-
};
147-
148-
super(type, eventListener, { passive: true, capture: options?.useEventCapturing });
138+
/** @type {import("./types.d.ts").EventType[]} */ const types = [];
139+
/** @type {((event: Event & {target: import("./types.d.ts").ValidatableField }) => void)[]} */ const listeners = [];
140+
141+
// NOTE: We know this looks like overkill for something so simple. It'll make sense when we support `revalidateOn`.
142+
if (typeof type === "string") {
143+
types.push(type);
144+
listeners.push((event) => {
145+
const fieldName = event.target.name;
146+
if (fieldName) this.validateField(fieldName);
147+
});
148+
}
149+
150+
super(types, listeners, { passive: true, capture: options?.useEventCapturing });
149151
this.#scrollTo = options?.scroller ?? defaultScroller;
150152
this.#renderError = /** @type {any} Necessary because of double `M`s */ (options?.renderer ?? defaultErrorRenderer);
151153
this.#renderByDefault = /** @type {any} Necessary because of double `R`s */ (options?.renderByDefault);

packages/core/__tests__/FormValidityObserver.test.ts

+24-4
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,26 @@ describe("Form Validity Observer (Class)", () => {
115115
expect(removeEventListener).toHaveBeenNthCalledWith(2, expect.anything(), expect.anything(), bubbleOptions);
116116
});
117117

118+
it("Does not register any event listeners when `null` is used for the event `type` (Manual Mode)", () => {
119+
/* ---------- Setup ---------- */
120+
const formValidityObserverManual = new FormValidityObserver(null);
121+
const form = document.body.appendChild(document.createElement("form"));
122+
123+
const addEventListener = vi.spyOn(form.ownerDocument, "addEventListener");
124+
const removeEventListener = vi.spyOn(form.ownerDocument, "removeEventListener");
125+
126+
/* ---------- Run Assertions ---------- */
127+
// Test `observe`
128+
formValidityObserverManual.observe(form);
129+
expect(addEventListener).not.toHaveBeenCalled();
130+
expect(() => formValidityObserverManual.validateFields()).not.toThrow();
131+
132+
// Test `unobserve`
133+
formValidityObserverManual.unobserve(form);
134+
expect(removeEventListener).not.toHaveBeenCalled();
135+
expect(() => formValidityObserverManual.validateFields()).toThrow();
136+
});
137+
118138
describe("Overriden Core Methods", () => {
119139
/* -------------------- Assertion Helpers for Core Methods -------------------- */
120140
function expectValidationMethodsToBeEnabled(observer: FormValidityObserver, enabled = true): void {
@@ -2838,10 +2858,10 @@ describe("Form Validity Observer (Class)", () => {
28382858
new FormValidityObserver(event, { useEventCapturing: true });
28392859

28402860
// Multiple Types
2841-
new FormValidityObserver(event);
2842-
new FormValidityObserver(event, {});
2843-
new FormValidityObserver(event, { scroller: undefined });
2844-
new FormValidityObserver(event, { useEventCapturing: undefined });
2861+
new FormValidityObserver(null);
2862+
new FormValidityObserver(null, {});
2863+
new FormValidityObserver(null, { scroller: undefined });
2864+
new FormValidityObserver(null, { useEventCapturing: undefined });
28452865

28462866
new FormValidityObserver(event);
28472867
new FormValidityObserver(event, {});

packages/lit/createFormValidityObserver.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import FormValidityObserver from "@form-observer/core/FormValidityObserver";
33
/**
44
* Creates a version of the {@link FormValidityObserver} that's more convenient for `Lit` apps
55
*
6-
* @template {import("./index.d.ts").EventType} T
6+
* @template {import("./index.d.ts").EventType | null} T
77
* @template [M=string]
88
* @template {import("./index.d.ts").ValidatableField} [E=import("./index.d.ts").ValidatableField]
99
* @template {boolean} [R=false]

packages/preact/createFormValidityObserver.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import FormValidityObserver from "@form-observer/core/FormValidityObserver";
33
/**
44
* Creates an enhanced version of the {@link FormValidityObserver} that's more convenient for `Preact` apps
55
*
6-
* @template {import("./index.d.ts").EventType} T
6+
* @template {import("./index.d.ts").EventType | null} T
77
* @template [M=string]
88
* @template {import("./index.d.ts").ValidatableField} [E=import("./index.d.ts").ValidatableField]
99
* @template {boolean} [R=false]

packages/react/createFormValidityObserver.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const constraintsMap = Object.freeze({
2121
/**
2222
* Creates an enhanced version of the {@link FormValidityObserver} that's more convenient for `React` apps
2323
*
24-
* @template {import("./index.d.ts").EventType} T
24+
* @template {import("./index.d.ts").EventType | null} T
2525
* @template [M=string]
2626
* @template {import("./index.d.ts").ValidatableField} [E=import("./index.d.ts").ValidatableField]
2727
* @template {boolean} [R=false]

packages/solid/createFormValidityObserver.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { onMount, onCleanup } from "solid-js";
44
/**
55
* Creates an enhanced version of the {@link FormValidityObserver} that's more convenient for `Solid` apps
66
*
7-
* @template {import("./index.d.ts").EventType} T
7+
* @template {import("./index.d.ts").EventType | null} T
88
* @template [M=string | import("solid-js").JSX.Element]
99
* @template {import("./index.d.ts").ValidatableField} [E=import("./index.d.ts").ValidatableField]
1010
* @template {boolean} [R=false]

packages/svelte/createFormValidityObserver.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import FormValidityObserver from "@form-observer/core/FormValidityObserver";
33
/**
44
* Creates an enhanced version of the {@link FormValidityObserver} that's more convenient for `Svelte` apps
55
*
6-
* @template {import("./index.d.ts").EventType} T
6+
* @template {import("./index.d.ts").EventType | null} T
77
* @template [M=string]
88
* @template {import("./index.d.ts").ValidatableField} [E=import("./index.d.ts").ValidatableField]
99
* @template {boolean} [R=false]

packages/vue/createFormValidityObserver.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import FormValidityObserver from "@form-observer/core/FormValidityObserver";
33
/**
44
* Creates an enhanced version of the {@link FormValidityObserver} that's more convenient for `Vue` apps
55
*
6-
* @template {import("./index.d.ts").EventType} T
6+
* @template {import("./index.d.ts").EventType | null} T
77
* @template [M=string]
88
* @template {import("./index.d.ts").ValidatableField} [E=import("./index.d.ts").ValidatableField]
99
* @template {boolean} [R=false]

0 commit comments

Comments
 (0)