Skip to content

Commit

Permalink
feat: Enable the FormValidityObserver to Only Be Used Manually
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ITenthusiasm committed Apr 30, 2024
1 parent 95c5488 commit 17a1c58
Show file tree
Hide file tree
Showing 10 changed files with 63 additions and 30 deletions.
11 changes: 8 additions & 3 deletions docs/form-validity-observer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,14 @@ As expected for any form validation library, we also support the following featu
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.

<dl>
<dt id="form-validity-observer-parameters-types"><code>type: EventType</code></dt>
<dt id="form-validity-observer-parameters-types"><code>type: EventType | null</code></dt>
<dd>
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.
<p>
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.
</p>
<p>
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.)
</p>
</dd>

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

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

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

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.

Expand Down
10 changes: 8 additions & 2 deletions packages/core/FormValidityObserver.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,15 @@ interface FormValidityObserverConstructor {
* Provides a way to validate an `HTMLFormElement`'s fields (and to display _accessible_ errors for those fields)
* in response to the events that the fields emit.
*
* @param type The type of event that triggers form field validation.
* @param type The type of event that triggers form field validation. (If you _only_ want to validate fields manually,
* you can specify `null` instead of an event type.)
*/
new <T extends EventType, M = string, E extends ValidatableField = ValidatableField, R extends boolean = false>(
new <
T extends EventType | null,
M = string,
E extends ValidatableField = ValidatableField,
R extends boolean = false,
>(
type: T,
options?: FormValidityObserverOptions<M, E, R>,
): FormValidityObserver<M, R>;
Expand Down
32 changes: 17 additions & 15 deletions packages/core/FormValidityObserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ class FormValidityObserver extends FormObserver {
* illegal generic constructors?
*/
/**
* @template {import("./types.d.ts").EventType} T
* @template {import("./types.d.ts").EventType | null} T
* @template [M=string]
* @template {import("./types.d.ts").ValidatableField} [E=import("./types.d.ts").ValidatableField]
* @template {boolean} [R=false]
Expand All @@ -124,28 +124,30 @@ class FormValidityObserver extends FormObserver {
* Provides a way to validate an `HTMLFormElement`'s fields (and to display _accessible_ errors for those fields)
* in response to the events that the fields emit.
*
* @param {T} type The type of event that triggers form field validation.
* @param {T} type The type of event that triggers form field validation. (If you _only_ want to validate fields
* manually, you can specify `null` instead of an event type.)
* @param {FormValidityObserverOptions<M, E, R>} [options]
* @returns {FormValidityObserver<M, R>}
*/

/**
* @param {import("./types.d.ts").EventType} type
* @param {import("./types.d.ts").EventType | null} type
* @param {FormValidityObserverOptions<M, import("./types.d.ts").ValidatableField, R>} [options]
*/
constructor(type, options) {
/**
* Event listener used to validate form fields in response to user interactions
*
* @param {Event & { target: import("./types.d.ts").ValidatableField }} event
* @returns {void}
*/
const eventListener = (event) => {
const fieldName = event.target.name;
if (fieldName) this.validateField(fieldName);
};

super(type, eventListener, { passive: true, capture: options?.useEventCapturing });
/** @type {import("./types.d.ts").EventType[]} */ const types = [];
/** @type {((event: Event & {target: import("./types.d.ts").ValidatableField }) => void)[]} */ const listeners = [];

// NOTE: We know this looks like overkill for something so simple. It'll make sense when we support `revalidateOn`.
if (typeof type === "string") {
types.push(type);
listeners.push((event) => {
const fieldName = event.target.name;
if (fieldName) this.validateField(fieldName);
});
}

super(types, listeners, { passive: true, capture: options?.useEventCapturing });
this.#scrollTo = options?.scroller ?? defaultScroller;
this.#renderError = /** @type {any} Necessary because of double `M`s */ (options?.renderer ?? defaultErrorRenderer);
this.#renderByDefault = /** @type {any} Necessary because of double `R`s */ (options?.renderByDefault);
Expand Down
28 changes: 24 additions & 4 deletions packages/core/__tests__/FormValidityObserver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,26 @@ describe("Form Validity Observer (Class)", () => {
expect(removeEventListener).toHaveBeenNthCalledWith(2, expect.anything(), expect.anything(), bubbleOptions);
});

it("Does not register any event listeners when `null` is used for the event `type` (Manual Mode)", () => {
/* ---------- Setup ---------- */
const formValidityObserverManual = new FormValidityObserver(null);
const form = document.body.appendChild(document.createElement("form"));

const addEventListener = vi.spyOn(form.ownerDocument, "addEventListener");
const removeEventListener = vi.spyOn(form.ownerDocument, "removeEventListener");

/* ---------- Run Assertions ---------- */
// Test `observe`
formValidityObserverManual.observe(form);
expect(addEventListener).not.toHaveBeenCalled();
expect(() => formValidityObserverManual.validateFields()).not.toThrow();

// Test `unobserve`
formValidityObserverManual.unobserve(form);
expect(removeEventListener).not.toHaveBeenCalled();
expect(() => formValidityObserverManual.validateFields()).toThrow();
});

describe("Overriden Core Methods", () => {
/* -------------------- Assertion Helpers for Core Methods -------------------- */
function expectValidationMethodsToBeEnabled(observer: FormValidityObserver, enabled = true): void {
Expand Down Expand Up @@ -2838,10 +2858,10 @@ describe("Form Validity Observer (Class)", () => {
new FormValidityObserver(event, { useEventCapturing: true });

// Multiple Types
new FormValidityObserver(event);
new FormValidityObserver(event, {});
new FormValidityObserver(event, { scroller: undefined });
new FormValidityObserver(event, { useEventCapturing: undefined });
new FormValidityObserver(null);
new FormValidityObserver(null, {});
new FormValidityObserver(null, { scroller: undefined });
new FormValidityObserver(null, { useEventCapturing: undefined });

new FormValidityObserver(event);
new FormValidityObserver(event, {});
Expand Down
2 changes: 1 addition & 1 deletion packages/lit/createFormValidityObserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import FormValidityObserver from "@form-observer/core/FormValidityObserver";
/**
* Creates a version of the {@link FormValidityObserver} that's more convenient for `Lit` apps
*
* @template {import("./index.d.ts").EventType} T
* @template {import("./index.d.ts").EventType | null} T
* @template [M=string]
* @template {import("./index.d.ts").ValidatableField} [E=import("./index.d.ts").ValidatableField]
* @template {boolean} [R=false]
Expand Down
2 changes: 1 addition & 1 deletion packages/preact/createFormValidityObserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import FormValidityObserver from "@form-observer/core/FormValidityObserver";
/**
* Creates an enhanced version of the {@link FormValidityObserver} that's more convenient for `Preact` apps
*
* @template {import("./index.d.ts").EventType} T
* @template {import("./index.d.ts").EventType | null} T
* @template [M=string]
* @template {import("./index.d.ts").ValidatableField} [E=import("./index.d.ts").ValidatableField]
* @template {boolean} [R=false]
Expand Down
2 changes: 1 addition & 1 deletion packages/react/createFormValidityObserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const constraintsMap = Object.freeze({
/**
* Creates an enhanced version of the {@link FormValidityObserver} that's more convenient for `React` apps
*
* @template {import("./index.d.ts").EventType} T
* @template {import("./index.d.ts").EventType | null} T
* @template [M=string]
* @template {import("./index.d.ts").ValidatableField} [E=import("./index.d.ts").ValidatableField]
* @template {boolean} [R=false]
Expand Down
2 changes: 1 addition & 1 deletion packages/solid/createFormValidityObserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { onMount, onCleanup } from "solid-js";
/**
* Creates an enhanced version of the {@link FormValidityObserver} that's more convenient for `Solid` apps
*
* @template {import("./index.d.ts").EventType} T
* @template {import("./index.d.ts").EventType | null} T
* @template [M=string | import("solid-js").JSX.Element]
* @template {import("./index.d.ts").ValidatableField} [E=import("./index.d.ts").ValidatableField]
* @template {boolean} [R=false]
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/createFormValidityObserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import FormValidityObserver from "@form-observer/core/FormValidityObserver";
/**
* Creates an enhanced version of the {@link FormValidityObserver} that's more convenient for `Svelte` apps
*
* @template {import("./index.d.ts").EventType} T
* @template {import("./index.d.ts").EventType | null} T
* @template [M=string]
* @template {import("./index.d.ts").ValidatableField} [E=import("./index.d.ts").ValidatableField]
* @template {boolean} [R=false]
Expand Down
2 changes: 1 addition & 1 deletion packages/vue/createFormValidityObserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import FormValidityObserver from "@form-observer/core/FormValidityObserver";
/**
* Creates an enhanced version of the {@link FormValidityObserver} that's more convenient for `Vue` apps
*
* @template {import("./index.d.ts").EventType} T
* @template {import("./index.d.ts").EventType | null} T
* @template [M=string]
* @template {import("./index.d.ts").ValidatableField} [E=import("./index.d.ts").ValidatableField]
* @template {boolean} [R=false]
Expand Down

0 comments on commit 17a1c58

Please sign in to comment.