Skip to content

Commit

Permalink
refactor: Use JSDocs Directly for FormObserver and `FormStorageObse…
Browse files Browse the repository at this point in the history
…rver`

This allows us to have our documentation and our types
in the same place! Hopefully it'll make developer experience
better in the long run. We'll make this update for the
`FormValidityObserver` as well.

HUGE thanks to @DanielRosenwasser! (See his current
solution for JSDocs in microsoft/TypeScript#55916.)

This is GAME CHANGING.
  • Loading branch information
ITenthusiasm committed Sep 30, 2023
1 parent bb00ae9 commit 52dfd6d
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 18 deletions.
82 changes: 72 additions & 10 deletions packages/core/FormObserver.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { assertElementIsForm } from "./utils/assertions.js";

/**
* @template {import("./types.d.ts").OneOrMany<import("./types.d.ts").EventType>} T
* @type {import("./types.d.ts").FormObserverConstructor}
*/
const FormObserver = class {
class FormObserver {
/* ---------- Constructor-related Fields. (Must be compatible with `document.addEventListener`.) ---------- */
/** @readonly @type {ReadonlyArray<import("./types.d.ts").EventType>} */
#types;
Expand All @@ -22,8 +18,67 @@ const FormObserver = class {
*/
#observedForms = new Set();

/* ---------------------------------------- Constructor Setup ---------------------------------------- */
/**
* Provides a way to respond to events emitted by the fields belonging to an `HTMLFormElement`.
*
* @template {import("./types.d.ts").EventType} T1
* @overload
*
* @param {T1} type The type of event to respond to.
* @param {import("./types.d.ts").FormFieldListener<T1>} listener The function to call when a form field
* emits an event matching the provided `type`.
* @param {import("./types.d.ts").ListenerOptions} [options] The `addEventListener` options for the provided
* `listener`.
*
* See {@link https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener addEventListener}.
* @returns {FormObserver}
*/

/**
* Provides a way to respond to events emitted by the fields belonging to an `HTMLFormElement`.
*
* @template {ReadonlyArray<import("./types.d.ts").EventType>} T2
* @overload
*
* @param {T2} types An array containing the types of events to respond to.
* @param {import("./types.d.ts").FormFieldListener<T2[number]>} listener The function to call when a form field emits
* an event specified in the list of `types`.
* @param {import("./types.d.ts").ListenerOptions} [options] The `addEventListener` options for the provided
* `listener`.
*
* See {@link https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener addEventListener}.
* @returns {FormObserver}
*/

/**
* Provides a way to respond to events emitted by the fields belonging to an `HTMLFormElement`.
*
* @template {ReadonlyArray<import("./types.d.ts").EventType>} T3
* @overload
*
* @param {T3} types An array containing the types of events to respond to.
* @param {import("./types.d.ts").TypesToListeners<T3>} listeners An array of event listeners corresponding
* to the provided list of `types`. When an event matching one of the `types` is emitted by a form field, its
* corresponding listener function will be called.
*
* For example, when a field emits an event matching the 2nd type in `types`, the 2nd listener will be called.
* @param {import("./types.d.ts").OneOrMany<import("./types.d.ts").ListenerOptions>} [options] An array of
* `addEventListener` options corresponding to the provided list of `listeners`. When a listener is attached
* to a form's `Document`, the listener's corresponding set of options will be used to configure it.
*
* For example, when the 2nd listener in `listeners` is attached to the `Document`, it will use the 2nd value
* in the `options` array for its configuration.
*
* If `options` is a single value instead of an array, then that value will be used to configure all of
* the listeners.)
*
* See {@link https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener addEventListener}.
* @returns {FormObserver}
*/

/**
* @param {T} types
* @param {import("./types.d.ts").OneOrMany<import("./types.d.ts").EventType>} types
* @param {import("./types.d.ts").OneOrMany<import("./types.d.ts").FormFieldListener<import("./types.d.ts").EventType>>} listeners
* @param {import("./types.d.ts").OneOrMany<import("./types.d.ts").ListenerOptions>} options
*/
Expand Down Expand Up @@ -69,9 +124,14 @@ const FormObserver = class {
else this.#options = Array.from({ length: types.length }, () => options);
}

/* ---------------------------------------- Class Methods ---------------------------------------- */
/**
* Instructs the observer to listen for events emitted from the provided `form`'s fields.
* The observer will only listen for events which match the types that were specified
* during its instantiation.
*
* @param {HTMLFormElement} form
* @returns {boolean}
* @returns {boolean} `true` if the `form` was not already being observed, and `false` otherwise.
*/
observe(form) {
assertElementIsForm(form);
Expand All @@ -98,8 +158,10 @@ const FormObserver = class {
}

/**
* Stops the observer from listening for any events emitted from the provided `form`'s fields.
*
* @param {HTMLFormElement} form
* @returns {boolean}
* @returns {boolean} `true` if the `form` was originally being observed, and `false` otherwise.
*/
unobserve(form) {
assertElementIsForm(form);
Expand Down Expand Up @@ -129,11 +191,11 @@ const FormObserver = class {
return true;
}

/** @returns {void} */
/** Stops the observer from listening for any events emitted from all `form` fields. @returns {void} */
disconnect() {
this.#observedForms.forEach((form) => this.unobserve(form));
}
};
}

/**
* @param {unknown} types
Expand Down
67 changes: 60 additions & 7 deletions packages/core/FormStorageObserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,39 @@ import FormObserver from "./FormObserver.js";
import { assertElementIsForm } from "./utils/assertions.js";

/**
* @template {import("./types.d.ts").OneOrMany<import("./types.d.ts").EventType>} T
* @type {import("./types.d.ts").FormStorageObserverConstructor}
* @typedef {Object} FormStorageObserverOptions
* @property {"loading" | "deletion" | "both" | "neither"} [automate] Indicates whether or not the observer should
* automate the loading/removal of a form's `localStorage` data.
* - `loading` (Default): A form's data will automatically be loaded from `localStorage` when it is observed.
* - `deletion`: A form's data will automatically be removed from `localStorage` when it is unobserved.
* - `both`: Behaves as if `loading` and `deletion` were specified simultaneously.
* - `neither`: The observer will not automate any data loading or data removal.
*
* @property {boolean} [useEventCapturing] Indicates that the observer's event listener should be called during the
* event capturing phase instead of the event bubbling phase. Defaults to `false`.
* See {@link https://www.w3.org/TR/DOM-Level-3-Events/#event-flow DOM Event Flow}
*/
const FormStorageObserver = class extends FormObserver {
/** @readonly @type {Required<import("./types.d.ts").FormStorageObserverOptions>["automate"]} */

class FormStorageObserver extends FormObserver {
/** @readonly @type {Required<FormStorageObserverOptions>["automate"]} */
#automate;

/**
* @param {T} types
* @param {import("./types.d.ts").FormStorageObserverOptions} [options]
* @template {import("./types.d.ts").OneOrMany<import("./types.d.ts").EventType>} T
* @overload
*
* Provides a way to store an `HTMLFormElement`'s data in `localStorage` automatically in response to
* the events emitted from its fields.
*
* @param {T} types The type(s) of event(s) that trigger(s) updates to `localStorage`.
* @param {FormStorageObserverOptions} [options]
* @returns {FormStorageObserver}
*/

/**
* @param {import("./types.d.ts").OneOrMany<import("./types.d.ts").EventType>} types The type(s) of event(s)
* that trigger(s) updates to `localStorage`.
* @param {FormStorageObserverOptions} [options]
*/
constructor(types, options) {
super(types, eventListener, { passive: true, capture: options?.useEventCapturing });
Expand All @@ -28,6 +51,21 @@ const FormStorageObserver = class extends FormObserver {
return newlyObserved;
}

/**
* @overload Loads all of the data in `localStorage` related to the provided `form`.
* @param {HTMLFormElement} form
* @returns {void}
*/

/**
* @overload Loads the data in `localStorage` for the field that has the provided `name` and belongs to
* the provided `form`.
*
* @param {HTMLFormElement} form
* @param {string} name
* @returns {void}
*/

/**
* @param {HTMLFormElement} form
* @param {string} [name]
Expand Down Expand Up @@ -133,6 +171,21 @@ const FormStorageObserver = class extends FormObserver {
return newlyUnobserved;
}

/**
* @overload Clears all of the data in `localStorage` related to the provided `form`.
* @param {HTMLFormElement} form
* @returns {void}
*/

/**
* @overload Clears the data in `localStorage` for the field that has the provided `name` and belongs to
* the provided `form`.
*
* @param {HTMLFormElement} form
* @param {string} name
* @returns {void}
*/

/**
* @param {HTMLFormElement} form
* @param {string} [name]
Expand All @@ -151,7 +204,7 @@ const FormStorageObserver = class extends FormObserver {
if (field.name) localStorage.removeItem(getFieldKey(form.name, field.name));
}
}
};
}

/* -------------------- Utility Functions -------------------- */
/*
Expand Down
2 changes: 1 addition & 1 deletion packages/core/__tests__/FormObserver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe("Form Observer (Class)", () => {
* @see {@link testCases}
* @throws {TypeError} for any invalid test case
*/
function getFormObserverByTestCase(testCase: (typeof testCases)[number]): InstanceType<typeof FormObserver> {
function getFormObserverByTestCase(testCase: (typeof testCases)[number]): FormObserver {
if (!testCases.includes(testCase)) {
throw new TypeError("Expected a standardized test case for generating a `Form Observer`.");
}
Expand Down

0 comments on commit 52dfd6d

Please sign in to comment.