Skip to content

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.

  • 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
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")!);

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.

  • 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