diff --git a/src/components/Form.astro b/src/components/Form.astro index 61909a4..ffeee4e 100644 --- a/src/components/Form.astro +++ b/src/components/Form.astro @@ -520,6 +520,50 @@ const formId = `f-${Math.random().toString(36).slice(2)}`; }); } + function enforcePasswordUiRules(formEl) { + let allValid = true; + + formEl.querySelectorAll("[data-pw-root]").forEach((pw) => { + // if the PasswordField marked itself valid, accept it + if (pw.dataset.pwValid === "1") return; + + // otherwise recompute from its declared rules (in case user edited quickly) + const name = pw.getAttribute("data-pw-name"); + const input = pw.querySelector("input[name]"); + const val = (input && input.value) || ""; + + let rules = {}; + try { + rules = JSON.parse( + pw.getAttribute("data-pw-rules") || "{}", + ); + } catch {} + + const ok = + (!rules.lower || /[a-z]/.test(val)) && + (!rules.upper || /[A-Z]/.test(val)) && + (!rules.number || /[0-9]/.test(val)) && + (!rules.symbol || /[^A-Za-z0-9]/.test(val)) && + (!rules.minLength || + val.length >= Number(rules.minLength || 0)); + + if (!ok) { + allValid = false; + // show a field error on the password field + const fieldRoot = + formEl.querySelector( + `[data-field="${CSS.escape(name)}"]`, + ) || pw; + setError( + fieldRoot, + "Password does not meet all requirements.", + ); + } + }); + + return allValid; + } + form.addEventListener("submit", async (e) => { const schema = getSchema(); const onSubmitName = form.dataset.onsubmit; @@ -528,16 +572,19 @@ const formId = `f-${Math.random().toString(36).slice(2)}`; if (schema) { const data = collectData(form); - console.log(data); const result = schema.safeParse(data); - console.log(result); if (!result.success) { showErrors(form, result.error); return; } + setFormError(""); form.querySelectorAll("[data-field]").forEach(clearState); + if (!enforcePasswordUiRules(form)) { + return; // block submit until all enabled rules pass + } + if (!onSubmitName) { form.submit(); return; diff --git a/src/components/PasswordField.astro b/src/components/PasswordField.astro new file mode 100644 index 0000000..6c5e255 --- /dev/null +++ b/src/components/PasswordField.astro @@ -0,0 +1,306 @@ +--- +import type { HTMLAttributes } from "astro/types"; +import { cn } from "../utils/cn"; +import { Icon } from "astro-icon/components"; + +interface Rules { + lower?: boolean; + upper?: boolean; + number?: boolean; + symbol?: boolean; + minLength?: number; // default 8 +} + +interface Props extends HTMLAttributes<"div"> { + id?: string; + name: string; // required (used by form validator) + label?: string; + description?: string; + placeholder?: string; + required?: boolean; + disabled?: boolean; + value?: string; + size?: "sm" | "md" | "lg"; + class?: string; + + showMeter?: boolean; // default true + showChecklist?: boolean; // default true + rules?: Rules; // enable/disable rules + custom minLength + error?: string; // SSR error (form client will overwrite) +} + +const p = Astro.props as Props; + +const fieldId = p.id ?? p.name; +const descId = p.description ? `${fieldId}-desc` : undefined; +const errId = `${fieldId}-err`; + +const sizeKls = + p.size === "sm" + ? "wr:h-10 wr:px-3 wr:text-sm" + : p.size === "lg" + ? "wr:h-12 wr:px-4 wr:text-base" + : "wr:h-11 wr:px-3 wr:text-sm"; + +const inputCls = cn( + "wr:py-2.5 wr:px-4 wr:block wr:w-full wr:rounded-lg wr:border wr:border-gray-200 wr:sm:text-sm", + "focus:wr:border-primary focus:wr:ring-primary/40", + "wr:disabled:opacity-50 wr:disabled:pointer-events-none", + "dark:wr:bg-neutral-900 dark:wr:border-neutral-700 dark:wr:text-neutral-400 dark:wr:placeholder-neutral-500 dark:focus:wr:ring-neutral-600", + sizeKls, + "wr:pr-10", // space for eye button + p.error && "wr:border-danger wr:focus-visible:wr:ring-danger/40", +); + +const containerId = `pf-${Math.random().toString(36).slice(2)}`; + +const rules = { + lower: true, + upper: true, + number: true, + symbol: true, + minLength: 8, + ...(p.rules ?? {}), +}; + +const showMeter = p.showMeter ?? true; +const showChecklist = p.showChecklist ?? true; +--- + +
+ { + p.label && ( + + ) + } + +
+ + + + +
+ + { + showMeter && ( +
+
+
+ ) + } + + { + showChecklist && ( + + ) + } + + { + p.description && ( +

+ {p.description} +

+ ) + } + + + +
+ + diff --git a/src/layouts/ComponentLayout.astro b/src/layouts/ComponentLayout.astro index 494ed88..cad3eb8 100644 --- a/src/layouts/ComponentLayout.astro +++ b/src/layouts/ComponentLayout.astro @@ -38,6 +38,9 @@ import Base from "./Base.astro";
  • Forms 4
  • +
  • + PassForm +
  • Theme
  • diff --git a/src/pages/PassForm.astro b/src/pages/PassForm.astro new file mode 100644 index 0000000..ef3426e --- /dev/null +++ b/src/pages/PassForm.astro @@ -0,0 +1,52 @@ +--- +import Form from "../components/Form.astro"; +import Field from "../components/Field.astro"; +import ComponentLayout from "../layouts/ComponentLayout.astro"; +import PasswordField from "../components/PasswordField.astro"; +--- + + +
    + + + + + + +
    + +