import { customElement, html, type TemplateResult, DeesElement, type CSSResult, property, } from '@design.estate/dees-element'; import * as domtools from '@design.estate/dees-domtools'; import { DeesInputCheckbox } from './dees-input-checkbox.js'; import { DeesInputText } from './dees-input-text.js'; import { DeesInputQuantitySelector } from './dees-input-quantityselector.js'; import { DeesInputRadio } from './dees-input-radio.js'; import { DeesInputDropdown } from './dees-input-dropdown.js'; import { DeesInputFileupload } from './dees-input-fileupload.js'; import { DeesInputIban } from './dees-input-iban.js'; import { DeesInputMultitoggle } from './dees-input-multitoggle.js'; import { DeesInputPhone } from './dees-input-phone.js'; import { DeesInputTypelist } from './dees-input-typelist.js'; import { DeesFormSubmit } from './dees-form-submit.js'; import { DeesTable } from './dees-table.js'; import { demoFunc } from './dees-form.demo.js'; // Unified set for form input types const FORM_INPUT_TYPES = [ DeesInputCheckbox, DeesInputDropdown, DeesInputFileupload, DeesInputIban, DeesInputMultitoggle, DeesInputPhone, DeesInputQuantitySelector, DeesInputRadio, DeesInputText, DeesInputTypelist, DeesTable, ]; export type TFormInputElement = | DeesInputCheckbox | DeesInputDropdown | DeesInputFileupload | DeesInputIban | DeesInputMultitoggle | DeesInputPhone | DeesInputQuantitySelector | DeesInputRadio | DeesInputText | DeesInputTypelist | DeesTable; declare global { interface HTMLElementTagNameMap { 'dees-form': DeesForm; } } @customElement('dees-form') export class DeesForm extends DeesElement { public static demo = demoFunc; public name: string = 'myform'; public changeSubject = new domtools.plugins.smartrx.rxjs.Subject(); public readyDeferred = domtools.plugins.smartpromise.defer(); /** * Controls the layout mode of child input components * When true, sets all child inputs to horizontal layout */ @property({ type: Boolean, reflect: true, attribute: 'horizontal-layout' }) public horizontalLayout: boolean = false; public render(): TemplateResult { return html` `; } public async firstUpdated() { const formChildren = this.getFormElements(); this.updateRequiredStatus(); this.updateChildrenLayoutMode(); for (const child of formChildren) { child.changeSubject.subscribe(async () => { const valueObject = await this.collectFormData(); this.changeSubject.next(valueObject); console.log(valueObject); this.updateRequiredStatus(); }); } await this.addBehaviours(); this.readyDeferred.resolve(); } public getFormElements(): Array { return Array.from(this.children).filter((child) => FORM_INPUT_TYPES.includes(child.constructor as any) ) as unknown as TFormInputElement[]; } public getSubmitButton(): DeesFormSubmit | undefined { return Array.from(this.children).find( (child) => child instanceof DeesFormSubmit ) as DeesFormSubmit; } public async updateRequiredStatus() { console.log('checking the required status.'); let requiredOK = true; for (const childArg of this.getFormElements()) { if (childArg.required && !childArg.value) { requiredOK = false; } } if (this.getSubmitButton()) { this.getSubmitButton().disabled = !requiredOK; } } /** * collects the form data * @returns */ public async collectFormData() { const children = this.getFormElements(); const valueObject: { [key: string]: string | number | boolean | any[] | File[] | { option: string; key: string; payload?: any } } = {}; for (const child of children) { if (!child.key) { console.log(`form element with label "${child.label}" has no key. skipping.`); } valueObject[child.key] = child.value; } return valueObject; } public async gatherAndDispatch() { const valueObject = await this.collectFormData(); const formDataEvent = new CustomEvent('formData', { detail: { data: valueObject, }, bubbles: true, }); this.dispatchEvent(formDataEvent); console.log('dispatched data:'); console.log(valueObject); } public setStatus( visualStateArg: 'normal' | 'pending' | 'error' | 'success', textStateArg: string ) { const inputChildren = this.getFormElements(); const submitButton = this.getSubmitButton(); switch (visualStateArg) { case 'normal': submitButton.disabled = false; submitButton.status = 'normal'; for (const inputChild of inputChildren) { inputChild.disabled = false; } break; case 'pending': submitButton.disabled = true; submitButton.status = 'pending'; for (const inputChild of inputChildren) { inputChild.disabled = true; } break; case 'success': submitButton.disabled = true; submitButton.status = 'success'; for (const inputChild of inputChildren) { inputChild.disabled = true; } break; case 'error': submitButton.disabled = true; submitButton.status = 'error'; for (const inputChild of inputChildren) { inputChild.disabled = true; } break; } submitButton.text = textStateArg; } /** * resets the form */ reset() { const inputChildren = this.getFormElements(); const submitButton = this.getSubmitButton(); for (const inputChild of inputChildren) { inputChild.value = null; } this.setStatus('normal', 'Submit'); } public async addBehaviours() { // Use event delegation this.addEventListener('keydown', (event: KeyboardEvent) => { const target = event.target as DeesElement; if (!FORM_INPUT_TYPES.includes(target.constructor as any)) return; if (event.key === 'Enter') { const children = this.getFormElements(); const currentIndex = children.indexOf(target as any); if (currentIndex < children.length - 1) { children[currentIndex + 1].focus(); } else { target.blur(); this.getSubmitButton()?.focus(); } } }); } /** * Updates the layout mode of child input components based on form's horizontalLayout property */ private updateChildrenLayoutMode() { const formChildren = this.getFormElements(); for (const child of formChildren) { if ('layoutMode' in child) { // The child's auto mode will detect this form's horizontal-layout attribute (child as any).layoutMode = 'auto'; } } } /** * Called when properties change */ updated(changedProperties: Map) { super.updated(changedProperties); if (changedProperties.has('horizontalLayout')) { this.updateChildrenLayoutMode(); } } }