Form Validation
A registration form with email, password, and confirm password fields. Validation runs reactively — errors appear as the user types, and the submit button enables only when everything is valid.
What You’ll Build
Section titled “What You’ll Build”- Email validation (must contain @)
- Password validation (minimum 8 characters, must contain a number)
- Confirm password (must match)
- Per-field error messages that appear after the user starts typing
- Submit button that’s disabled until all fields are valid
Full Source
Section titled “Full Source”import { signal, computed, component, form, div, label, input, button, p, when, mount,} from "@whisq/core";
const RegistrationForm = component(() => { const email = signal(""); const password = signal(""); const confirm = signal(""); const submitted = signal(false);
// Validation rules const emailValid = computed(() => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value)); const passwordLong = computed(() => password.value.length >= 8); const passwordHasNumber = computed(() => /\d/.test(password.value)); const passwordValid = computed(() => passwordLong.value && passwordHasNumber.value); const confirmValid = computed(() => confirm.value === password.value); const formValid = computed(() => emailValid.value && passwordValid.value && confirmValid.value);
// Only show errors after user has typed something const emailTouched = computed(() => email.value.length > 0); const passwordTouched = computed(() => password.value.length > 0); const confirmTouched = computed(() => confirm.value.length > 0);
const handleSubmit = (e: Event) => { e.preventDefault(); if (!formValid.value) return; submitted.value = true; console.log({ email: email.value, password: password.value }); };
return form({ onsubmit: handleSubmit }, // Email div({ class: "field" }, label("Email"), input({ type: "email", placeholder: "you@example.com", value: () => email.value, oninput: (e) => email.value = e.target.value, }), when(() => emailTouched.value && !emailValid.value, () => p({ class: "error" }, "Enter a valid email address"), ), ),
// Password div({ class: "field" }, label("Password"), input({ type: "password", placeholder: "At least 8 characters with a number", value: () => password.value, oninput: (e) => password.value = e.target.value, }), when(() => passwordTouched.value && !passwordLong.value, () => p({ class: "error" }, "Password must be at least 8 characters"), ), when(() => passwordTouched.value && passwordLong.value && !passwordHasNumber.value, () => p({ class: "error" }, "Password must contain at least one number"), ), ),
// Confirm Password div({ class: "field" }, label("Confirm Password"), input({ type: "password", placeholder: "Repeat your password", value: () => confirm.value, oninput: (e) => confirm.value = e.target.value, }), when(() => confirmTouched.value && !confirmValid.value, () => p({ class: "error" }, "Passwords do not match"), ), ),
// Submit button({ disabled: () => !formValid.value, }, "Create Account"),
when(() => submitted.value, () => p({ class: "success" }, "Account created!"), ), );});
mount(RegistrationForm({}), document.getElementById("app")!);How It Works
Section titled “How It Works”1. Validation rules as computed values
const emailValid = computed(() => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value));const passwordValid = computed(() => passwordLong.value && passwordHasNumber.value);const formValid = computed(() => emailValid.value && passwordValid.value && confirmValid.value);Each rule is a computed() that re-evaluates automatically when its dependencies change. formValid composes all individual rules.
2. Touched state for UX
const emailTouched = computed(() => email.value.length > 0);Errors only appear after the user starts typing. This prevents a wall of error messages on initial load.
3. Conditional error display
when(() => emailTouched.value && !emailValid.value, () => p({ class: "error" }, "Enter a valid email address"),),when() renders the error only when both conditions are true: the field has been touched AND the value is invalid.
4. Reactive button state
button({ disabled: () => !formValid.value }, "Create Account")The button enables automatically when all validation passes.
Key Concepts
Section titled “Key Concepts”- computed() — derive validation state reactively from signals
- Composable validation — small rules combine into
formValid - Touched state — track whether the user has interacted with a field
- when() — show/hide error messages based on reactive conditions
- Reactive disabled — button enables itself when form becomes valid
Next Steps
Section titled “Next Steps”- Forms Guide — More form patterns
- Todo App — Full CRUD example
- Testing — Test form components