UNPKG

formoosejs

Version:

A simple, lightweight, and flexible form validation library

596 lines (532 loc) 17.1 kB
"use strict"; class Formoose { constructor(form) { this.forms = form || document.querySelectorAll("[data-formoose-form]"); // Rules for validation this.rules = { required: { test: (value, input) => { if (input.type === "radio" || input.type === "checkbox") { const form = input.closest("form"); const groupName = input.name; const group = form.querySelectorAll( `input[name="${groupName}"]:checked` ); // Get the checked inputs in the group return group.length > 0; } return value.trim() !== ""; }, message: "This field is required", }, email: { test: (value) => { const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return re.test(value); }, message: "Please enter a valid email address", }, min: { test: (value, input) => { return ( value.length >= parseInt(input.getAttribute("data-formoose-min")) ); }, message: (input) => { return `Please enter at least ${input.getAttribute( "data-formoose-min" )} characters`; }, }, max: { test: (value, input) => { return ( value.length <= parseInt(input.getAttribute("data-formoose-max")) ); }, message: (input) => { return `Please enter less than or equal to ${input.getAttribute( "data-formoose-max" )} characters`; }, }, number: { test: (value) => { return !isNaN(value); }, message: "Please enter a number", }, strongPassword: { test: (value) => { return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^a-zA-Z\d])/.test(value); }, message: "Password must contain at least one lowercase letter, one uppercase letter, one number, and one special character", }, same: { test: (value, input) => { const otherInput = input.getAttribute("data-formoose-same"); const otherValue = document.querySelector( `[name="${otherInput}"]` ).value; return value === otherValue; }, message: (input) => { return `Please enter the same value as ${input.getAttribute( "data-formoose-same" )}`; }, }, date: { test: (value) => { return !isNaN(Date.parse(value)); }, message: "Please enter a valid date", }, alpha: { test: (value) => { return /^[a-zA-Z]+$/.test(value); }, message: "Please enter only letters", }, alphaNumeric: { test: (value) => { return /^[a-zA-Z0-9]+$/.test(value); }, message: "Please enter only letters and numbers", }, alphaDash: { test: (value) => { return /^[a-zA-Z_-]+$/.test(value); }, message: "Please enter only letters, underscores, and dashes", }, accepted: { test: (value) => { return value === "on" || value === "yes" || value === "1"; }, message: "The field must be accepted", }, array: { test: (_value, input) => { const isArray = input.hasAttribute("data-formoose-array"); const name = input.name; if (!isArray || !name) { return false; } const inputs = document.querySelectorAll(`input[name="${name}"]`); const values = Array.from(inputs).map((el) => el.value); return Array.isArray(values) && name.endsWith("[]"); }, message: "Please enter a valid array", }, dateBefore: { test: (value, input) => { const dateRule = input.getAttribute("data-formoose-dateBefore"); return Date.parse(value) < Date.parse(dateRule); }, message: (input) => { return `Please enter a date before ${input.getAttribute( "data-formoose-dateBefore" )}`; }, }, dateAfter: { test: (value, input) => { const dateRule = input.getAttribute("data-formoose-dateAfter"); return Date.parse(value) > Date.parse(dateRule); }, message: (input) => { return `Please enter a date after ${input.getAttribute( "data-formoose-dateAfter" )}`; }, }, between: { test: (value, input) => { const values = input.getAttribute("data-formoose-between").split(","); const min = parseInt(values[0]); const max = parseInt(values[1]); return value >= min && value <= max; }, message: (input) => { let value = input.getAttribute("data-formoose-between"); let values = value.split(","); let min = parseInt(values[0]); let max = parseInt(values[1]); return `Please enter a value between ${min} and ${max}`; }, }, boolean: { test: (value) => { return ( value == "true" || value == "1" || value == "false" || value == "0" ); }, message: "Please enter a valid boolean", }, different: { test: (value, input) => { const otherValue = document.querySelector( `input[name="${input.getAttribute("data-formoose-different")}"]` ).value; return value !== otherValue; }, message: (input) => { return `Please enter a different value than ${input.getAttribute( "data-formoose-different" )}`; }, }, url: { test: (value) => { return /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/.test( value ); }, message: "Please enter a valid URL", }, in: { test: (value, input) => { const values = input.getAttribute("data-formoose-in").split(","); return values.includes(value); }, message: (input) => { return `Please enter a value in ${input.getAttribute( "data-formoose-in" )}`; }, }, notIn: { test: (value, input) => { const values = input.getAttribute("data-formoose-notIn").split(","); return !values.includes(value); }, message: (input) => { return `Please enter a value not in ${input.getAttribute( "data-formoose-notIn" )}`; }, }, startsWith: { test: (value, input) => { return value.startsWith( input.getAttribute("data-formoose-startsWith") ); }, message: (input) => { return `Please enter a value that starts with ${input.getAttribute( "data-formoose-startsWith" )}`; }, }, endsWith: { test: (value, input) => { return value.endsWith(input.getAttribute("data-formoose-endsWith")); }, message: (input) => { return `Please enter a value that ends with ${input.getAttribute( "data-formoose-endsWith" )}`; }, }, contains: { test: (value, input) => { return value.includes(input.getAttribute("data-formoose-contains")); }, message: (input) => { return `Please enter a value that contains ${input.getAttribute( "data-formoose-contains" )}`; }, }, notContains: { test: (value, input) => { return !value.includes( input.getAttribute("data-formoose-notContains") ); }, message: (input) => { return `Please enter a value that does not contain ${input.getAttribute( "data-formoose-notContains" )}`; }, }, phone: { test: (value) => { return /^\+[1-9]{1}[0-9]{0,2}[ /-]?[2-9]{1}[0-9]{2}[ /-]?[2-9]{1}[0-9]{2}[ /-]?[0-9]{4}$/.test( value ); }, message: () => { return "Please enter a valid phone number"; }, }, time: { test: (value) => { return /^([1-9]|0[1-9]|1[0-2]):[0-5][0-9] ([AaPp][Mm])$/.test(value); }, message: () => { return "Please enter a valid time e.g. 12:00 PM"; }, }, time24: { test: (value) => { return /^(?:[01][0-9]|2[0-3]):[0-5][0-9](?::[0-5][0-9])?$/.test( value ); }, message: () => { return "Please enter a valid 24-hour time e.g. 23:59"; }, }, ipv4: { test: (value) => { return /^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}$/.test( value ); }, message: () => { return "Please enter a valid IPv4 address"; }, }, ipv6: { test: (value) => { return /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/.test(value); }, message: () => { return "Please enter a valid IPv6 address"; }, }, macAddress: { test: (value) => { return /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/.test(value); }, message: () => { return "Please enter a valid MAC address"; }, }, image: { test: (value) => { if (!(value instanceof File)) { return false; } // Check if the type property exists and starts with "image/" return value.type && value.type.startsWith("image/"); }, message: () => { return "Please upload a valid image"; }, }, formats: { test: (value, input) => { const formats = input .getAttribute("data-formoose-formats") .split(","); const mimeTypes = formats.map((format) => `image/${format}`); return value instanceof File && mimeTypes.includes(value.type); }, message: (input) => { return `Please upload a file with one of the following formats: ${input.getAttribute( "data-formoose-formats" )}`; }, }, }; this.init(); } // Initialize Formoose init() { this.forms.forEach((form) => { form.addEventListener("submit", (e) => { e.preventDefault(); if (!this.validateForm(form)) { this.disableSubmitButton(form); } else { document.dispatchEvent( new Event("formoose:submitting", { detail: form }) ); this.disableSubmitButton(form); form.submit(); document.dispatchEvent( new Event("formoose:submitted", { detail: form }) ); } }); const inputs = form.querySelectorAll( "input, textarea, select, radio, checkbox" ); inputs.forEach((input) => { input.addEventListener("input", this.handleInputEvents); }); }); } handleInputEvents = (event) => { const input = event.target; const { isValid, errors } = this.validateInput(input); if (isValid) { this.removeErrors(input); } else { this.displayErrors(input, errors[0]); this.disableSubmitButton(input.closest("form")); } }; disableSubmitButton(form) { const submitButton = form.querySelector('button[type="submit"]'); submitButton.setAttribute("disabled", "disabled"); } enableSubmitButton(form) { const submitButton = form.querySelector('button[type="submit"]'); submitButton.removeAttribute("disabled"); } validateForm(form) { const inputs = form.querySelectorAll( "input, textarea, select, radio, checkbox" ); const uniqueGroups = new Set(); const results = Array.from(inputs).map((input) => { if ( (input.type === "radio" || input.type === "checkbox") && uniqueGroups.has(input.name) ) { return { input, isValid: true }; // Skip already validated group } if (input.type === "radio" || input.type === "checkbox") { uniqueGroups.add(input.name); } const { isValid, errors } = this.validateInput(input); if (isValid) { new Event("formoose:success", { detail: form }); this.removeErrors(input); } else { new Event("formoose:error", { detail: form }); this.displayErrors(input, errors[0]); } return { input, isValid }; }); return results.every((result) => result.isValid); } validateInput(input) { const form = input.closest("form"); const groupName = input.name; const isGrouped = input.type === "radio" || input.type === "checkbox"; const isFileInput = input.type === "file"; const group = isGrouped && (form.querySelector('input[type="radio"]') || form.querySelector('input[type="checkbox"]')) ? form.querySelectorAll(`input[name="${groupName}"]`) : [input]; const value = isFileInput ? input.files[0] : isGrouped ? null : input.value.trim(); const errors = Object.keys(this.rules) .filter((rule) => { if (isGrouped) return false; const hasRule = input.hasAttribute(`data-formoose-${rule}`); if (!hasRule) return false; // Special handling for file inputs if (isFileInput && rule === "image") { return !this.rules[rule].test(value, input); } // Default validation for non-file inputs return !this.rules[rule].test(value, input); }) .map((rule) => this.getMessage(input, rule)); const isValid = errors.length === 0; // If the input is a radio or checkbox, validate the group if (isGrouped) { // Validate the group as a whole const groupErrors = Object.keys(this.rules) .filter( (rule) => group[0].hasAttribute(`data-formoose-${rule}`) && !this.rules[rule].test(null, group[0]) ) .map((rule) => this.getMessage(group[0], rule)); return { isValid: groupErrors.length === 0, errors: groupErrors }; } return { isValid, errors }; } getMessage(input, rule) { const customMessage = input.getAttribute(`data-formoose-${rule}-message`); if (typeof this.rules[rule].message === "function") { return customMessage || this.rules[rule].message(input); } return customMessage || this.rules[rule].message || "Invalid value"; } formHasErrors = (form) => { return form.querySelectorAll(".formoose-invalid").length; }; displayErrors(input, error) { this.removeErrors(input); input.classList.add("formoose-invalid"); const container = this.createErrorContainer(); const errorElement = this.createErrorElement(); errorElement.textContent = error; container.appendChild(errorElement); if ( input.type === "radio" || input.type === "checkbox" || input.classList.contains("form-group") ) { const fieldset = input.closest("fieldset"); fieldset.appendChild(container); } else { container.appendChild(errorElement); input.insertAdjacentElement("afterend", container); } } createErrorContainer() { const container = document.createElement("div"); container.classList.add("formoose-invalid-feedback"); return container; } createErrorElement() { const error = document.createElement("p"); error.classList.add("formoose-error-message"); return error; } removeErrors(input) { const form = input.closest("form"); if ( input.type === "radio" || input.type === "checkbox" || input.classList.contains("form-group") ) { const group = input .closest("fieldset") .querySelector(".formoose-invalid-feedback"); if (group) { input.classList.remove("formoose-invalid"); group.remove(); } } else { input.classList.remove("formoose-invalid"); if ( input.nextElementSibling && input.nextElementSibling.classList.contains("formoose-invalid-feedback") ) { input.nextElementSibling.remove(); } } if (!this.formHasErrors(form)) { this.enableSubmitButton(form); } else { this.disableSubmitButton(form); } } addRule(name, rule, message) { // rule should be a function if (typeof rule !== "function") { throw new Error("The rule must be a function"); } // message should be a string or a function if (typeof message !== "string" && typeof message !== "function") { throw new Error("The message must be a string or a function"); } this.rules[name] = { test: rule, message: message || "Invalid value" }; } } export default Formoose;