Repository: Rezact/Rezact
Pull request #19 (2023-10-01 19:19:35)

Add form validation example and documentation


Patch:
diff --git a/FormValidation.md b/FormValidation.md
new file mode 100644
index 0000000..1e8fa07
--- /dev/null
+++ b/FormValidation.md
@@ -0,0 +1,303 @@
+# Guide to Using the Form Validation Library
+
+## Table of Contents
+
+1. [Import](#import)
+2. [Basic Setup](#basic-setup)
+   - [Example](#example)
+3. [Options](#options)
+4. [Custom Error Messages](#custom-error-messages)
+   - [Usage](#usage)
+   - [Available Custom Error Message Keys](#available-custom-error-message-keys)
+   - [Example](#example-1)
+5. [Masking Input](#masking-input)
+   - [Practical Usage](#practical-usage)
+6. [Custom Validation](#custom-validation)
+   - [Example](#example-2)
+7. [Displaying Validation Errors with `errorElm`](#displaying-validation-errors-with-errorElm)
+   - [Error Element (`errorElm`)](#1-error-element-errorElm)
+   - [Manipulating `errorElm` Display](#2-manipulating-errorElm-display)
+   - [Custom Error Messages](#3-custom-error-messages)
+   - [Styling and Visual Feedback](#4-styling-and-visual-feedback)
+   - [Validation Triggering and Message Clearance](#5-validation-triggering-and-message-clearance)
+   - [Example](#example-3)
+8. [Full Single File Example](#full-single-file-example)
+
+---
+
+### **Import**
+
+Include the library in your project. If it’s a module, you can import it as follows:
+
+```javascript
+import { setupValidatorInput } from "@rezact/rezact/validator";
+import type { ValidatorOptions } from "@rezact/rezact/validator";
+```
+
+---
+
+### **Basic Setup**
+
+To set up an input for validation, you need to call the `setupValidatorInput` function with the appropriate options.
+
+#### Example:
+
+```javascript
+import { getFormData } from "@rezact/rezact/formHelper";
+import { setupValidatorInput } from "@rezact/rezact/validator";
+
+const MyInputGroup = (
+  <>
+    <label>My Input</label>
+    <input name="my-input" />
+    <div class="error-elm"></div>
+  </>
+);
+
+// call this function in the onMount of the form
+function setupInputValidators() {
+  setupValidatorInput({
+    inputElm: MyInputGroup[1],
+    errorElm: MyInputGroup[2],
+    required: true,
+    minLength: 5,
+  });
+}
+
+const handleSubmit = (ev) => {
+  ev.preventDefault();
+  if (Form.checkValidity()) {
+    const data = getFormData(Form);
+    console.log(data);
+  }
+};
+
+const Form = (
+  <form onMount={setupInputValidators} onSubmit={handleSubmit}>
+    {MyInputGroup}
+    <button>Submit</button>
+  </form>
+);
+
+export default function App() {
+  return <>{Form}</>;
+}
+```
+
+---
+
+### **Options**
+
+Here are the options you can pass to `setupValidatorInput`:
+
+- `inputElm` (Required): The input element you want to validate. It must be an HTMLInputElement.
+- `errorElm` (Optional): The element where error messages will be displayed. It must be an HTMLElement
+- `required` (Optional): Whether the input is required. Default is `false`.
+- `minLength` (Optional): The minimum length of the input value.
+- `maxLength` (Optional): The maximum length of the input value.
+- `exactLength` (Optional): The exact length the input value should be.
+- `pattern` (Optional): A RegExp pattern the input value should match.
+- `customValidator` (Optional): A custom validation function. It should return `true` if valid, `false` or an error message `string` if invalid.
+- `mask` (Optional): A string representing the input mask.
+- `maskSlots` (Optional): A string representing the mask slots.
+- `dataAccept` (Optional): A string representing the RegExp pattern of accepted characters.
+- `allowUnacceptedChars` (Optional): Whether to allow characters not matching `dataAccept`. Default is `false`.
+- `unmaskInputValueProp` (Optional): Whether to unmask the input value property. Default is `false`.
+- `showFullMaskWhileTyping` (Optional): Whether to show the full mask while typing. Default is `false`.
+- `hideDotSlots` (Optional): Whether to hide dot slots in the mask. Default is `true`.
+- `validateUnMaskedValue` (Optional): Whether to validate the unmasked value. Default is `true`.
+- `validateOnInput` (Optional): Whether to validate on input (every key press). Default is `true`.
+- `isNumeric` (Optional): Whether the input is numeric. Default is determined by `dataAccept`.
+- `customErrorMessages` (Optional): An object containing custom error messages for different validation errors.
+
+---
+
+### Custom Error Messages
+
+Custom error messages can be defined in the `ValidatorOptions` for each input component. They allow you to override the default error messages with messages that are more specific and meaningful to your application's context.
+
+#### Usage:
+
+To use custom error messages, you can define them in the `customErrorMessages` property of the `ValidatorOptions` object. The keys of this object correspond to the type of validation error, and the values are the custom messages you want to display.
+
+#### Available Custom Error Message Keys:
+
+- `isRequired`: Use this key when the input is required but not provided.
+- `inputNotLongEnough`: Use this key when the input does not meet the required minimum length.
+- `inputLengthTooLong`: Use this key when the input exceeds the allowed maximum length.
+- `patternNotValid`: Use this key when the input does not match the specified pattern.
+- `customNotValid`: Use this key for any custom validation error messages when using `customValidator` option.
+
+#### Example:
+
+```javascript
+const expirationInputValidationOptions: ValidatorOptions = {
+  // ... other options
+  customErrorMessages: {
+    patternNotValid: "Expiration date is invalid or expired.",
+    isRequired: "Expiration date is required.",
+  },
+};
+```
+
+In this example, if the input does not match the specified pattern for the expiration date, the custom error message "Expiration date is invalid or expired." will be displayed. If the input is required but not provided, "Expiration date is required." will be displayed.
+
+---
+
+### **Masking Input**
+
+The mask options in the Validator Library allow you to define a user-friendly format for input fields, guiding users as they enter data and ensuring consistency in the data format. Below are the mask-related options and their effects:
+
+#### 1. `mask`:
+
+- **Description**: A string representing the format you want the user to see and follow. It can include characters representing data slots, and any other characters representing static parts of the mask.
+- **Example**: For a serial number input, you might use a mask like "XXX \_**\_ XDX-( \_\_** )-{ \_**\_ }-[ \_\_** ]".
+
+#### 2. `maskSlots`:
+
+- **Description**: A string representing the character used as a placeholder in the mask for the user to enter data.
+- **Example**: If you use "\_" as the `maskSlots`, in a mask like "XXX \_\_\_\_ XDX", the underscores represent the slots where users can enter data.
+
+#### 3. `showFullMaskWhileTyping`:
+
+- **Description**: A boolean that, when true, displays the full mask as soon as the user starts typing, helping them understand the expected format.
+- **Example**: If you have a mask like "(**_) _**-\_\_\_\_" for a phone number, enabling this option will show the entire mask, including parentheses and the dash, as soon as the user starts typing.
+
+#### 4. `unmaskInputValueProp`:
+
+- **Description**: A boolean that, when true, removes the mask from the value property of the input element. While the user sees the masked value as they input data, programmatic access to the input's value will retrieve the unmasked value.
+- **Example**: If an input has a mask of "XXX \_**\_ XDX-( \_\_** )-{ \_**\_ }-[ \_\_** ]" and displays as "XXX 1231 XDX-( 2312 )-{ 3123 }-[ 1231 ]" after user input, accessing `inputElm.value` with this option enabled will return "1231231231231231".
+
+#### 5. `dataAccept`:
+
+- **Description**: A regular expression defining the characters allowed in the data slots of the mask.
+- **Example**: If you want to allow only digits in the data slots, you would use `/[\d]/`.
+
+#### 6. `validateUnMaskedValue`:
+
+- **Description**: A boolean that, when true, will perform any validations on the unmasked value of the input.
+- **Example**: If you have an input with a mask and want to validate the length of the unmasked value, enabling this option will use the length of the unmasked value for the validation.
+
+### Practical Usage:
+
+Below is a basic example of how you can create a masked input for a telephone number using the library. The mask is set up to format the input as `(XXX) XXX-XXXX`, where `X` represents a digit.
+
+### JavaScript/JSX Example:
+
+```javascript
+import { setupValidatorInput } from "@rezact/rezact/validator";
+import type { ValidatorOptions } from "@rezact/rezact/validator";
+
+const TelephoneInputGroup = (
+  <>
+    <label htmlFor="telephone">Telephone</label>
+    <input id="telephone" name="telephone" />
+    <div className="error-elm"></div>
+  </>
+);
+
+const telephoneInputValidationOptions: ValidatorOptions = {
+  inputElm: TelephoneInputGroup[1],
+  errorElm: TelephoneInputGroup[2],
+  required: true,
+  mask: "(XXX) XXX-XXXX",
+  maskSlots: "X",
+  dataAccept: /[\d]/,
+  showFullMaskWhileTyping: true,
+  exactLength: 10,
+};
+
+function setupInputValidators() {
+  setupValidatorInput(telephoneInputValidationOptions);
+}
+
+const handleSubmit = (ev) => {
+  ev.preventDefault();
+  const Form = ev.target;
+  if (Form.checkValidity()) {
+    // Handle form submission, e.g., send data to the server
+  }
+};
+
+const Form = (
+  <form onMount={setupInputValidators} onSubmit={handleSubmit}>
+    {TelephoneInputGroup}
+    <button type="submit">Submit</button>
+  </form>
+);
+
+export default function App() {
+  return <>{Form}</>;
+}
+```
+
+---
+
+### **Custom Validation**
+
+You can also provide a custom validation function using the `customValidator` option. This function should return `true` if the input is valid, and `false` or a custom error message string if the input is invalid.
+
+#### Example:
+
+```javascript
+setupValidatorInput({
+  inputElm: document.querySelector("#myInput"),
+  customValidator: (value) => {
+    if (value !== "expectedValue") return "The value is not valid.";
+    return true;
+  },
+});
+```
+
+---
+
+### Displaying Validation Errors with `errorElm`
+
+#### 1. **Error Element (`errorElm`):**
+
+- Each input field has a corresponding `errorElm`, typically a `div`, where validation error messages are displayed.
+- This `errorElm` is usually placed below the input field and is responsible for showing the user what went wrong with their input.
+
+#### 2. **Manipulating `errorElm` Display:**
+
+- The library dynamically manipulates the `height` attribute of the `errorElm` to show and hide error messages.
+- When a validation error occurs, the `height` of the `errorElm` is adjusted to reveal the error message.
+- When the input is corrected, the `height` is reset to `0px`, hiding the error message and giving a smooth transition effect between states.
+- This manipulation is done with a transition effect, typically set with CSS, to ensure a smooth and visually pleasing appearance and disappearance of the error message.
+
+#### 3. **Custom Error Messages:**
+
+- Custom error messages can be defined for various validation scenarios using the `customErrorMessages` property in the `ValidatorOptions`.
+- These messages are displayed in the `errorElm` when the corresponding validation fails, providing clear and concise feedback to the user.
+
+#### 4. **Styling and Visual Feedback:**
+
+- The `errorElm` and its messages are typically styled with attention-grabbing colors, like red, to ensure users notice them.
+- The transition effect on the `height` attribute provides smooth visual feedback, enhancing user experience by not startling them with sudden changes.
+
+#### 5. **Validation Triggering and Message Clearance:**
+
+- Error messages are dynamically displayed or cleared based on user interaction and input validity.
+- The library can be configured to validate inputs at different interaction points, displaying error messages immediately or after specific user actions.
+- Once the user corrects their input to meet the validation criteria, the error messages are cleared, and the `errorElm` is hidden by resetting its `height` to `0px`.
+
+### Example:
+
+In the provided example, each `Input` component has an associated `errorElm` placed below the input field. When a validation error occurs, the `height` of the `errorElm` is adjusted to reveal the custom error message, and it’s reset to `0px` when the error is corrected.
+
+```jsx
+const serialNoInputValidationOptions: ValidatorOptions = {
+  // ...
+  errorElm: SerialInput[2],
+  customErrorMessages: {
+    inputNotLongEnough: "Serial Number is not complete.",
+  },
+  // ...
+};
+```
+
+---
+
+# Full Single File Example
+
+For a more comprehensive single file example checkout https://github.com/Rezact/Rezact/blob/main/src/examples/FormValidation/FormValidation.tsx
diff --git a/package.json b/package.json
index cb23d7f..c9ea8c2 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@rezact/rezact",
-  "version": "1.0.15-beta.16",
+  "version": "1.0.15-beta.17",
   "type": "module",
   "description": "Intuitive Reactivity, Simplified State. Embrace a modern UI framework that encourages direct data mutations, offers module-level reactivity, and simplifies component design. With Rezact, you get the power of reactivity without the boilerplate. Dive into a seamless development experience tailored for today's web.",
   "main": "index.cjs",
diff --git a/src/components/appRouter.ts b/src/components/appRouter.ts
index 2b9b91f..660f12b 100644
--- a/src/components/appRouter.ts
+++ b/src/components/appRouter.ts
@@ -78,4 +78,9 @@ router.addRoute(
   () => import("src/examples/SimpleStringListState/SimpleStringList")
 );
 
+router.addRoute(
+  "/form-validation",
+  () => import("src/examples/FormValidation/FormValidation")
+);
+
 export { router };
diff --git a/src/examples/FormValidation/FormValidation.tsx b/src/examples/FormValidation/FormValidation.tsx
new file mode 100644
index 0000000..94d176a
--- /dev/null
+++ b/src/examples/FormValidation/FormValidation.tsx
@@ -0,0 +1,246 @@
+import { setupValidatorInput } from "src/lib/rezact/validator";
+import type { ValidatorOptions } from "src/lib/rezact/validator";
+import { MyLayout } from "../Layout/nestedLayout";
+import { getFormData } from "src/lib/rezact/formHelper";
+
+type InputProps = {
+  label: string;
+  placeholder?: string;
+};
+
+function Input({ label, placeholder }: InputProps) {
+  const id = label.toLowerCase();
+  return (
+    <>
+      <label for={id}>{label}</label>
+      <input
+        style="display: block; "
+        autocomplete="off"
+        name={id}
+        id={id}
+        placeholder={placeholder || ""}
+      />
+      <div style="height: 0px; overflow: hidden; transition: all 300ms ease 0s; color: red; font-size: 10pt; margin-bottom: 10px;"></div>
+    </>
+  );
+}
+
+const SerialInput = <Input label="Serial No." />;
+const serialNoInputValidationOptions: ValidatorOptions = {
+  inputElm: SerialInput[1],
+  errorElm: SerialInput[2],
+  required: true,
+  mask: "XXX ____ XDX-( ____ )-{ ____ }-[ ____ ]",
+  maskSlots: "_",
+  dataAccept: /[\d]/,
+  unmaskInputValueProp: false,
+  validateUnMaskedValue: true,
+  showFullMaskWhileTyping: true,
+  exactLength: 16,
+  customErrorMessages: {
+    inputNotLongEnough: "Serial Number is not complete.",
+  },
+  // showFullMaskWhileTyping: true,
+  // hideDotSlots: false
+};
+
+const SerialInput2 = <Input label="Serial2 No." />;
+const serialNoInputValidationOptions2: ValidatorOptions = {
+  inputElm: SerialInput2[1],
+  errorElm: SerialInput2[2],
+  required: true,
+  mask: "XXX ____ XDX-( ____ )-{ ____ }-[ ____ ]",
+  maskSlots: "_",
+  dataAccept: /[\d]/,
+  unmaskInputValueProp: true,
+  validateUnMaskedValue: true,
+  showFullMaskWhileTyping: true,
+  exactLength: 16,
+  customErrorMessages: {
+    inputNotLongEnough: "Serial Number is not complete.",
+  },
+  // showFullMaskWhileTyping: true,
+  // hideDotSlots: false
+};
+
+const IPv4Input = <Input label="IP v4" placeholder="123.234.123.234" />;
+const ipv4InputValidationOptions: ValidatorOptions = {
+  inputElm: IPv4Input[1],
+  errorElm: IPv4Input[2],
+  pattern: /^$|^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/,
+  dataAccept: /[\d.]/,
+  validateOnInput: false,
+};
+
+const NameInput = <Input label="Name" placeholder="John Doe" />;
+const nameInputValidationOptions: ValidatorOptions = {
+  inputElm: NameInput[1],
+  errorElm: NameInput[2],
+  required: true,
+};
+
+const TelephoneInput = <Input label="Telephone" placeholder="(___) ___ ____" />;
+const phoneInputValidationOptions: ValidatorOptions = {
+  inputElm: TelephoneInput[1],
+  errorElm: TelephoneInput[2],
+  required: true,
+  mask: "(___) ___ ____",
+  maskSlots: "_",
+  dataAccept: /[\d]/,
+  showFullMaskWhileTyping: true,
+  exactLength: 10,
+};
+
+const StreetInput = <Input label="Street" placeholder="1234 Main St." />;
+const streetInputValidationOptions: ValidatorOptions = {
+  inputElm: StreetInput[1],
+  errorElm: StreetInput[2],
+  required: true,
+  minLength: 4,
+  maxLength: 10,
+};
+
+const ZipInput = <Input label="Zip" placeholder="12345" />;
+const zipInputValidationOptions: ValidatorOptions = {
+  inputElm: ZipInput[1],
+  errorElm: ZipInput[2],
+  required: true,
+  mask: ".....",
+  maskSlots: ".",
+  dataAccept: /[\d]/,
+  exactLength: 5,
+};
+
+const CommentsInput = <Input label="Comments" placeholder="" />;
+const commentsInputValidationOptions: ValidatorOptions = {
+  inputElm: CommentsInput[1],
+  errorElm: CommentsInput[2],
+  maxLength: 20,
+};
+
+const CreditCardInput = <Input label="Credit Card" />;
+const creditCardInputValidationOptions: ValidatorOptions = {
+  inputElm: CreditCardInput[1],
+  errorElm: CreditCardInput[2],
+  mask: ".... .... .... ....",
+  maskSlots: ".",
+  dataAccept: /[\d]/,
+  exactLength: 16,
+  required: true,
+  customValidator: luhnCheck,
+  customErrorMessages: {
+    customNotValid: "Credit Card Number is not Valid.",
+  },
+};
+
+const ExpirationInput = <Input label="Expiration" placeholder="MM/YY" />;
+const expirationInputValidationOptions: ValidatorOptions = {
+  inputElm: ExpirationInput[1],
+  errorElm: ExpirationInput[2],
+  pattern: /\d\d\/\d\d/,
+  mask: "../..",
+  maskSlots: ".",
+  exactLength: 5,
+  required: true,
+  validateUnMaskedValue: false,
+  customErrorMessages: {
+    patternNotValid: "Expiration date is invalid or expired.",
+  },
+  customValidator: validateExpireDate,
+};
+
+const CVVInput = <Input label="CVV" placeholder="000" />;
+const cvvInputValidationOptions: ValidatorOptions = {
+  inputElm: CVVInput[1],
+  errorElm: CVVInput[2],
+  minLength: 3,
+  maxLength: 4,
+  required: true,
+};
+
+function setupInputValidators() {
+  setupValidatorInput(serialNoInputValidationOptions);
+  setupValidatorInput(serialNoInputValidationOptions2);
+  setupValidatorInput(ipv4InputValidationOptions);
+  setupValidatorInput(nameInputValidationOptions);
+  setupValidatorInput(phoneInputValidationOptions);
+  setupValidatorInput(streetInputValidationOptions);
+  setupValidatorInput(zipInputValidationOptions);
+  setupValidatorInput(commentsInputValidationOptions);
+  setupValidatorInput(creditCardInputValidationOptions);
+  setupValidatorInput(expirationInputValidationOptions);
+  setupValidatorInput(cvvInputValidationOptions);
+}
+
+const handleSubmit = (ev) => {
+  ev.preventDefault();
+  if (Form.checkValidity()) {
+    const data = getFormData(Form);
+    console.log(data);
+  }
+};
+
+const Form = (
+  <form onMount={setupInputValidators} onSubmit={handleSubmit}>
+    {SerialInput}
+    {SerialInput2}
+    {IPv4Input}
+    {NameInput}
+    {TelephoneInput}
+    {StreetInput}
+    {ZipInput}
+    {CommentsInput}
+    {CreditCardInput}
+    {ExpirationInput}
+    {CVVInput}
+
+    <input type="submit" />
+  </form>
+);
+
+export function Page() {
+  return (
+    <>
+      <h1>Validator Demo</h1>
+      {Form}
+    </>
+  );
+}
+
+function luhnCheck(ccNumWithSpaces) {
+  const ccNum = ccNumWithSpaces.replaceAll(" ", "");
+  const arr = [0, 2, 4, 6, 8, 1, 3, 5, 7, 9];
+  let bit = 1;
+  let len = ccNum.length;
+  let sum = 0;
+  let val;
+
+  while (len) {
+    val = parseInt(ccNum.charAt(--len), 10);
+    sum += (bit ^= 1) ? arr[val] : val;
+  }
+
+  return sum && sum % 10 === 0;
+}
+
+function validateExpireDate(dateString) {
+  const re = new RegExp("^(0[1-9]|1[0-2])/?([0-9]{2})$");
+  if (!re.test(dateString)) return "Expiration date is invalid or expired.";
+  // return "The expire date format must be MM/YY. ie: 03/24";
+
+  const [month, year] = dateString.split("/");
+  const ccMonth = parseInt(month);
+  const ccYear = parseInt(year);
+
+  const d = new Date();
+  const thisMonth = d.getMonth() + 1;
+  const thisYear = parseInt(d.getFullYear().toString().slice(2));
+  const maxYear = thisYear + 10;
+  const expYearIsInRange = ccYear >= thisYear && ccYear < maxYear;
+  const expIsInFutureYear = ccYear > thisYear;
+  const expMonthIsInRange = expIsInFutureYear || ccMonth >= thisMonth;
+  if (expYearIsInRange && expMonthIsInRange) return true;
+  return "Expiration date is invalid or expired.";
+}
+
+export const Layout = MyLayout;
diff --git a/src/examples/FormValidation/index.html b/src/examples/FormValidation/index.html
new file mode 100644
index 0000000..cb2b361
--- /dev/null
+++ b/src/examples/FormValidation/index.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Vite + Rezact + TS</title>
+</head>
+
+<body>
+    <div id="app"></div>
+    <script type="module" src="./main.ts"></script>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/src/examples/FormValidation/main.ts b/src/examples/FormValidation/main.ts
new file mode 100644
index 0000000..d66b41f
--- /dev/null
+++ b/src/examples/FormValidation/main.ts
@@ -0,0 +1,5 @@
+import { render } from "rezact";
+import { Page } from "./FormValidation";
+
+const app = document.getElementById("app");
+render(app, Page);
diff --git a/src/examples/Layout/nestedLayout.tsx b/src/examples/Layout/nestedLayout.tsx
index 4d364ee..d334483 100644
--- a/src/examples/Layout/nestedLayout.tsx
+++ b/src/examples/Layout/nestedLayout.tsx
@@ -71,6 +71,9 @@ export function MyLayout({ Component, pageProps }: any) {
         <li>
           <a href="/escape-hatches">Escape Hatches</a>
         </li>
+        <li>
+          <a href="/form-validation">Form Validator</a>
+        </li>
         <li>
           <a href="/asdfasdfasdfasdf">A Route that does not exist</a>
         </li>
diff --git a/src/lib/rezact/validator.ts b/src/lib/rezact/validator.ts
new file mode 100644
index 0000000..4eccd66
--- /dev/null
+++ b/src/lib/rezact/validator.ts
@@ -0,0 +1,696 @@
+interface CustomInput extends HTMLInputElement {
+  errorElm: HTMLElement;
+  errorObj: Map<string, boolean>;
+}
+
+interface CustomErrorElm extends HTMLElement {
+  errorObj?: Map<string, boolean>;
+}
+
+interface ValidatorOptions {
+  inputElm: CustomInput;
+  errorElm?: CustomErrorElm;
+  required?: boolean;
+  minLength?: number;
+  maxLength?: number;
+  exactLength?: number;
+  pattern?: RegExp;
+  customValidator?: (value: string) => boolean | string;
+  mask?: string;
+  maskSlots?: string;
+  dataAccept?: RegExp;
+  allowUnacceptedChars?: boolean;
+  unmaskInputValueProp?: boolean;
+  showFullMaskWhileTyping?: boolean;
+  hideDotSlots?: boolean;
+  validateUnMaskedValue?: boolean;
+  validateOnInput?: boolean;
+  isNumeric?: boolean;
+  customErrorMessages?: {
+    isRequired?: string;
+    inputNotLongEnough?: string;
+    inputLengthTooLong?: string;
+    patternNotValid?: string;
+    customNotValid?: string;
+  };
+}
+
+// a deduplicated list of all the inputs and errorElements that are registered by setupInput
+const inputElms = new Map();
+const errorElms = new Map();
+
+const keyDownEvent = "keydown";
+const inputEvent = "input";
+const blurEvent = "blur";
+const clickEvent = "click";
+const focusEvent = "focus";
+
+function setupValidatorInput(opts: ValidatorOptions) {
+  opts.inputElm = getInputElm(opts) as CustomInput;
+  opts.errorElm = getErrorElm(opts);
+
+  const inputElm = opts.inputElm;
+  if (inputElms.has(inputElm))
+    return console.error(
+      `${inputElm.name} - This input has already been setup. If you would like to make changes to the options you can access them directly on the [element].validatorOptions.`
+    );
+
+  opts.errorElm = opts.errorElm || createErrorElementForInput(inputElm);
+  opts = mergeDefaultOptions(opts);
+  console.info(
+    "validating validator options for",
+    opts.inputElm,
+    validateOptions(opts)
+  );
+  setupOptionPropertyGettersAndSetters(opts);
+
+  const errorElm = opts.errorElm;
+  const form = inputElm.form;
+
+  inputElm.errorElm = errorElm as HTMLElement;
+
+  inputElms.set(inputElm, opts);
+  errorElms.set(errorElm, opts);
+  errorElm.errorObj = new Map();
+
+  if (opts.mask) VMasker(inputElm);
+  if (!opts.mask) setupValueProxy(inputElm);
+
+  addEventListener(inputElm, blurEvent, onBlur);
+  addEventListener(inputElm, inputEvent, onInput);
+  addEventListener(inputElm, keyDownEvent, onKeyDown);
+
+  !form.zwcSubmitValidation && addEventListener(form, "submit", onSubmit, true);
+  form.zwcSubmitValidation = true;
+  form.noValidate = true; //disable browser validation, we got it from here
+}
+
+function addEventListener(elm, event, func, opts = {}) {
+  elm.addEventListener(event, func, opts);
+}
+
+function ObjectDefineProperty(obj, propName, opts) {
+  return Object.defineProperty(obj, propName, opts);
+}
+
+function ObjectGetOwnPropertyDescriptor(proto, propName) {
+  return Object.getOwnPropertyDescriptor(proto, propName);
+}
+
+function setupValueProxy(inputElm) {
+  var valueProp = ObjectGetOwnPropertyDescriptor(
+    HTMLInputElement.prototype,
+    "value"
+  );
+  const setVal = (newValue) => valueProp.set.call(inputElm, newValue);
+  const getVal = () => valueProp.get.call(inputElm);
+
+  ObjectDefineProperty(inputElm, "value", {
+    get: getVal,
+    set: (newValue) => {
+      setVal(newValue);
+      const blurEv = new Event(blurEvent);
+      inputElm.dispatchEvent(blurEv);
+    },
+  });
+}
+
+function querySel(selector) {
+  return document.querySelector(selector);
+}
+
+function getInputElm(opts) {
+  if (typeof opts.inputElm === "function") return opts.inputElm(opts);
+  if (typeof opts.inputElm === "string") return querySel(opts.inputElm);
+  return opts.inputElm as HTMLInputElement;
+}
+
+function getErrorElm(opts) {
+  if (typeof opts.errorElm === "function") return opts.errorElm(opts);
+  if (typeof opts.errorElm === "string") return querySel(opts.errorElm);
+  return opts.errorElm as HTMLElement;
+}
+
+function mergeDefaultOptions(opts) {
+  const accept = new RegExp(opts.dataAccept || "[\\d\\w]", "g");
+  const testString = `abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890 !@#$%^&*()_+-=[]\;',./{}|:"<>?`;
+  const matchedChars = testString.match(accept);
+
+  const defaults = {
+    inputElm: null,
+    errorElm: null,
+    required: false,
+    minLength: null,
+    maxLength: null,
+    exactLength: null,
+    pattern: null,
+    customValidator: null,
+    mask: null,
+    maskSlots: null,
+    dataAccept: null,
+    allowUnacceptedChars: false,
+    unmaskInputValueProp: false,
+    showFullMaskWhileTyping: false,
+    hideDotSlots: true,
+    validateUnMaskedValue: true,
+    validateOnInput: true,
+    isNumeric: opts.dataAccept && matchedChars.length === 10,
+    customErrorMessages: {
+      isRequired: null,
+      inputNotLongEnough: null,
+      inputLengthTooLong: null,
+      patternNotValid: null,
+    },
+  };
+
+  return { ...defaults, ...opts };
+}
+
+function setupOptionPropertyGettersAndSetters(opts) {
+  opts.inputElm.mask = opts.mask;
+  opts.inputElm.maskSlots = opts.maskSlots;
+  opts.inputElm.dataAccept = opts.dataAccept;
+  opts.inputElm.unmaskInputValueProp = opts.unmaskInputValueProp;
+  opts.inputElm.showFullMaskWhileTyping = opts.showFullMaskWhileTyping;
+  opts.inputElm.hideDotSlots = opts.hideDotSlots;
+  opts.inputElm.validatorOptions = opts;
+
+  const proxyOption = (prop) =>
+    ObjectDefineProperty(opts.inputElm.validatorOptions, prop, {
+      get: () => opts.inputElm[prop],
+      set: (newValue) => (opts.inputElm[prop] = newValue),
+    });
+
+  const optionsToProxy = [
+    "mask",
+    "maskSlots",
+    "dataAccept",
+    "unmaskInputValueProp",
+    "showFullMaskWhileTyping",
+    "hideDotSlots",
+  ];
+
+  optionsToProxy.forEach((prop) => proxyOption(prop));
+}
+
+function validateOptions(opts) {
+  if (!opts.inputElm) throw `inputElm is a required option`;
+  if (opts.inputElm.nodeName !== `INPUT`)
+    throw `inputElm must be an INPUT element`;
+
+  const name = opts.inputElm.name;
+  const prefix = ` - ${name} - invalid config - `;
+
+  if (!name) {
+    console.error("No name attribute given on input", opts.inputElm);
+    throw `${prefix} Please give your input a name attribute`;
+  }
+
+  if (!(opts.errorElm instanceof HTMLElement))
+    throw `${prefix} errorElm must be an HTML Element`;
+
+  if (opts.mask && !opts.maskSlots)
+    throw `${prefix} cannot define a mask without defining a maskSlot`;
+
+  if (opts.minLength && opts.exactLength)
+    throw `${prefix} cannot define both minLength and exactLength, pick one.`;
+
+  if (opts.maxLength && opts.exactLength)
+    throw `${prefix} cannot define both maxLength and exactLength, pick one.`;
+
+  const patternWithoutMinLength =
+    opts.pattern &&
+    !(opts.minLength || opts.exactLength || !opts.validateOnInput);
+  const customWithoutMinLength =
+    opts.customValidator &&
+    !(opts.minLength || opts.exactLength || !opts.validateOnInput);
+
+  if (patternWithoutMinLength || customWithoutMinLength)
+    throw `${prefix} need to define minLength, exactLength, or set validateOnInput: false when using pattern or custom validators.  Otherwise the user will be shown errors immediately as they start typing. This leads to poor UX.  Ideally, we want to give the user a chance to put in the correct input before yelling at them. If your pattern cannot possibly be valid when the input is below a certain length, then we want to wait until that minimum length is met before attempting this validation.`;
+
+  if (opts.mask) {
+    const accept = new RegExp(opts.dataAccept || "[\\d\\w]", "g");
+    const maskContainsAcceptChars = opts.mask.match(accept) || [];
+    if (maskContainsAcceptChars.length > 0)
+      throw `${prefix} The 'mask' contains characters that are also able to be typed into the input, please fix the mask or add/correct the 'dataAccept' property to fix this issue`;
+  }
+
+  return "success";
+}
+
+function onSubmit(ev) {
+  const form = ev.target;
+
+  checkAll();
+  const isValid = form.checkValidity();
+  if (!isValid) return ev.preventDefault();
+  // return isValid;
+  // if (isValid) alert("Form is Valid!");
+  // ev.preventDefault();
+}
+
+function onBlur(ev) {
+  const inputElm = ev.target;
+  if (requiredValidationActive(inputElm)) return reportRequired(inputElm);
+  if (getValue(inputElm) === "") return clearValidationError(inputElm);
+  if (inputContainsUnacceptedChars(inputElm))
+    return reportPatternNotValid(inputElm);
+  if (inputNotLongEnough(inputElm)) return reportNotLongEnough(inputElm);
+  if (inputLengthTooLong(inputElm)) return reportLengthTooLong(inputElm);
+  if (patternNotValid(inputElm)) return reportPatternNotValid(inputElm);
+  if (customNotValid(inputElm)) return reportCustomNotValid(inputElm);
+  clearValidationError(inputElm);
+}
+
+function onKeyDown(ev) {
+  const inputElm = ev.target;
+  const opts = inputElms.get(inputElm);
+  if (opts.mask) return; // the mask handles what data to accept, no need to do it here;
+  if (!opts.dataAccept) return; // only intercept if dataAccept is explicitly set
+  if (opts.allowUnacceptedChars) return;
+  const allowedKeys = ["Backspace", "Tab", "ArrowRight", "ArrowLeft"];
+  if (allowedKeys.includes(ev.key)) return;
+
+  const accept = new RegExp(opts.dataAccept);
+  const keyNotAccepted = !accept.test(ev.key);
+  if (keyNotAccepted) return ev.preventDefault();
+}
+
+function onInput(ev) {
+  const inputElm = ev.target;
+  const opts = inputElms.get(inputElm);
+  if (!opts.validateOnInput && inputElm.checkValidity()) return;
+  if (requiredValidationActive(inputElm)) return reportRequired(inputElm);
+  if (getValue(inputElm) === "") return clearValidationError(inputElm);
+  if (inputContainsUnacceptedChars(inputElm))
+    return reportPatternNotValid(inputElm);
+  if (inputNotLongEnough(inputElm)) return;
+  if (inputLengthTooLong(inputElm)) return reportLengthTooLong(inputElm);
+  if (patternNotValid(inputElm)) return reportPatternNotValid(inputElm);
+  if (customNotValid(inputElm)) return reportCustomNotValid(inputElm);
+  clearValidationError(inputElm);
+}
+
+function checkAll() {
+  const allInputs = Array.from(inputElms.keys());
+  allInputs.forEach((inputElm) => {
+    if (isRequired(inputElm)) return reportRequired(inputElm);
+    if (inputContainsUnacceptedChars(inputElm))
+      return reportPatternNotValid(inputElm);
+    if (inputNotLongEnough(inputElm)) return reportNotLongEnough(inputElm);
+    if (inputLengthTooLong(inputElm)) return reportLengthTooLong(inputElm);
+    if (patternNotValid(inputElm)) return reportPatternNotValid(inputElm);
+    if (customNotValid(inputElm)) return reportCustomNotValid(inputElm);
+    clearValidationError(inputElm);
+  });
+}
+
+function getValue(inputElm) {
+  const opts = inputElms.get(inputElm);
+
+  const unmaskedValue =
+    typeof inputElm.unmaskedValue === "string"
+      ? inputElm.unmaskedValue
+      : inputElm.value;
+
+  const maskedValue =
+    typeof inputElm.maskedValue === "string"
+      ? inputElm.maskedValue
+      : inputElm.value;
+
+  return opts.validateUnMaskedValue ? unmaskedValue : maskedValue;
+}
+
+function isRequired(inputElm) {
+  const opts = inputElms.get(inputElm);
+  if (!opts.required) return false;
+  const inputVal = opts.mask ? inputElm.unmaskedValue : inputElm.value;
+  if (inputVal.length === 0) return true;
+  return false;
+}
+
+function reportRequired(inputElm) {
+  const opts = inputElms.get(inputElm);
+  const customMsg = opts.customErrorMessages.isRequired;
+  if (customMsg) return setErrorMessage(inputElm, customMsg);
+
+  inputElm.requiredValidationActive = true;
+  inputElm.failedRequiredValidationOnce = true;
+  const label = inputElm.labels[0]?.innerText || "This field";
+  const msg = `${label} is required.`;
+  setErrorMessage(inputElm, msg);
+}
+
+function requiredValidationActive(inputElm) {
+  if (!inputElm.failedRequiredValidationOnce) return false;
+  const opts = inputElms.get(inputElm);
+  const inputVal = opts.mask ? inputElm.unmaskedValue : inputElm.value;
+  if (inputVal.length < 1) return true;
+
+  if (!inputElm.requiredValidationActive) return false;
+  inputElm.requiredValidationActive = false;
+  clearValidationError(inputElm);
+  return false;
+}
+
+function inputContainsUnacceptedChars(inputElm) {
+  const opts = inputElms.get(inputElm);
+  if (!opts.dataAccept) return false;
+  const valSplit = getValue(inputElm).split("");
+  const accept = new RegExp(opts.dataAccept);
+
+  for (const char of valSplit) {
+    const keyNotAccepted = !accept.test(char);
+    if (keyNotAccepted) return true;
+  }
+
+  if (inputElm.patternValidationActive) clearValidationError(inputElm);
+  inputElm.patternValidationActive = false;
+  return false;
+}
+
+function inputNotLongEnough(inputElm) {
+  const opts = inputElms.get(inputElm);
+  if (!opts.minLength && !opts.exactLength) return false;
+
+  const inpVal = getValue(inputElm);
+
+  const tooLongActive = inputElm.inputTooLongErrorActive;
+  const maxLen = opts.maxLength || opts.exactLength;
+  if (tooLongActive && inpVal.length <= maxLen) {
+    inputElm.inputTooLongErrorActive = false;
+    clearValidationError(inputElm);
+  }
+
+  if (opts.minLength && inpVal.length < opts.minLength) return true;
+
+  if (opts.exactLength && inpVal.length < opts.exactLength) return true;
+
+  return false;
+}
+
+function reportNotLongEnough(inputElm) {
+  const opts = inputElms.get(inputElm);
+  const customMsg = opts.customErrorMessages.inputNotLongEnough;
+  if (customMsg) return setErrorMessage(inputElm, customMsg);
+
+  const label = inputElm.labels[0]?.innerText || "This field";
+  const n = opts.minLength;
+  const x = opts.exactLength;
+  const m = opts.maxLength;
+  const exact = opts.exactLength || m === n;
+  const exactOrAtleast = exact ? "exactly" : "atleast";
+  const chars = opts.isNumeric ? "digits" : "characters";
+  const msg = `${label} must be ${exactOrAtleast} ${n || x} ${chars} long.`;
+  setErrorMessage(inputElm, msg);
+}
+
+function inputLengthTooLong(inputElm) {
+  const opts = inputElms.get(inputElm);
+  const maxLen = opts.maxLength || opts.exactLength;
+  if (!maxLen) return false;
+
+  const inpVal = getValue(inputElm);
+  if (inpVal.length > maxLen) {
+    inputElm.inputTooLongErrorActive = true;
+    return true;
+  }
+
+  return false;
+}
+
+function reportLengthTooLong(inputElm) {
+  const opts = inputElms.get(inputElm);
+  if (opts.exactLength) return reportNotLongEnough(inputElm);
+  const maxLen = opts.maxLength;
+  const customMsg = opts.customErrorMessages.inputLengthTooLong;
+  if (customMsg) return setErrorMessage(inputElm, customMsg);
+
+  const label = inputElm.labels[0]?.innerText || "This field";
+  const chars = opts.isNumeric ? "digits" : "characters";
+  const msg = `${label} must not be longer than ${maxLen} ${chars}.`;
+  setErrorMessage(inputElm, msg);
+}
+
+function patternNotValid(inputElm) {
+  const opts = inputElms.get(inputElm);
+  if (!opts.pattern) return false;
+  const val = inputElm.value;
+  const re = new RegExp(opts.pattern);
+  return !re.test(val);
+}
+
+function reportPatternNotValid(inputElm) {
+  const opts = inputElms.get(inputElm);
+  const customMsg = opts.customErrorMessages.patternNotValid;
+  if (customMsg) return setErrorMessage(inputElm, customMsg);
+
+  inputElm.patternValidationActive = true;
+  const label = inputElm.labels[0]?.innerText || "This field";
+  const msg = `${label} is not valid.`;
+  setErrorMessage(inputElm, msg);
+}
+
+function customNotValid(inputElm) {
+  const opts = inputElms.get(inputElm);
+  if (!opts.customValidator) return false;
+
+  const test = opts.customValidator(getValue(inputElm));
+  if (typeof test === "string") {
+    opts.customErrorMessages.customNotValid = test;
+    return true; //input is not valid is true
+  }
+  return !test;
+}
+
+function reportCustomNotValid(inputElm) {
+  const opts = inputElms.get(inputElm);
+  const customMsg = opts.customErrorMessages.customNotValid;
+  if (customMsg) return setErrorMessage(inputElm, customMsg);
+
+  const label = inputElm.labels[0]?.innerText || "This field";
+  const msg = `${label} is not valid.`;
+  setErrorMessage(inputElm, msg);
+}
+
+function openErrorElm(errorElm) {
+  errorElm.style.height = errorElm.scrollHeight + "px";
+}
+
+function closeErrorElm(errorElm) {
+  errorElm.style.height = "0px";
+}
+
+function clearValidationError(inputElm, skipSet = false) {
+  const currentErrorMsg = inputElm.validationMessage;
+  inputElm.classList.remove("invalid");
+  inputElm.setCustomValidity("");
+
+  const errorElm = inputElm.errorElm;
+  errorElm.errorObj.delete(currentErrorMsg);
+  !skipSet && setErrorMessages(errorElm);
+}
+
+function setErrorMessage(inputElm, msg) {
+  clearValidationError(inputElm, true);
+  if (inputElm.validationMessage === msg) return;
+
+  inputElm.classList.add("invalid");
+  inputElm.setCustomValidity(msg);
+
+  const errorElm = inputElm.errorElm;
+  errorElm.errorObj.set(msg, true);
+  setErrorMessages(errorElm);
+}
+
+let showErrorDeBounce = null;
+function setErrorMessages(errorElm) {
+  let messages = Array.from(errorElm.errorObj.keys()).join("\n");
+  errorElm.innerText = messages;
+
+  clearTimeout(showErrorDeBounce);
+  showErrorDeBounce = setTimeout(showErrors, 100);
+}
+
+function showErrors() {
+  const allErrorElms = Array.from(errorElms.keys());
+  for (const errorElm of allErrorElms) {
+    if (!errorElm) continue;
+    if (errorElm.errorObj.size === 0) closeErrorElm(errorElm);
+    if (errorElm.errorObj.size > 0) openErrorElm(errorElm);
+  }
+}
+
+function createErrorElementForInput(inputElm) {
+  const errorElm = document.createElement("div");
+  errorElm.style.height = "0px";
+  errorElm.style.overflow = "hidden";
+  errorElm.style.transition = "all 300ms";
+  errorElm.style.color = "red";
+  errorElm.style.fontSize = "10pt";
+  errorElm.style.marginBottom = "10px";
+  errorElm.classList.add("error-message");
+  inputElm.style.marginBottom = "0px";
+  inputElm.after(errorElm);
+  return errorElm;
+}
+
+function VMaskerSetupProperties(el, formatValue) {
+  const valueProp = ObjectGetOwnPropertyDescriptor(
+    HTMLInputElement.prototype,
+    "value"
+  );
+  const setVal = (newValue) => valueProp.set.call(el, newValue);
+  const getVal = () => valueProp.get.call(el);
+
+  ObjectDefineProperty(el, "value", {
+    get: function () {
+      const unmaskedValue =
+        typeof el.unmaskedValue === "string" ? el.unmaskedValue : getVal();
+
+      const maskedValue =
+        typeof el.maskedValue === "string" ? el.maskedValue : getVal();
+
+      return el.unmaskInputValueProp ? unmaskedValue : maskedValue;
+    },
+    set: function (newValue) {
+      setVal(formatValue(newValue));
+      const blurEv = new Event(blurEvent);
+      el.dispatchEvent(blurEv);
+    },
+  });
+
+  const storedValues = {
+    mask: el.mask,
+    maskSlots: el.maskSlots,
+    dataAccept: el.dataAccept,
+  };
+
+  const maskGettersAndSetters = (prop) =>
+    ObjectDefineProperty(el, prop, {
+      get: () => storedValues[prop],
+      set: (newValue) => {
+        storedValues[prop] = newValue;
+        el.resetVars();
+        el.value = el.value;
+      },
+    });
+
+  const maskProps = ["mask", "maskSlots", "dataAccept"];
+
+  maskProps.forEach((prop) => maskGettersAndSetters(prop));
+
+  return [getVal, setVal] as const;
+}
+
+function VMasker(el) {
+  let pattern = null;
+  let slots = null;
+  let accept = null;
+  let prev = null;
+  let first = null;
+
+  el.resetVars = () => {
+    pattern = el.mask || el.getAttribute("placeholder");
+    slots = new Set(el.maskSlots || el.dataset.slots || "_");
+    accept = new RegExp(el.dataAccept || el.dataset.accept || "[\\d\\w]", "g");
+
+    prev = ((j) =>
+      Array.from(pattern, (c, i) => (slots.has(c) ? (j = i + 1) : j)))(0);
+
+    first = [...pattern].findIndex((c) => slots.has(c));
+  };
+
+  el.resetVars();
+
+  const clean = (inputReceived) => {
+    const unmaskedInput = inputReceived.match(accept) || [];
+    const cleanInput = Array.from(pattern, (maskChar) =>
+      unmaskedInput[0] === maskChar || slots.has(maskChar)
+        ? unmaskedInput.shift() || maskChar
+        : maskChar
+    );
+    el.unmaskedValue = cleanInput?.join("").match(accept)?.join("") || "";
+    return cleanInput;
+  };
+
+  const formatValue = (value) => {
+    const fullMaskValue = clean(value);
+    const newFirstSlot = fullMaskValue.findIndex((c) => slots.has(c));
+    const showFullMask = el.showFullMaskWhileTyping || newFirstSlot === -1;
+    const partialMaskValue =
+      newFirstSlot === -1
+        ? fullMaskValue
+        : fullMaskValue.slice(0, newFirstSlot);
+
+    const newVal = showFullMask ? fullMaskValue : partialMaskValue;
+    const slotsToHide = el.hideDotSlots && slots.has(".") ? "." : "";
+    const maskedValue = newVal.join("").replaceAll(slotsToHide, "").trim();
+    el.fullMaskValue = fullMaskValue
+      .join("")
+      .replaceAll(slotsToHide, "")
+      .trim();
+    el.partialMaskValue = partialMaskValue
+      .join("")
+      .replaceAll(slotsToHide, "")
+      .trim();
+    el.maskedValue = maskedValue;
+    return maskedValue;
+  };
+
+  const [getVal, setVal] = VMaskerSetupProperties(el, formatValue);
+
+  const format = () => {
+    const [i, j] = [el.selectionStart, el.selectionEnd].map((i) => {
+      i = clean(getVal().slice(0, i)).findIndex((c) => slots.has(c));
+      const test =
+        i < 0 ? prev[prev.length - 1] : back ? prev[i - 1] || first : i;
+      return test;
+    });
+
+    setVal(formatValue(getVal()));
+    el.setSelectionRange(i, j);
+    back = false;
+  };
+
+  const focusFormat = () => {
+    setVal(formatValue(getVal()));
+    setTimeout(() => {
+      const [i, j] = [el.selectionStart, el.selectionEnd].map((i) => {
+        i = clean(getVal().slice(0, i)).findIndex((c) => slots.has(c));
+        const test =
+          i < 0 ? prev[prev.length - 1] : back ? prev[i - 1] || first : i;
+        return test;
+      });
+      el.setSelectionRange(i, j);
+      back = false;
+    }, 100);
+  };
+
+  let back = false;
+
+  addEventListener(
+    el,
+    keyDownEvent,
+    (e) => (back = e.key === "Backspace"),
+    true
+  );
+  addEventListener(el, inputEvent, format, true);
+  addEventListener(el, focusEvent, focusFormat, true);
+  addEventListener(el, clickEvent, focusFormat, true);
+  addEventListener(
+    el,
+    blurEvent,
+    () => pattern.startsWith(getVal()) && setVal(""),
+    true
+  );
+
+  // mask any default values already set in the input
+  el.value = el.value;
+
+  el.alreadyMasked = true;
+}
+
+export { setupValidatorInput };
+
+export type { ValidatorOptions };
diff --git a/src/lib/rezact/vite-plugin.ts b/src/lib/rezact/vite-plugin.ts
index 221c734..6dedec6 100644
--- a/src/lib/rezact/vite-plugin.ts
+++ b/src/lib/rezact/vite-plugin.ts
@@ -798,6 +798,7 @@ function rezact(): PluginOption {
       if (id.includes("rezact/router.ts")) return;
       if (id.includes("signals.ts")) return;
       if (id.includes("mapState.ts")) return;
+      if (id.includes("validator.ts")) return;
       src = _src;
       itemsAlreadyImported = [];
       magicString = null;
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
new file mode 100644
index 0000000..5921663
--- /dev/null
+++ b/src/lib/utils.ts
@@ -0,0 +1,2 @@
+export const delay = (ms: number) =>
+  new Promise((resolve) => setTimeout(resolve, ms));
diff --git a/vite.config.ts b/vite.config.ts
index e49bbac..43398ce 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -42,6 +42,7 @@ export default defineConfig(({ command, mode, ssrBuild }) => {
           resolve(__dirname, "src/lib/rezact/router.ts"),
           resolve(__dirname, "src/lib/rezact/index.ts"),
           resolve(__dirname, "src/lib/rezact/signals.ts"),
+          resolve(__dirname, "src/lib/rezact/validator.ts"),
         ],
         name: "Rezact",
         // the proper extensions will be added


Test patch:
diff --git a/src/examples/EscapeHatches/__snapshots__/escapehatches.test.ts.snap b/src/examples/EscapeHatches/__snapshots__/escapehatches.test.ts.snap
new file mode 100644
index 0000000..bafc4d3
--- /dev/null
+++ b/src/examples/EscapeHatches/__snapshots__/escapehatches.test.ts.snap
@@ -0,0 +1,3 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Escape Hatch suite > Renders Escape Hatch Demo 1`] = `"<h1>Hello World</h1><div><div id=\\"iss14-1\\">Hallo, jesen</div><div id=\\"iss14-2\\">width: 10</div><div id=\\"iss14-3\\">height: 20</div><div id=\\"iss14-4\\">area: 200</div><button>Update Width</button><h1>Simple String List</h1><!--start map--><div dataval=\\"Jack\\"><input></div><div dataval=\\"Jill\\"><input></div><div dataval=\\"John\\"><input></div><div dataval=\\"Jane\\"><input></div><!--end map--><!--start map--><div>Jack</div><div>Jill</div><div>John</div><div>Jane</div><!--end map--></div>"`;
diff --git a/src/examples/EscapeHatches/escapehatches.test.ts b/src/examples/EscapeHatches/escapehatches.test.ts
index ab7cee1..d605763 100644
--- a/src/examples/EscapeHatches/escapehatches.test.ts
+++ b/src/examples/EscapeHatches/escapehatches.test.ts
@@ -2,11 +2,15 @@ import { describe, expect, it } from "vitest";
 import { screen } from "@testing-library/dom";
 import { render } from "rezact";
 import { Page } from "./EscapeHatches";
+import { delay } from "src/lib/utils";
 
 describe("Escape Hatch suite", () => {
-  it("Renders Escape Hatch Demo", () => {
+  it("Renders Escape Hatch Demo", async () => {
     render(document.body, Page);
+    await delay(1000);
     const allHeaders = screen.getAllByText(/hello world/i);
     expect(allHeaders).toHaveLength(1);
+
+    expect(document.body.innerHTML).toMatchSnapshot();
   });
 });
diff --git a/src/examples/FormValidation/formValidation.test.ts b/src/examples/FormValidation/formValidation.test.ts
new file mode 100644
index 0000000..916fda6
--- /dev/null
+++ b/src/examples/FormValidation/formValidation.test.ts
@@ -0,0 +1,125 @@
+import { describe, it, expect, beforeAll, afterAll } from "vitest";
+import { render } from "rezact";
+import { Page } from "./FormValidation"; // Adjust the import to the actual file path
+import { screen } from "@testing-library/dom";
+import userEvent from "@testing-library/user-event";
+import { delay } from "src/lib/utils";
+
+const user = userEvent.setup();
+let originalConsoleInfo = console.info;
+
+beforeAll(() => {
+  console.info = () => {};
+});
+
+afterAll(() => {
+  console.info = originalConsoleInfo;
+});
+
+describe("Page Component Tests", () => {
+  it("renders Page component and all input fields", async () => {
+    render(document.body, Page);
+    expect(screen.getByText("Validator Demo")).not.toBeNull();
+    await delay(100); //give the framework a bit to render everything
+
+    expect(screen.getByLabelText("Serial No.")).not.toBeNull();
+    expect(screen.getByLabelText("IP v4")).not.toBeNull();
+    expect(screen.getByLabelText("Name")).not.toBeNull();
+    expect(screen.getByLabelText("Telephone")).not.toBeNull();
+    expect(screen.getByLabelText("Street")).not.toBeNull();
+    expect(screen.getByLabelText("Zip")).not.toBeNull();
+    expect(screen.getByLabelText("Comments")).not.toBeNull();
+    expect(screen.getByLabelText("Credit Card")).not.toBeNull();
+    expect(screen.getByLabelText("Expiration")).not.toBeNull();
+    expect(screen.getByLabelText("CVV")).not.toBeNull();
+  });
+
+  it("renders placeholders correctly", async () => {
+    expect(screen.getByPlaceholderText("123.234.123.234")).not.toBeNull();
+    expect(screen.getByPlaceholderText("John Doe")).not.toBeNull();
+    expect(screen.getByPlaceholderText("(___) ___ ____")).not.toBeNull();
+    expect(screen.getByPlaceholderText("1234 Main St.")).not.toBeNull();
+    expect(screen.getByPlaceholderText("12345")).not.toBeNull();
+    expect(screen.getByPlaceholderText("MM/YY")).not.toBeNull();
+    expect(screen.getByPlaceholderText("000")).not.toBeNull();
+  });
+
+  it("reports error on required elements when submit button is clicked", async () => {
+    // Click the submit button
+    const submitButton = screen.getByRole("button", { name: /submit/i });
+    await user.click(submitButton);
+    await delay(100);
+
+    // Check if error elements/messages are rendered for required fields
+    const checkError = (labelText: string) => {
+      const inputElement = screen.getByLabelText(labelText);
+      const expectedErrorMessage = `${labelText} is required.`;
+      expect(inputElement.nextElementSibling.textContent).toBe(
+        expectedErrorMessage
+      );
+    };
+
+    checkError("Serial No.");
+    checkError("Name");
+    checkError("Telephone");
+    checkError("Street");
+    checkError("Zip");
+    checkError("Credit Card");
+    checkError("Expiration");
+    checkError("CVV");
+  });
+
+  it("renders and validates Serial No input", async () => {
+    const serialNoInput = screen.getByLabelText(
+      "Serial No."
+    ) as HTMLInputElement;
+
+    expect(serialNoInput.nextElementSibling.textContent).toBe(
+      "Serial No. is required."
+    );
+
+    // Test: Partial input should show "not complete" error
+    serialNoInput.value = "123";
+    serialNoInput.dispatchEvent(new Event("blur"));
+    expect(serialNoInput.nextElementSibling.textContent).toBe(
+      "Serial Number is not complete."
+    );
+    expect(serialNoInput.value).toBe("XXX 123_ XDX-( ____ )-{ ____ }-[ ____ ]");
+
+    // // Test: Invalid characters should not be accepted
+    serialNoInput.value = "XXX-ABCD-XDX-( EFGH )-{ IJKL }-[ MNOP ]";
+    expect(serialNoInput.value).toBe("XXX ____ XDX-( ____ )-{ ____ }-[ ____ ]"); // Invalid characters are not accepted
+
+    // // Test: Valid input should not show error
+    serialNoInput.value = "1231231231231231";
+    expect(serialNoInput.value).toBe("XXX 1231 XDX-( 2312 )-{ 3123 }-[ 1231 ]");
+    expect(serialNoInput.nextElementSibling.textContent).toBe("");
+  });
+
+  it("renders and validates Serial No input with unmasked value prop", async () => {
+    const serialNoInput = screen.getByLabelText(
+      "Serial2 No."
+    ) as HTMLInputElement;
+
+    expect(serialNoInput.nextElementSibling.textContent).toBe(
+      "Serial2 No. is required."
+    );
+
+    // Test: Partial input should show "not complete" error
+    serialNoInput.value = "XXX 123_ XDX-( ____ )-{ ____ }-[ ____ ]";
+    serialNoInput.dispatchEvent(new Event("blur"));
+    expect(serialNoInput.nextElementSibling.textContent).toBe(
+      "Serial Number is not complete."
+    );
+    expect(serialNoInput.value).toBe("123");
+
+    // // Test: Invalid characters should not be accepted
+    serialNoInput.value = "XXX-ABCD-XDX-( EFGH )-{ IJKL }-[ MNOP ]";
+    expect(serialNoInput.value).toBe(""); // Invalid characters are not accepted
+
+    // // Test: Valid input should not show error
+    serialNoInput.value = "XXX 1231 XDX-( 2312 )-{ 3123 }-[ 1231 ]";
+    expect(serialNoInput.value).toBe("1231231231231231");
+    expect(serialNoInput.nextElementSibling.textContent).toBe("");
+  });
+});