import { css, html } from "lit"; import { customElement, property, query } from "lit/decorators.js"; import { BaseElement } from "../../core/base-element.ts"; /** * CTForm Component * * A form wrapper component that provides consistent layout and spacing for forms. * Emits a custom ct-submit event when the form is submitted. * * @element ct-form * * @attr {string} method - HTTP method for form submission (GET or POST) * @attr {string} action - URL for form submission * * @event ct-submit - Fired when the form is submitted (includes form data in detail) * * @slot - Form content (inputs, labels, buttons, etc.) * * @example * ```html * * Name * * * Email * * *
* Submit * Cancel *
*
* ``` */ @customElement("ct-form") export class CTForm extends BaseElement { static override styles = css` :host { display: block; width: 100%; /* Default color values if not provided */ --background: #ffffff; --foreground: #0f172a; --border: #e2e8f0; --ring: #94a3b8; /* Form spacing variables */ --form-gap: 1.5rem; --form-field-gap: 0.5rem; --form-padding: 0; } form { display: flex; flex-direction: column; gap: var(--form-gap); padding: var(--form-padding); width: 100%; } /* Direct children spacing */ ::slotted(*) { margin: 0; } /* Common form field patterns */ ::slotted(ct-label) { margin-bottom: var(--form-field-gap); } /* Field groups (divs, fieldsets) */ ::slotted(div), ::slotted(fieldset) { display: flex; flex-direction: column; gap: var(--form-field-gap); margin: 0; padding: 0; border: none; } /* Horizontal field groups */ ::slotted(.form-row), ::slotted([data-orientation="horizontal"]) { flex-direction: row; align-items: center; gap: 1rem; } /* Form sections */ ::slotted(.form-section) { display: flex; flex-direction: column; gap: var(--form-gap); } /* Button groups typically at form bottom */ ::slotted(.form-actions), ::slotted(.form-buttons) { display: flex; gap: 0.75rem; margin-top: 0.5rem; } /* Responsive adjustments */ @media (max-width: 640px) { ::slotted(.form-row), ::slotted([data-orientation="horizontal"]) { flex-direction: column; align-items: stretch; } ::slotted(.form-actions), ::slotted(.form-buttons) { flex-direction: column; } ::slotted(.form-actions) ct-button, ::slotted(.form-buttons) ct-button { width: 100%; } } `; @property() method: "GET" | "POST" = "GET"; @property() action = ""; @query("form") private _form!: HTMLFormElement; override render() { return html`
`; } private handleSubmit(event: Event): void { // Prevent default form submission event.preventDefault(); event.stopPropagation(); if (!this._form) return; // Collect form data const formData = new FormData(this._form); const data: Record = {}; // Convert FormData to plain object for (const [key, value] of formData.entries()) { if (data[key] !== undefined) { // Handle multiple values with same name (like checkboxes) if (!Array.isArray(data[key])) { data[key] = [data[key]]; } data[key].push(value); } else { data[key] = value; } } // Emit custom event with form data const submitted = this.emit("ct-submit", { data, formData, method: this.method, action: this.action, form: this._form, }); // If event wasn't prevented, submit the form natively if (submitted && this.action) { this._form.submit(); } } /** * Get the form element */ get form(): HTMLFormElement | null { return this._form; } /** * Submit the form programmatically */ submit(): void { if (this._form) { const event = new Event("submit", { bubbles: true, cancelable: true, }); this._form.dispatchEvent(event); } } /** * Reset the form */ reset(): void { if (this._form) { this._form.reset(); } } /** * Check form validity */ checkValidity(): boolean { return this._form?.checkValidity() ?? false; } /** * Report form validity */ reportValidity(): boolean { return this._form?.reportValidity() ?? false; } }