/** * Client-side validation. `renderSchemasScript` bakes the discovered schema * descriptors into `window.__wireSchemas`; `VALIDATE_RUNTIME` is a generic, * eval-free validator that reads them and validates every `form[data-schema]` * on submit and blur, writing messages into `[data-error=""]` elements. * The rule logic mirrors `checkField`/`applyRule` in index.ts. */ import type { SchemaDescriptor } from "./index.ts"; /** `window.__wireSchemas = { name: descriptor, ... }` for the client validator. */ export function renderSchemasScript(descriptors: Record): string { return `window.__wireSchemas=Object.assign(window.__wireSchemas||{},${JSON.stringify(descriptors)});`; } export const VALIDATE_RUNTIME = String.raw` (function () { var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; var URL_RE = /^https?:\/\/[^\s/$.?#][^\s]*$/i; var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; function applyRule(type, r, value) { if (r.kind === "min") { if (type === "string") return String(value).length < r.n ? (r.message || ("Must be at least " + r.n + " characters")) : null; return value < r.n ? (r.message || ("Must be at least " + r.n)) : null; } if (r.kind === "max") { if (type === "string") return String(value).length > r.n ? (r.message || ("Must be at most " + r.n + " characters")) : null; return value > r.n ? (r.message || ("Must be at most " + r.n)) : null; } if (r.kind === "length") return String(value).length !== r.n ? (r.message || ("Must be exactly " + r.n + " characters")) : null; if (r.kind === "email") return EMAIL.test(String(value)) ? null : (r.message || "Must be a valid email"); if (r.kind === "url") return URL_RE.test(String(value)) ? null : (r.message || "Must be a valid URL"); if (r.kind === "uuid") return UUID_RE.test(String(value)) ? null : (r.message || "Must be a valid UUID"); if (r.kind === "date") return isNaN(Date.parse(String(value))) ? (r.message || "Must be a valid date") : null; if (r.kind === "oneOf") return r.values.indexOf(value) !== -1 ? null : (r.message || ("Must be one of: " + r.values.join(", "))); if (r.kind === "pattern") { try { return new RegExp(r.source, r.flags || "").test(String(value)) ? null : (r.message || "Invalid format"); } catch (e) { return null; } } if (r.kind === "integer") return Number.isInteger(value) ? null : (r.message || "Must be a whole number"); return null; } function checkField(desc, raw) { if (desc.type === "unknown") { var missing = raw === undefined || raw === null || raw === ""; return (missing && !desc.optional) ? (desc.requiredMessage || "Required") : null; } if (desc.type === "boolean") { var b = raw === true || raw === "true" || raw === "on"; return (!desc.optional && !b) ? (desc.requiredMessage || "Required") : null; } var pre = desc.trim && typeof raw === "string" ? raw.trim() : raw; var empty = pre === undefined || pre === null || pre === ""; if (empty) return desc.optional ? null : (desc.requiredMessage || "Required"); var value; if (desc.type === "number") { value = Number(pre); if (isNaN(value)) return desc.typeMessage || "Must be a number"; } else value = String(pre); for (var i = 0; i < desc.rules.length; i++) { var err = applyRule(desc.type, desc.rules[i], value); if (err) return err; } return null; } function rawValue(form, name) { var el = form.elements[name]; if (!el) return undefined; return el.type === "checkbox" ? el.checked : el.value; } function showError(form, name, err) { var box = form.querySelector('[data-error="' + name + '"]'); if (box) box.textContent = err || ""; var el = form.elements[name]; if (el && el.setAttribute) { if (err) { el.setAttribute("aria-invalid", "true"); if (el.classList) el.classList.add("wire-invalid"); } else { el.removeAttribute("aria-invalid"); if (el.classList) el.classList.remove("wire-invalid"); } var field = el.closest && el.closest(".wire-next--field, .wire-next--choice-field"); if (field) field.setAttribute("data-invalid", err ? "true" : "false"); } } function showFormError(form, message) { var box = form.querySelector('[data-error="_form"]'); if (!box) return; box.textContent = message || ""; box.hidden = !message; if (box.classList) box.classList.toggle("hidden", !message); } function hideSuccess(form) { var box = form.querySelector("[data-success]"); if (!box) return; box.hidden = true; if (box.classList) box.classList.add("hidden"); } function clearErrors(form, schema) { Object.keys(schema.fields).forEach(function (name) { showError(form, name, ""); }); showFormError(form, ""); } function validateForm(form, schema) { var errors = 0; Object.keys(schema.fields).forEach(function (name) { var err = checkField(schema.fields[name], rawValue(form, name)); if (err) errors++; showError(form, name, err); }); return errors; } function collect(form) { var out = {}; for (var i = 0; i < form.elements.length; i++) { var el = form.elements[i]; if (!el.name) continue; if (el.type === "checkbox") out[el.name] = el.checked; else if (el.type === "radio") { if (el.checked) out[el.name] = el.value; } else out[el.name] = el.value; } return out; } function onSuccess(form, data, responseRedirect) { showFormError(form, ""); form.dispatchEvent(new CustomEvent("wire:success", { detail: data, bubbles: true })); var redirect = responseRedirect || form.getAttribute("data-redirect") || (data && data.redirect); if (redirect) { // Prefer client-side navigation for same-origin application redirects. try { var target = new URL(redirect, location.href); if (target.origin === location.origin && window.__wrnexusNavigate) { window.__wrnexusNavigate(target.pathname + target.search + target.hash); } else location.assign(target.href); } catch (e) { location.assign(redirect); } return; } var box = form.querySelector("[data-success]"); if (box) { box.textContent = box.getAttribute("data-success") || "Success"; box.hidden = false; if (box.classList) box.classList.remove("hidden"); } form.reset(); } function csrfHeader() { var m = document.cookie.match(/(?:^|;\s*)wire-csrf=([^;]+)/); return m ? { "x-csrf-token": decodeURIComponent(m[1]) } : {}; } function submitForm(form, schema) { var method = (form.getAttribute("method") || "post").toUpperCase(); var action = form.getAttribute("action") || location.pathname; var btns = form.querySelectorAll("[type=submit]"); clearErrors(form, schema); hideSuccess(form); btns.forEach(function (b) { b.disabled = true; }); var headers = { "content-type": "application/json", accept: "application/json" }; var csrf = csrfHeader(); for (var k in csrf) headers[k] = csrf[k]; fetch(action, { method: method, headers: headers, credentials: "same-origin", body: JSON.stringify(collect(form)), }) .then(function (res) { var responseRedirect = res.redirected && res.url ? res.url : ""; return res.json().catch(function () { return {}; }).then(function (data) { return { res: res, data: data, responseRedirect: responseRedirect }; }); }) .then(function (r) { if (r.res.ok && (!r.data || r.data.ok !== false)) { onSuccess(form, r.data, r.responseRedirect); return; } // Surface server-side field errors and a form-level API message. var errors = r.data && r.data.errors; if (errors) Object.keys(errors).forEach(function (f) { showError(form, f, errors[f]); }); var message = r.data && (r.data.message || r.data.error); showFormError(form, message || (errors ? "" : ("Request failed (" + r.res.status + ")"))); form.dispatchEvent(new CustomEvent("wire:error", { detail: r.data, bubbles: true })); }) .catch(function (error) { var detail = { network: true, message: error && error.message ? error.message : "Network request failed" }; showFormError(form, detail.message); form.dispatchEvent(new CustomEvent("wire:error", { detail: detail, bubbles: true })); }) .then(function () { btns.forEach(function (b) { b.disabled = false; }); }); } function runSubmitGuards(form, event) { var guards = form.__wireSubmitGuards || []; for (var index = 0; index < guards.length; index += 1) { if (guards[index](event) === false) return false; } return true; } function bind(form) { if (form.__wireValidateBound) return; var name = form.getAttribute("data-schema"); var schema = (window.__wireSchemas || {})[name]; // Package runtimes may register schemas later in the same page load. // Do not report an error during the first scan. if (!schema) { form.__wireValidatePending = 1; return; } form.__wireValidatePending = 0; form.__wireValidateMissingWarned = 0; form.__wireValidateBound = 1; form.__wireValidateMissingWarned = 0; // Schema-backed forms use WRNexus messages instead of the browser's // non-themeable native validation bubbles. form.noValidate = true; form.setAttribute("novalidate", ""); form.addEventListener("submit", function (e) { e.preventDefault(); clearErrors(form, schema); hideSuccess(form); if (validateForm(form, schema) > 0) return; // client-invalid: stay put, errors shown if (!runSubmitGuards(form, e)) return; submitForm(form, schema); }); function validateTarget(e) { var t = e.target; if (t && t.name && schema.fields[t.name]) { showError(form, t.name, checkField(schema.fields[t.name], t.type === "checkbox" ? t.checked : t.value)); } } form.addEventListener("input", validateTarget, true); form.addEventListener("change", validateTarget, true); form.addEventListener("blur", validateTarget, true); } function registerSchemas(descriptors, root) { window.__wireSchemas = window.__wireSchemas || {}; Object.keys(descriptors || {}).forEach( function (name) { window.__wireSchemas[name] = descriptors[name]; } ); init(root || document); } function init(root) { (root || document).querySelectorAll("form[data-schema]").forEach(bind); } window.__wireValidate = { init: init, registerSchemas: registerSchemas }; if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", function () { init(document); }); else init(document); document.addEventListener( "submit", function (event) { var form = event.target; if ( !form || !form.matches || !form.matches("form[data-schema]") ) { return; } if (form.__wireValidateBound) { return; } var name = form.getAttribute("data-schema"); var schema = (window.__wireSchemas || {})[name]; if (schema) { bind(form); return; } event.preventDefault(); showFormError( form, "Validation could not be loaded. Please refresh and try again." ); if ( !form.__wireValidateMissingWarned && window.console && console.error ) { form.__wireValidateMissingWarned = 1; console.error( "[wrnexus:validation] Schema '" + name + "' is unavailable:", form ); } }, true ); })(); `.trim();