Fixxed bugs Form Validation

This commit is contained in:
2025-08-16 14:29:39 +05:30
parent 34a972ddfd
commit 2cc07951cf
+305 -276
View File
@@ -83,308 +83,337 @@ 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 type="module" define:vars={{ formId, showSuccesInput }}> <script type="module" is:inline define:vars={{ formId, showSuccesInput }}>
const INVALID_KLS = ["!border-danger", "focus-visible:ring-danger/40"]; const start = () => {
const VALID_KLS = ["!border-success", "focus-visible:ring-success/40"]; const INVALID_KLS = ["!border-danger", "focus-visible:ring-danger/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) throw new Error("Form element not found");
function getSchema() { function getSchema() {
const name = form.dataset.schema; const name = form.dataset.schema;
if (!name) return null; if (!name) return null;
const val = window[name]; const val = window[name];
if (!val) { if (!val) {
console.warn("Schema not found:", name); console.warn("Schema not found:", name);
return null; return null;
}
return typeof val === "function" ? val() : val;
}
// Prefer explicit [data-control]; fall back to any input/select/textarea
function fieldControls(root) {
// Prefer explicit data-control, fallback to any control
let ctrls = root.querySelectorAll(
"input[data-control], select[data-control], textarea[data-control]",
);
if (ctrls.length === 0)
ctrls = root.querySelectorAll("input,select,textarea");
const all = Array.from(ctrls);
const radios = all.filter(
(el) => el instanceof HTMLInputElement && el.type === "radio",
);
const checks = all.filter(
(el) => el instanceof HTMLInputElement && el.type === "checkbox",
);
const err = root.querySelector('[id$="-err"]');
const isRadioGroup = radios.length > 1;
const isMultiCheckbox = checks.length > 1;
return {
ctrls: all, // includes the readonly summary input if present
err,
isRadioGroup,
isMultiCheckbox,
checkboxes: checks, // handy for multi-select
};
}
function setError(root, msg) {
const { ctrls, err } = fieldControls(root);
if (!ctrls.length) return;
const first = ctrls[0];
if (msg) {
if (showSuccesInput) first.classList.remove(...VALID_KLS);
first.classList.add(...INVALID_KLS);
first.setAttribute("aria-invalid", "true");
if (err) {
err.textContent = msg;
err.hidden = false;
} }
} else { return typeof val === "function" ? val() : val;
first.classList.remove(...INVALID_KLS); }
if (showSuccesInput) first.classList.add(...VALID_KLS);
first.setAttribute("aria-invalid", "false"); // Prefer explicit [data-control]; fall back to any input/select/textarea
function fieldControls(root) {
// Prefer explicit data-control, fallback to any control
let ctrls = root.querySelectorAll(
"input[data-control], select[data-control], textarea[data-control]",
);
if (ctrls.length === 0)
ctrls = root.querySelectorAll("input,select,textarea");
const all = Array.from(ctrls);
const radios = all.filter(
(el) => el instanceof HTMLInputElement && el.type === "radio",
);
const checks = all.filter(
(el) =>
el instanceof HTMLInputElement && el.type === "checkbox",
);
const err = root.querySelector('[id$="-err"]');
const isRadioGroup = radios.length > 1;
const isMultiCheckbox = checks.length > 1;
return {
ctrls: all, // includes the readonly summary input if present
err,
isRadioGroup,
isMultiCheckbox,
checkboxes: checks, // handy for multi-select
};
}
function setError(root, msg) {
const { ctrls, err } = fieldControls(root);
if (!ctrls.length) return;
const first = ctrls[0];
if (msg) {
if (showSuccesInput) first.classList.remove(...VALID_KLS);
first.classList.add(...INVALID_KLS);
first.setAttribute("aria-invalid", "true");
if (err) {
err.textContent = msg;
err.hidden = false;
}
} else {
first.classList.remove(...INVALID_KLS);
if (showSuccesInput) first.classList.add(...VALID_KLS);
first.setAttribute("aria-invalid", "false");
if (err) {
err.textContent = "";
err.hidden = true;
}
}
}
function clearState(root) {
const { ctrls, err } = fieldControls(root);
ctrls.forEach((c) =>
c.classList.remove(...INVALID_KLS, ...VALID_KLS),
);
ctrls[0]?.removeAttribute("aria-invalid");
if (err) { if (err) {
err.textContent = ""; err.textContent = "";
err.hidden = true; err.hidden = true;
} }
} }
}
function clearState(root) { // Robust data collector (radios, checkboxes, multi-select, files)
const { ctrls, err } = fieldControls(root); function collectData(formEl) {
ctrls.forEach((c) => c.classList.remove(...INVALID_KLS, ...VALID_KLS)); const names = new Set(
ctrls[0]?.removeAttribute("aria-invalid"); Array.from(
if (err) { formEl.querySelectorAll(
err.textContent = ""; "input[name], select[name], textarea[name]",
err.hidden = true; ),
} )
} .map((el) => el.getAttribute("name"))
.filter(Boolean),
// Robust data collector (radios, checkboxes, multi-select, files)
function collectData(formEl) {
const names = new Set(
Array.from(
formEl.querySelectorAll(
"input[name], select[name], textarea[name]",
),
)
.map((el) => el.getAttribute("name"))
.filter(Boolean),
);
const data = {};
for (const name of names) {
const els = Array.from(
formEl.querySelectorAll(`[name="${CSS.escape(name)}"]`),
);
const radios = els.filter(
(el) => el instanceof HTMLInputElement && el.type === "radio",
);
const checks = els.filter(
(el) =>
el instanceof HTMLInputElement && el.type === "checkbox",
); );
const data = {};
if (radios.length) { for (const name of names) {
const chosen = radios.find((el) => el.checked); const els = Array.from(
data[name] = chosen ? chosen.value : ""; formEl.querySelectorAll(`[name="${CSS.escape(name)}"]`),
continue; );
} const radios = els.filter(
(el) =>
el instanceof HTMLInputElement && el.type === "radio",
);
const checks = els.filter(
(el) =>
el instanceof HTMLInputElement &&
el.type === "checkbox",
);
if (checks.length) { if (radios.length) {
if (checks.length > 1) { const chosen = radios.find((el) => el.checked);
data[name] = checks data[name] = chosen ? chosen.value : "";
.filter((el) => el.checked) continue;
.map((el) => el.value || "on");
} else {
data[name] = checks[0].checked;
} }
continue;
}
const el = els[0]; if (checks.length) {
if (checks.length > 1) {
data[name] = checks
.filter((el) => el.checked)
.map((el) => el.value || "on");
} else {
data[name] = checks[0].checked;
}
continue;
}
if (el instanceof HTMLSelectElement && el.multiple) { const el = els[0];
data[name] = Array.from(el.selectedOptions).map((o) => o.value);
} else if (el instanceof HTMLInputElement && el.type === "file") {
data[name] = el.multiple
? Array.from(el.files || [])
: (el.files?.[0] ?? null);
} else {
const fd = new FormData(formEl);
data[name] = fd.get(name);
}
}
return data;
}
function showErrors(formEl, zodError) { if (el instanceof HTMLSelectElement && el.multiple) {
formEl.querySelectorAll("[data-field]").forEach(clearState); data[name] = Array.from(el.selectedOptions).map(
let firstRoot = null; (o) => o.value,
for (const issue of zodError.issues ?? []) { );
const key = } else if (
Array.isArray(issue.path) && issue.path.length
? String(issue.path[0])
: null;
if (!key) continue;
const root = formEl.querySelector(
`[data-field="${CSS.escape(key)}"]`,
);
if (!root) continue;
setError(root, issue.message);
if (!firstRoot) firstRoot = root;
}
if (firstRoot) {
const { ctrls } = fieldControls(firstRoot);
ctrls[0]?.focus();
}
}
function validateField(name) {
const schema = getSchema();
if (!schema || !schema.shape || !schema.shape[name]) return;
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) {
// one-of
const chosen = ctrls.find(
(el) =>
el instanceof HTMLInputElement && el instanceof HTMLInputElement &&
el.type === "radio" && el.type === "file"
el.checked, ) {
); data[name] = el.multiple
value = chosen ? chosen.value : ""; ? Array.from(el.files || [])
} else if (isMultiCheckbox) { : (el.files?.[0] ?? null);
// array<string> for MultiSelect / checkbox groups } else {
value = checkboxes const fd = new FormData(formEl);
.filter((c) => c.checked) data[name] = fd.get(name);
.map((c) => c.value || "on"); }
} else { }
// single control return data;
const ctrl = ctrls[0]; }
if (ctrl instanceof HTMLInputElement && ctrl.type === "checkbox") {
value = ctrl.checked; function showErrors(formEl, zodError) {
} else if ( formEl.querySelectorAll("[data-field]").forEach(clearState);
ctrl instanceof HTMLInputElement && let firstRoot = null;
ctrl.type === "file" for (const issue of zodError.issues ?? []) {
) { const key =
value = ctrl.multiple Array.isArray(issue.path) && issue.path.length
? Array.from(ctrl.files || []) ? String(issue.path[0])
: (ctrl.files?.[0] ?? null); : null;
} else if (ctrl instanceof HTMLSelectElement && ctrl.multiple) { if (!key) continue;
value = Array.from(ctrl.selectedOptions).map((o) => o.value); const root = formEl.querySelector(
} else { `[data-field="${CSS.escape(key)}"]`,
value = ctrl.value; );
if (!root) continue;
setError(root, issue.message);
if (!firstRoot) firstRoot = root;
}
if (firstRoot) {
const { ctrls } = fieldControls(firstRoot);
ctrls[0]?.focus();
} }
} }
const res = schema.shape[name].safeParse(value); function validateField(name) {
setError( const schema = getSchema();
root, if (!schema || !schema.shape || !schema.shape[name]) return;
res.success ? "" : res.error.issues[0]?.message || "Invalid value",
);
}
const validateOn = (form.dataset.validateon || "input,blur,change") const root = form.querySelector(
.split(",") `[data-field="${CSS.escape(name)}"]`,
.map((s) => s.trim()) );
.filter(Boolean);
if (validateOn.includes("input")) {
form.addEventListener("input", (e) => {
const root = e.target?.closest?.("[data-field]");
if (!root) return; if (!root) return;
const name = root.getAttribute("data-field");
if (name) validateField(name); const { ctrls, isRadioGroup, isMultiCheckbox, checkboxes } =
}); fieldControls(root);
} if (!ctrls.length) return;
if (validateOn.includes("blur")) {
form.addEventListener( let value;
"blur",
(e) => { 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;
}
}
const res = schema.shape[name].safeParse(value);
setError(
root,
res.success
? ""
: res.error.issues[0]?.message || "Invalid value",
);
}
const validateOn = (form.dataset.validateon || "input,blur,change")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
if (validateOn.includes("input")) {
form.addEventListener("input", (e) => {
const root = e.target?.closest?.("[data-field]"); const root = e.target?.closest?.("[data-field]");
if (!root) return; if (!root) return;
const name = root.getAttribute("data-field"); const name = root.getAttribute("data-field");
if (name) validateField(name); if (name) validateField(name);
}, });
true, }
); if (validateOn.includes("blur")) {
} form.addEventListener(
if (validateOn.includes("change")) { "blur",
form.addEventListener("change", (e) => { (e) => {
const root = e.target?.closest?.("[data-field]"); const root = e.target?.closest?.("[data-field]");
if (!root) return; if (!root) return;
const name = root.getAttribute("data-field"); const name = root.getAttribute("data-field");
if (name) validateField(name); if (name) validateField(name);
}); },
} true,
);
form.addEventListener("submit", async (e) => { }
const schema = getSchema(); if (validateOn.includes("change")) {
const onSubmitName = form.dataset.onsubmit; form.addEventListener("change", (e) => {
const root = e.target?.closest?.("[data-field]");
if (!schema && !onSubmitName) return; // native submit if (!root) return;
const name = root.getAttribute("data-field");
e.preventDefault(); if (name) validateField(name);
});
if (schema) {
const data = collectData(form);
const result = schema.safeParse(data);
if (!result.success) {
showErrors(form, result.error);
return;
}
form.querySelectorAll("[data-field]").forEach(clearState);
if (!onSubmitName) {
form.submit(); // native submit after validation
return;
}
const btn = form.querySelector('button[type="submit"]');
btn && (btn.disabled = true);
try {
const fn = window[onSubmitName];
const res =
typeof fn === "function"
? await fn(result.data, form)
: null;
if (res !== false && form.dataset.reset) form.reset();
} catch (err) {
console.error("onSubmit error:", err);
} finally {
btn && (btn.disabled = false);
}
} else if (onSubmitName) {
const btn = form.querySelector('button[type="submit"]');
btn && (btn.disabled = true);
try {
const data = collectData(form);
const fn = window[onSubmitName];
const res =
typeof fn === "function" ? await fn(data, form) : null;
if (res !== false && form.dataset.reset) form.reset();
} finally {
btn && (btn.disabled = false);
}
} }
});
form.querySelectorAll("textarea[data-initial]").forEach((t) => { form.addEventListener("submit", async (e) => {
t.value = t.getAttribute("data-initial") || ""; const schema = getSchema();
t.removeAttribute("data-initial"); const onSubmitName = form.dataset.onsubmit;
});
if (!schema && !onSubmitName) return; // native submit
e.preventDefault();
if (schema) {
const data = collectData(form);
const result = schema.safeParse(data);
if (!result.success) {
showErrors(form, result.error);
return;
}
form.querySelectorAll("[data-field]").forEach(clearState);
if (!onSubmitName) {
form.submit(); // native submit after validation
return;
}
const btn = form.querySelector('button[type="submit"]');
btn && (btn.disabled = true);
try {
const fn = window[onSubmitName];
const res =
typeof fn === "function"
? await fn(result.data, form)
: null;
if (res !== false && form.dataset.reset) form.reset();
} catch (err) {
console.error("onSubmit error:", err);
} finally {
btn && (btn.disabled = false);
}
} else if (onSubmitName) {
const btn = form.querySelector('button[type="submit"]');
btn && (btn.disabled = true);
try {
const data = collectData(form);
const fn = window[onSubmitName];
const res =
typeof fn === "function" ? await fn(data, form) : null;
if (res !== false && form.dataset.reset) form.reset();
} finally {
btn && (btn.disabled = false);
}
}
});
form.querySelectorAll("textarea[data-initial]").forEach((t) => {
t.value = t.getAttribute("data-initial") || "";
t.removeAttribute("data-initial");
});
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", start, { once: true });
} else {
start();
}
// For Astro client routing / view transitions
document.addEventListener("astro:page-load", start);
</script> </script>