mirror of
https://github.com/jaandrle/deka-dom-el
synced 2025-04-02 20:15:53 +02:00
* 🔤 ⚡ T now uses DocumentFragment * 🔤 * 🔤 ⚡ * 🐛 lint * ⚡ cleanup * ⚡ 🔤 lib download * ⚡ 🔤 ui * ⚡ reorganize files * ⚡ on.host * 🐛 on.* types * ⚡ 🔤 cdn * 🔤 converter * 🐛 signal.set(value, force) * ⚡ 🔤 * 🔤 ⚡ converter - convert also comments * ⚡ bs/build * 🔤 ui p14 * 🔤 * 🔤 Examples * 🔤 * 🐛 now only el(..., string|number) * 🐛 fixes #38 * 🔤 * ⚡ on.host → on.defer * 🔤 * 📺
343 lines
8.2 KiB
JavaScript
343 lines
8.2 KiB
JavaScript
/**
|
|
* 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<FormState>} 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;
|
|
}
|
|
});
|
|
|
|
// Derived signals for validation
|
|
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);
|
|
|
|
// Overall form validity
|
|
const formValid = S(() =>
|
|
nameValid.get() &&
|
|
emailValid.get() &&
|
|
passwordValid.get() &&
|
|
passwordsMatch.get() &&
|
|
termsAgreed.get()
|
|
);
|
|
|
|
// Event handlers
|
|
/**
|
|
* 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]);
|
|
};
|
|
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;
|
|
}
|
|
`),
|
|
);
|