/** * Case Study: Interactive Form with Validation * * This example demonstrates: * - Form handling with real-time validation * - Reactive UI updates based on input state * - Complex form state management * - Clean separation of concerns (data, validation, UI) */ import { dispatchEvent, el, on, scope } from "deka-dom-el"; import { S } from "deka-dom-el/signals"; /** * @typedef {Object} FormState * @property {string} name * @property {string} email * @property {string} password * @property {string} confirmPassword * @property {boolean} agreedToTerms * */ /** * Interactive Form with Validation Component * @returns {HTMLElement} Form element */ export function InteractiveForm() { const submitted = S(false); /** @type {FormState|null} */ let formState = null; /** @param {CustomEvent} event */ const onSubmit = ({ detail }) => { submitted.set(true); formState = detail; }; const onAnotherAccount = () => { submitted.set(false) formState = null; }; return el("div", { className: "form-container" }).append( S.el(submitted, s => s ? el("div", { className: "success-message" }).append( el("h3", "Thank you for registering!"), el("p", `Welcome, ${formState.name}! Your account has been created successfully.`), el("button", { textContent: "Register another account", type: "button" }, on("click", onAnotherAccount) ), ) : el(Form, { initial: formState }, on("form:submit", onSubmit)) ) ); } /** * Form Component * @type {(props: { initial: FormState | null }) => HTMLElement} * */ export function Form({ initial }) { const { host }= scope; // Form state management const formState = S(initial || { name: '', email: '', password: '', confirmPassword: '', agreedToTerms: false }, { /** * @template {keyof FormState} K * @param {K} key * @param {FormState[K]} value * */ update(key, value) { this.value[key] = value; } }); /** * Event handler for input events * @param {"value"|"checked"} prop * @returns {(ev: Event) => void} * */ const onChange= prop => ev => { const input = /** @type {HTMLInputElement} */(ev.target); S.action(formState, "update", /** @type {keyof FormState} */(input.id), input[prop]); }; // Form validate state const nameValid = S(() => formState.get().name.length >= 3); const emailValid = S(() => { const email = formState.get().email; return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); }); const passwordValid = S(() => { const password = formState.get().password; return password.length >= 8 && /[A-Z]/.test(password) && /[0-9]/.test(password); }); const passwordsMatch = S(() => { const { password, confirmPassword } = formState.get(); return password === confirmPassword && confirmPassword !== ''; }); const termsAgreed = S(() => formState.get().agreedToTerms); const formValid = S(() => nameValid.get() && emailValid.get() && passwordValid.get() && passwordsMatch.get() && termsAgreed.get() ); const dispatcSubmit = dispatchEvent("form:submit", host); const onSubmit = on("submit", e => { e.preventDefault(); if (!formValid.get()) { return; } dispatcSubmit(formState.get()); }); // Component UI return el("form", { className: "registration-form" }, onSubmit).append( el("h2", "Create an Account"), // Name field el("div", { classList: { "form-group": true, valid: nameValid, invalid: S(()=> !nameValid.get() && formState.get().name) }}).append( el("label", { htmlFor: "name", textContent: "Full Name" }), el("input", { id: "name", type: "text", value: formState.get().name, placeholder: "Enter your full name" }, on("input", onChange("value"))), el("div", { className: "validation-message", textContent: "Name must be at least 3 characters long" }), ), // Email field el("div", { classList: { "form-group": true, valid: emailValid, invalid: S(()=> !emailValid.get() && formState.get().email) }}).append( el("label", { htmlFor: "email", textContent: "Email Address" }), el("input", { id: "email", type: "email", value: formState.get().email, placeholder: "Enter your email address" }, on("input", onChange("value"))), el("div", { className: "validation-message", textContent: "Please enter a valid email address" }) ), // Password field el("div", { classList: { "form-group": true, valid: passwordValid, invalid: S(()=> !passwordValid.get() && formState.get().password) }}).append( el("label", { htmlFor: "password", textContent: "Password" }), el("input", { id: "password", type: "password", value: formState.get().password, placeholder: "Create a password" }, on("input", onChange("value"))), el("div", { className: "validation-message", textContent: "Password must be at least 8 characters with at least one uppercase letter and one number", }), ), // Confirm password field el("div", { classList: { "form-group": true, valid: passwordsMatch, invalid: S(()=> !passwordsMatch.get() && formState.get().confirmPassword) }}).append( el("label", { htmlFor: "confirmPassword", textContent: "Confirm Password" }), el("input", { id: "confirmPassword", type: "password", value: formState.get().confirmPassword, placeholder: "Confirm your password" }, on("input", onChange("value"))), el("div", { className: "validation-message", textContent: "Passwords must match" }), ), // Terms agreement el("div", { className: "form-group checkbox-group" }).append( el("input", { id: "agreedToTerms", type: "checkbox", checked: formState.get().agreedToTerms }, on("change", onChange("checked"))), el("label", { htmlFor: "agreedToTerms", textContent: "I agree to the Terms and Conditions" }), ), // Submit button el("button", { textContent: "Create Account", type: "submit", className: "submit-button", disabled: S(() => !formValid.get()) }), ); } // Render the component document.body.append( el("div", { style: "padding: 20px; background: #f5f5f5; min-height: 100vh;" }).append( el(InteractiveForm) ), el("style", ` .form-container { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; max-width: 500px; margin: 0 auto; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); background: #fff; } h2 { margin-top: 0; color: #333; margin-bottom: 1.5rem; } .form-group { margin-bottom: 1.5rem; position: relative; transition: all 0.3s ease; } label { display: block; margin-bottom: 0.5rem; color: #555; font-weight: 500; } input[type="text"], input[type="email"], input[type="password"] { width: 100%; padding: 0.75rem; border: 1px solid #ddd; border-radius: 4px; font-size: 1rem; transition: border-color 0.3s ease; } input:focus { outline: none; border-color: #4a90e2; box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2); } .checkbox-group { display: flex; align-items: center; } .checkbox-group label { margin: 0 0 0 0.5rem; } .validation-message { font-size: 0.85rem; color: #e74c3c; margin-top: 0.5rem; height: 0; overflow: hidden; opacity: 0; transition: all 0.3s ease; } .form-group.invalid .validation-message { height: auto; opacity: 1; } .form-group.valid input { border-color: #2ecc71; } .form-group.invalid input { border-color: #e74c3c; } .submit-button { background-color: #4a90e2; color: white; border: none; border-radius: 4px; padding: 0.75rem 1.5rem; font-size: 1rem; cursor: pointer; transition: background-color 0.3s ease; width: 100%; } .submit-button:hover:not(:disabled) { background-color: #3a7bc8; } .submit-button:disabled { background-color: #b5b5b5; cursor: not-allowed; } .success-message { text-align: center; color: #2ecc71; } .success-message h3 { margin-top: 0; } .success-message button { background-color: #2ecc71; color: white; border: none; border-radius: 4px; padding: 0.75rem 1.5rem; font-size: 1rem; cursor: pointer; margin-top: 1rem; } .success-message button:hover { background-color: #27ae60; } `), );