Fixxed bugs Form Validation

This commit is contained in:
2025-08-16 15:08:50 +05:30
parent e55ecf8882
commit 3ed6dabff5
2 changed files with 128 additions and 143 deletions
+121 -136
View File
@@ -83,13 +83,14 @@ const formId = `f-${Math.random().toString(36).slice(2)}`;
</form> </form>
<!-- Client validation & submit hook (no zod import here) --> <!-- Client validation & submit hook (no zod import here) -->
<script is:inline define:vars={{ formId, showSuccesInput }}> <script type="module" is:inline define:vars={{ formId, showSuccesInput }}>
const start = () => { const start = () => {
const INVALID_KLS = ["!border-danger", "focus-visible:ring-danger/40"]; const INVALID_KLS = ["!border-danger", "focus-visible:ring-danger/40"];
const VALID_KLS = ["!border-success", "focus-visible:ring-success/40"]; const VALID_KLS = ["!border-success", "focus-visible:ring-success/40"];
const form = document.getElementById(formId); const form = document.getElementById(formId);
if (!form) throw new Error("Form element not found"); if (!form || form.dataset.__afInit) return;
form.dataset.__afInit = "1";
function getSchema() { function getSchema() {
const name = form.dataset.schema; const name = form.dataset.schema;
@@ -102,15 +103,45 @@ const formId = `f-${Math.random().toString(36).slice(2)}`;
return typeof val === "function" ? val() : val; return typeof val === "function" ? val() : val;
} }
// Prefer explicit [data-control]; fall back to any input/select/textarea function unwrapToObject(schema) {
let cur = schema;
while (cur && cur._def) {
const t = cur._def.typeName;
if (t === "ZodObject") return cur;
if (t === "ZodEffects") {
cur = cur._def.schema;
continue;
}
if (
t === "ZodOptional" ||
t === "ZodNullable" ||
t === "ZodDefault" ||
t === "ZodBranded" ||
t === "ZodCatch"
) {
cur = cur._def.innerType;
continue;
}
if (t === "ZodPipeline") {
cur = cur._def.out;
continue;
}
break;
}
return null;
}
function getObjectShape(obj) {
if (!obj) return null;
const s = obj.shape ?? obj._def?.shape;
return typeof s === "function" ? s() : s;
}
function fieldControls(root) { function fieldControls(root) {
// Prefer explicit data-control, fallback to any control
let ctrls = root.querySelectorAll( let ctrls = root.querySelectorAll(
"input[data-control],select[data-control],textarea[data-control]", "input[data-control],select[data-control],textarea[data-control]",
); );
if (ctrls.length === 0) if (ctrls.length === 0)
ctrls = root.querySelectorAll("input,select,textarea"); ctrls = root.querySelectorAll("input,select,textarea");
const all = Array.from(ctrls); const all = Array.from(ctrls);
const radios = all.filter( const radios = all.filter(
(el) => el instanceof HTMLInputElement && el.type === "radio", (el) => el instanceof HTMLInputElement && el.type === "radio",
@@ -119,17 +150,13 @@ const formId = `f-${Math.random().toString(36).slice(2)}`;
(el) => (el) =>
el instanceof HTMLInputElement && el.type === "checkbox", el instanceof HTMLInputElement && el.type === "checkbox",
); );
const err = root.querySelector('[id$="-err"]'); const err = root.querySelector('[id$="-err"]');
const isRadioGroup = radios.length > 1;
const isMultiCheckbox = checks.length > 1;
return { return {
ctrls: all, // includes the readonly summary input if present ctrls: all,
err, err,
isRadioGroup, isRadioGroup: radios.length > 1,
isMultiCheckbox, isMultiCheckbox: checks.length > 1,
checkboxes: checks, // handy for multi-select checkboxes: checks,
}; };
} }
@@ -137,7 +164,6 @@ const formId = `f-${Math.random().toString(36).slice(2)}`;
const { ctrls, err } = fieldControls(root); const { ctrls, err } = fieldControls(root);
if (!ctrls.length) return; if (!ctrls.length) return;
const first = ctrls[0]; const first = ctrls[0];
if (msg) { if (msg) {
if (showSuccesInput) first.classList.remove(...VALID_KLS); if (showSuccesInput) first.classList.remove(...VALID_KLS);
first.classList.add(...INVALID_KLS); first.classList.add(...INVALID_KLS);
@@ -156,7 +182,6 @@ const formId = `f-${Math.random().toString(36).slice(2)}`;
} }
} }
} }
function clearState(root) { function clearState(root) {
const { ctrls, err } = fieldControls(root); const { ctrls, err } = fieldControls(root);
ctrls.forEach((c) => ctrls.forEach((c) =>
@@ -169,7 +194,64 @@ const formId = `f-${Math.random().toString(36).slice(2)}`;
} }
} }
// Robust data collector (radios, checkboxes, multi-select, files) function validateField(name) {
const root = form.querySelector(
`[data-field="${CSS.escape(name)}"]`,
);
if (!root) return;
const { ctrls, isRadioGroup, isMultiCheckbox, checkboxes } =
fieldControls(root);
if (!ctrls.length) return;
let value;
if (isRadioGroup) {
const chosen = ctrls.find(
(el) =>
el instanceof HTMLInputElement &&
el.type === "radio" &&
el.checked,
);
value = chosen ? chosen.value : "";
} else if (isMultiCheckbox) {
value = checkboxes
.filter((c) => c.checked)
.map((c) => c.value || "on");
} else {
const ctrl = ctrls[0];
if (
ctrl instanceof HTMLInputElement &&
ctrl.type === "checkbox"
)
value = ctrl.checked;
else if (
ctrl instanceof HTMLInputElement &&
ctrl.type === "file"
)
value = ctrl.multiple
? Array.from(ctrl.files || [])
: (ctrl.files?.[0] ?? null);
else if (ctrl instanceof HTMLSelectElement && ctrl.multiple)
value = Array.from(ctrl.selectedOptions).map(
(o) => o.value,
);
else value = ctrl.value;
}
const schema = getSchema();
const obj = unwrapToObject(schema);
const shape = getObjectShape(obj);
const fieldSchema = shape?.[name];
if (!fieldSchema) return;
const res = fieldSchema.safeParse(value);
setError(
root,
res.success
? ""
: res.error.issues[0]?.message || "Invalid value",
);
}
function collectData(formEl) { function collectData(formEl) {
const names = new Set( const names = new Set(
Array.from( Array.from(
@@ -181,7 +263,6 @@ const formId = `f-${Math.random().toString(36).slice(2)}`;
.filter(Boolean), .filter(Boolean),
); );
const data = {}; const data = {};
for (const name of names) { for (const name of names) {
const els = Array.from( const els = Array.from(
formEl.querySelectorAll(`[name="${CSS.escape(name)}"]`), formEl.querySelectorAll(`[name="${CSS.escape(name)}"]`),
@@ -195,38 +276,30 @@ const formId = `f-${Math.random().toString(36).slice(2)}`;
el instanceof HTMLInputElement && el instanceof HTMLInputElement &&
el.type === "checkbox", el.type === "checkbox",
); );
if (radios.length) { if (radios.length) {
const chosen = radios.find((el) => el.checked); const chosen = radios.find((el) => el.checked);
data[name] = chosen ? chosen.value : ""; data[name] = chosen ? chosen.value : "";
continue; continue;
} }
if (checks.length) { if (checks.length) {
if (checks.length > 1) { data[name] =
data[name] = checks checks.length > 1
? checks
.filter((el) => el.checked) .filter((el) => el.checked)
.map((el) => el.value || "on"); .map((el) => el.value || "on")
} else { : checks[0].checked;
data[name] = checks[0].checked;
}
continue; continue;
} }
const el = els[0]; const el = els[0];
if (el instanceof HTMLSelectElement && el.multiple)
if (el instanceof HTMLSelectElement && el.multiple) {
data[name] = Array.from(el.selectedOptions).map( data[name] = Array.from(el.selectedOptions).map(
(o) => o.value, (o) => o.value,
); );
} else if ( else if (el instanceof HTMLInputElement && el.type === "file")
el instanceof HTMLInputElement &&
el.type === "file"
) {
data[name] = el.multiple data[name] = el.multiple
? Array.from(el.files || []) ? Array.from(el.files || [])
: (el.files?.[0] ?? null); : (el.files?.[0] ?? null);
} else { else {
const fd = new FormData(formEl); const fd = new FormData(formEl);
data[name] = fd.get(name); data[name] = fd.get(name);
} }
@@ -250,126 +323,41 @@ const formId = `f-${Math.random().toString(36).slice(2)}`;
setError(root, issue.message); setError(root, issue.message);
if (!firstRoot) firstRoot = root; if (!firstRoot) firstRoot = root;
} }
if (firstRoot) { if (firstRoot) fieldControls(firstRoot).ctrls[0]?.focus();
const { ctrls } = fieldControls(firstRoot);
ctrls[0]?.focus();
}
}
function validateField(name) {
console.log(name);
const schema = getSchema();
console.log(schema);
console.log(schema.shape);
console.log(schema.shape[name]);
if (!schema || !schema.shape || !schema.shape[name]) return;
const root = form.querySelector(
`[data-field="${CSS.escape(name)}"]`,
);
console.log(root);
if (!root) return;
const { ctrls, isRadioGroup, isMultiCheckbox, checkboxes } =
fieldControls(root);
if (!ctrls.length) return;
let value;
if (isRadioGroup) {
// one-of
const chosen = ctrls.find(
(el) =>
el instanceof HTMLInputElement &&
el.type === "radio" &&
el.checked,
);
value = chosen ? chosen.value : "";
} else if (isMultiCheckbox) {
// array<string> for MultiSelect / checkbox groups
value = checkboxes
.filter((c) => c.checked)
.map((c) => c.value || "on");
} else {
// single control
const ctrl = ctrls[0];
if (
ctrl instanceof HTMLInputElement &&
ctrl.type === "checkbox"
) {
value = ctrl.checked;
} else if (
ctrl instanceof HTMLInputElement &&
ctrl.type === "file"
) {
value = ctrl.multiple
? Array.from(ctrl.files || [])
: (ctrl.files?.[0] ?? null);
} else if (ctrl instanceof HTMLSelectElement && ctrl.multiple) {
value = Array.from(ctrl.selectedOptions).map(
(o) => o.value,
);
} else {
value = ctrl.value;
}
}
console.log(value);
const res = schema.shape[name].safeParse(value);
console.log(res);
setError(
root,
res.success
? ""
: res.error.issues[0]?.message || "Invalid value",
);
} }
const validateOn = (form.dataset.validateon || "input,blur,change") const validateOn = (form.dataset.validateon || "input,blur,change")
.split(",") .split(",")
.map((s) => s.trim()) .map((s) => s.trim())
.filter(Boolean); .filter(Boolean);
if (validateOn.includes("input"))
if (validateOn.includes("input")) {
form.addEventListener("input", (e) => { form.addEventListener("input", (e) => {
const root = e.target?.closest?.("[data-field]"); const r = e.target?.closest?.("[data-field]");
if (!root) return; const n = r?.getAttribute("data-field");
const name = root.getAttribute("data-field"); if (n) validateField(n);
if (name) validateField(name);
}); });
} if (validateOn.includes("blur"))
if (validateOn.includes("blur")) {
form.addEventListener( form.addEventListener(
"blur", "blur",
(e) => { (e) => {
const root = e.target?.closest?.("[data-field]"); const r = e.target?.closest?.("[data-field]");
if (!root) return; const n = r?.getAttribute("data-field");
const name = root.getAttribute("data-field"); if (n) validateField(n);
if (name) validateField(name);
}, },
true, true,
); );
} if (validateOn.includes("change"))
if (validateOn.includes("change")) {
form.addEventListener("change", (e) => { form.addEventListener("change", (e) => {
const root = e.target?.closest?.("[data-field]"); const r = e.target?.closest?.("[data-field]");
if (!root) return; const n = r?.getAttribute("data-field");
const name = root.getAttribute("data-field"); if (n) validateField(n);
if (name) validateField(name);
}); });
}
form.addEventListener("submit", async (e) => { form.addEventListener("submit", async (e) => {
const schema = getSchema(); const schema = getSchema();
const onSubmitName = form.dataset.onsubmit; const onSubmitName = form.dataset.onsubmit;
if (!schema && !onSubmitName) return;
if (!schema && !onSubmitName) return; // native submit
e.preventDefault(); e.preventDefault();
if (schema) { if (schema) {
const data = collectData(form); const data = collectData(form);
const result = schema.safeParse(data); const result = schema.safeParse(data);
@@ -378,12 +366,10 @@ const formId = `f-${Math.random().toString(36).slice(2)}`;
return; return;
} }
form.querySelectorAll("[data-field]").forEach(clearState); form.querySelectorAll("[data-field]").forEach(clearState);
if (!onSubmitName) { if (!onSubmitName) {
form.submit(); // native submit after validation form.submit();
return; return;
} }
const btn = form.querySelector('button[type="submit"]'); const btn = form.querySelector('button[type="submit"]');
btn && (btn.disabled = true); btn && (btn.disabled = true);
try { try {
@@ -424,6 +410,5 @@ const formId = `f-${Math.random().toString(36).slice(2)}`;
} else { } else {
start(); start();
} }
// For Astro client routing / view transitions
document.addEventListener("astro:page-load", start); document.addEventListener("astro:page-load", start);
</script> </script>
+4 -4
View File
@@ -9,8 +9,8 @@ import Base from "../layouts/Base.astro";
title="Create Project" title="Create Project"
description="Fields marked * are required." description="Fields marked * are required."
columns={2} columns={2}
schemaName="projectSchema" schemaName="schema"
onSubmit="handleProjectSubmit" onSubmit="submit"
submitLabel="Create Project" submitLabel="Create Project"
submitIcon="mdi:check" submitIcon="mdi:check"
resetOnSubmit resetOnSubmit
@@ -74,7 +74,7 @@ import Base from "../layouts/Base.astro";
<script> <script>
import { z } from "zod"; import { z } from "zod";
window.projectSchema = z.object({ window.schema = z.object({
name: z.string().min(3, "Please enter at least 3 characters."), name: z.string().min(3, "Please enter at least 3 characters."),
email: z.string().email("Enter a valid email address."), email: z.string().email("Enter a valid email address."),
teamSize: z.coerce teamSize: z.coerce
@@ -95,7 +95,7 @@ import Base from "../layouts/Base.astro";
}), }),
}); });
window.handleProjectSubmit = async (data: any, form: any) => { window.submit = async (data: any, form: any) => {
console.log("Validated data:", data); console.log("Validated data:", data);
}; };
</script> </script>