/** * 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=${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 === "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 "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"); } } } 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) { var redirect = form.getAttribute("data-redirect") || (data && data.redirect); if (redirect) { // Prefer client-side navigation (no full reload) when it is available. if (window.__wrnexusNavigate) window.__wrnexusNavigate(redirect); else location.assign(redirect); return; } var box = form.querySelector("[data-success]"); if (box) { box.textContent = box.getAttribute("data-success") || "Success"; box.hidden = false; } form.reset(); form.dispatchEvent(new CustomEvent("wire:success", { detail: data, bubbles: true })); } 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]"); 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) { return res.json().catch(function () { return {}; }).then(function (data) { return { res: res, data: data }; }); }) .then(function (r) { if (r.res.ok) { onSuccess(form, r.data); return; } // Surface server-side field errors (e.g. "email already taken"). var errors = r.data && r.data.errors; if (errors) Object.keys(errors).forEach(function (f) { showError(form, f, errors[f]); }); form.dispatchEvent(new CustomEvent("wire:error", { detail: r.data, bubbles: true })); }) .catch(function () { form.dispatchEvent(new CustomEvent("wire:error", { detail: { network: true }, bubbles: true })); }) .then(function () { btns.forEach(function (b) { b.disabled = false; }); }); } function bind(form) { if (form.__wireValidateBound) return; form.__wireValidateBound = 1; var name = form.getAttribute("data-schema"); var schema = (window.__wireSchemas || {})[name]; if (!schema) return; // 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(); if (validateForm(form, schema) > 0) return; // client-invalid: stay put, errors shown submitForm(form, schema); }); form.addEventListener("blur", function (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)); } }, true); } function init(root) { (root || document).querySelectorAll("form[data-schema]").forEach(bind); } window.__wireValidate = { init: init }; if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", function () { init(document); }); else init(document); })(); `.trim();