mirror of
https://github.com/jaandrle/deka-dom-el
synced 2025-07-01 12:22:15 +02:00
🔤 Examples
This commit is contained in:
342
docs/components/examples/case-studies/interactive-form.js
Normal file
342
docs/components/examples/case-studies/interactive-form.js
Normal file
@ -0,0 +1,342 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
`),
|
||||
);
|
Reference in New Issue
Block a user