import { DC } from './MC.js'; import { copyGettersSetters, LooseObject } from './helpers.js'; import { observe } from './observables.js'; /** Union type for form input elements. */ type inputTypes = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement; /** Extracts the lowercase tag name of an element (e.g., 'input', 'select', 'textarea'). @param e - The HTML element to extract the tag name from. @returns The lowercase tag name string. */ const getInputName = (e: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement): string => { return e.tagName.toLowerCase(); }; /** Abstract base class for form input value handlers. Defines the interface that all handler classes must implement. */ abstract class InputBase { /** Creates a new InputBase instance. @param e - The HTML element being handled (input, select, or textarea). */ constructor(e: inputTypes) {} /** Gets the current value of the form control. @returns The value in an appropriate type for the control. */ abstract get val(): any; /** Sets the value of the form control. @param value - The new value to assign (type depends on control). */ abstract set val(value: any); } /** Handler class specifically for radio button groups. Provides synchronized access to the selected value in a group of related radio buttons. */ class RadioHandler extends InputBase { #e: HTMLInputElement; /** Creates a new RadioHandler instance. @param e - The first radio button element in the group (used to determine name and form scope). */ constructor(e: HTMLInputElement) { super(e); this.#e = e; } /** Gets the currently selected value of the radio button group using FormData API. @returns The string value of the selected radio button, or undefined if none is selected. */ get val(): string { let ret = new FormData(this.#e.form as HTMLFormElement).get(this.#e.name); //console.log('radio[', this.#e.name, '] = "' + ret + '"'); return ret as string; } /** Sets the selected radio button by value. Finds and checks the first radio input matching the specified value within the same group. @param value - The value to match against radio inputs in this group. */ set val(value: string) { let arr = [ ...(this.#e.form as HTMLFormElement).querySelectorAll( `input[type="radio"][name="${this.#e.name}"]` ), ] as HTMLInputElement[]; (arr.filter((e) => e.getAttribute('value') === value).shift() as HTMLInputElement).checked = true; } } /** * Handler class for checkbhox inputs. * Provides synchronized get/set access to the checked state of a single checkbox. */ class CheckboxHandler extends InputBase { #e: HTMLInputElement; /** Creates a new CheckboxHandler instance. @param e - The checkbox element to handle. */ constructor(e: HTMLInputElement) { super(e); this.#e = e; } /** Gets the checked state of the checkbox. @returns True if the checkbox is checked, false otherwise. */ get val(): boolean { return this.#e.checked; } /** Sets the checked state of the checkbox. @param value - The boolean value to assign as the new checked state. */ set val(value: boolean) { this.#e.checked = value; } } /** Handler class for standard single-line input fields (text, password, email, etc.). Provides synchronized get/set access to the value of an input field. */ class InputHandler extends InputBase { #e: HTMLInputElement; /** * Creates a new InputHandler instance. * @param e - The HTML input element to handle. */ constructor(e: HTMLInputElement) { super(e); this.#e = e; } /** * Gets the current value of the input field. * @returns The string content of the input element. */ get val(): string { return this.#e.value; } /** * Sets the value of the input field. * @param value - The new string value for the input element. */ set val(value: string) { this.#e.value = value; } } /** Handler class for select dropdown elements, supporting both single and multiple selection modes. Provides synchronized get/set access to selected option values. */ class SelectHandler extends InputBase { #e: HTMLSelectElement; #multi: boolean = false; /** * Creates a new SelectHandler instance. * @param e - The select element to handle. */ constructor(e: HTMLSelectElement) { super(e); this.#e = e; this.#multi = e.multiple; } /** * Gets the currently selected option value(s). * For single selection, returns a string. For multiple selection, returns an array of strings. * @returns A string or array of strings representing selected values. */ get val(): string[] | string { const ret: string[] = []; // get all options [...this.#e.querySelectorAll('option')] .filter((o) => o.selected) .forEach((o) => { ret.push(o.value); }); if (this.#multi) return ret; return ret.length ? ret[0] : ''; } /** * Sets the selected option(s) by value. * @param value - A string or array of strings representing values to select. */ set val(value: string[] | string) { value = typeof value === 'string' ? [value] : value; // set all options [...this.#e.querySelectorAll('option')].forEach((o) => { if (value.indexOf(o.value) > -1) { o.selected = true; } else { o.selected = false; } }); } } /** Handler class for textarea elements. Provides synchronized get/set access to the multi-line text content of a textarea. */ class TextAreaHandler extends InputBase { #e: HTMLTextAreaElement; /** * Creates a new TextAreaHandler instance. * @param e - The textarea element to handle. */ constructor(e: HTMLTextAreaElement) { super(e); this.#e = e; } /** * Gets the current value of the textarea (defaults to empty string if null/undefined). * @returns The string content of the textarea element. */ get val(): string { return this.#e.value || ''; } /** * Sets the value of the textarea. * @param value - The new string value for the textarea element. */ set val(value: string) { this.#e.value = value; } } /** Collects all form controls within a given DOM scope and creates handler objects that provide synchronized get/set access to their values. @param e - Optional root DOM element or shadow root to search within; defaults to document if omitted. @returns An object where each key is the name attribute of an input, select, or textarea, and each value is a proxy-like accessor object with `val` getter/setter methods. */ const getInputHandlers = (e: HTMLElement | ShadowRoot): LooseObject => { let inputList = [...(e || document).querySelectorAll('input,select,textarea')], handlers: LooseObject = {}; inputList.forEach((input) => { const name: string | null = (input as inputTypes).getAttribute('name'); if (name) { let handler: any = false; switch (getInputName(input as inputTypes)) { case 'input': switch ((input as HTMLInputElement).type) { case 'radio': handler = new RadioHandler(input as HTMLInputElement); break; case 'checkbox': handler = new CheckboxHandler(input as HTMLInputElement); break; default: handler = new InputHandler(input as HTMLInputElement); } break; case 'select': handler = new SelectHandler(input as HTMLSelectElement); break; case 'textarea': handler = new TextAreaHandler(input as HTMLTextAreaElement); break; default: console.warn('handler missing:', name, 'for', input.tagName.toLowerCase(), input); break; } if (handler) { DC.add(input as HTMLElement, name, handler); Object.defineProperty(handlers, name, { enumerable: true, configurable: true, get: (): any => { return handler.val; }, set: (value: any) => { if (handler.val !== value) { handler.val = value; } }, }); } } }); return handlers; }; /** Attaches a form-values observable to a target object property, allowing two-way binding between the DOM form controls and the object's state. @param targetObject - The object to which the form values will be attached as an observable. @param propertyName - The name of the property on the target object that will hold the form data. @param dom - The DOM element or shadow root (typically a form) whose inputs should be observed and bound. @example HTML ```html
Male Female
``` Javascript ```typescript import { formValues } from './formValues.js'; const state = {}; // Bind the form to state.formData formValues(state, 'formData', document.getElementById('myForm')); // Read values from the form console.log(state.formData.username); // string console.log(state.formData.role); // "admin" or "user" console.log(state.formData.bio); // string console.log(state.formData.active); // boolean console.log(state.formData.gender); // "male" or "female" // Write values to the form (updates DOM automatically) state.formData.username = 'john_doe'; state.formData.role = 'user'; state.formData.bio = 'Hello world!'; state.formData.active = true; state.formData.gender = 'male'; ``` */ export function formValues( targetObject: LooseObject, propertyName: string, dom: HTMLElement | ShadowRoot ) { let handlers = getInputHandlers(dom); targetObject[propertyName] = Object.assign({}, handlers); observe(targetObject, propertyName); copyGettersSetters(handlers, targetObject[propertyName]); //Object.seal(targetObject[propertyName]); // object structure may not be changed from now on [...dom.querySelectorAll('input, textarea, select')].forEach((input) => { input.addEventListener('input', (ev) => { //console.log('input event', input, ev); targetObject[propertyName].notify(); }); }); }