@shezy26/validajs
Version:
Universal form validation library with Laravel-style syntax for vanilla JS, React, and Vue
2 lines (1 loc) • 17.8 kB
JavaScript
var ValidaJS=function(t){"use strict";function e(t){return null!=t&&("string"==typeof t?t.trim().length>0:!Array.isArray(t)||t.length>0)}function r(t){return!t||!isNaN(parseFloat(t))&&isFinite(t)}function i(t,e){return!t||e.includes(String(t))}function n(t){return["yes","on","1",1,!0,"true"].includes(t)}function s(t){return["no","off","0",0,!1,"false"].includes(t)}function a(t){if(!t)return!0;const e=new Date(t);return!isNaN(e.getTime())}var u=Object.freeze({__proto__:null,accepted:n,accepted_if:function(t,[e,r],i){const s=i[e];return String(s)!==String(r)||n(t)},active_url:function(t){if(!t)return!0;try{const e=new URL(t);return"http:"===e.protocol||"https:"===e.protocol}catch{return!1}},after:function(t,[e]){if(!t)return!0;const r=new Date(t),i=new Date(e);return!isNaN(r.getTime())&&!isNaN(i.getTime())&&r>i},after_or_equal:function(t,[e]){if(!t)return!0;const r=new Date(t),i=new Date(e);return!isNaN(r.getTime())&&!isNaN(i.getTime())&&r>=i},alpha:function(t){return!t||/^[a-zA-Z]+$/.test(String(t))},alpha_dash:function(t){return!t||/^[a-zA-Z0-9_-]+$/.test(String(t))},alpha_num:function(t){return!t||/^[a-zA-Z0-9]+$/.test(String(t))},array:function(t){return null==t||Array.isArray(t)},ascii:function(t){return!t||/^[\x00-\x7F]*$/.test(String(t))},before:function(t,[e]){if(!t)return!0;const r=new Date(t),i=new Date(e);return!isNaN(r.getTime())&&!isNaN(i.getTime())&&r<i},before_or_equal:function(t,[e]){if(!t)return!0;const r=new Date(t),i=new Date(e);return!isNaN(r.getTime())&&!isNaN(i.getTime())&&r<=i},between:function(t,[e,r]){if(!t)return!0;e=Number(e),r=Number(r);const i=Number(t);if(!isNaN(i)&&isFinite(i))return i>=e&&i<=r;if("string"==typeof t){const i=t.length;return i>=e&&i<=r}if(Array.isArray(t)){const i=t.length;return i>=e&&i<=r}return!0},boolean:function(t){return null==t||[!0,!1,1,0,"1","0","true","false"].includes(t)},confirmed:function(t,e,r,i){return!t||t===r[`${i}_confirmation`]},date:a,date_equals:function(t,[e]){if(!t)return!0;const r=new Date(t),i=new Date(e);return!isNaN(r.getTime())&&!isNaN(i.getTime())&&r.toDateString()===i.toDateString()},date_format:function(t,[e]){return!t||("Y-m-d"===e?/^\d{4}-\d{2}-\d{2}$/.test(t)&&a(t):"d/m/Y"===e||"m/d/Y"===e?/^\d{2}\/\d{2}\/\d{4}$/.test(t):a(t))},decimal:function(t,[e,i]){if(!t)return!0;if(!r(t))return!1;const n=String(t).split(".");if(1===n.length)return void 0===e||0===Number(e);const s=n[1].length;return void 0!==i?s>=(Number(e)||0)&&s<=Number(i):s===Number(e)},declined:s,declined_if:function(t,[e,r],i){const n=i[e];return String(n)!==String(r)||s(t)},different:function(t,[e],r){return!t||t!==r[e]},digits:function(t,[e]){if(!t)return!0;const r=String(t);return/^\d+$/.test(r)&&r.length===Number(e)},digits_between:function(t,[e,r]){if(!t)return!0;const i=String(t),n=i.length;return/^\d+$/.test(i)&&n>=Number(e)&&n<=Number(r)},distinct:function(t){return null==t||(!Array.isArray(t)||[...new Set(t)].length===t.length)},doesnt_end_with:function(t,e){if(!t)return!0;const r=String(t);return!e.some(t=>r.endsWith(t))},doesnt_start_with:function(t,e){if(!t)return!0;const r=String(t);return!e.some(t=>r.startsWith(t))},email:function(t){return!t||/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(t))},ends_with:function(t,e){if(!t)return!0;const r=String(t);return e.some(t=>r.endsWith(t))},filled:function(t){return null==t||("string"==typeof t?t.trim().length>0:!Array.isArray(t)||t.length>0)},gt:function(t,[e],r){if(!t)return!0;const i=r[e];return void 0===i||Number(t)>Number(i)},gte:function(t,[e],r){if(!t)return!0;const i=r[e];return void 0===i||Number(t)>=Number(i)},hex_color:function(t){return!t||/^#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(String(t))},in:i,in_rule:i,integer:function(t){return!t||Number.isInteger(Number(t))},ip:function(t){if(!t)return!0;if(/^(\d{1,3}\.){3}\d{1,3}$/.test(t)){return t.split(".").every(t=>Number(t)>=0&&Number(t)<=255)}return/^([0-9a-fA-F]{0,4}:){7}[0-9a-fA-F]{0,4}$/.test(t)},ipv4:function(t){return!t||!!/^(\d{1,3}\.){3}\d{1,3}$/.test(t)&&t.split(".").every(t=>Number(t)>=0&&Number(t)<=255)},ipv6:function(t){return!t||/^([0-9a-fA-F]{0,4}:){7}[0-9a-fA-F]{0,4}$/.test(t)},json:function(t){if(!t)return!0;try{return JSON.parse(t),!0}catch{return!1}},lowercase:function(t){return!t||String(t)===String(t).toLowerCase()},lt:function(t,[e],r){if(!t)return!0;const i=r[e];return void 0===i||Number(t)<Number(i)},lte:function(t,[e],r){if(!t)return!0;const i=r[e];return void 0===i||Number(t)<=Number(i)},mac_address:function(t){return!t||/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/.test(String(t))},max:function(t,[e]){return!t||(e=Number(e),"string"==typeof t?t.length<=e:"number"==typeof t?t<=e:!Array.isArray(t)||t.length<=e)},max_digits:function(t,[e]){return!t||String(t).replace(/[^0-9]/g,"").length<=Number(e)},min:function(t,[e]){return!t||(e=Number(e),"string"==typeof t?t.length>=e:"number"==typeof t?t>=e:!Array.isArray(t)||t.length>=e)},min_digits:function(t,[e]){return!t||String(t).replace(/[^0-9]/g,"").length>=Number(e)},multiple_of:function(t,[e]){if(!t)return!0;const r=Number(t),i=Number(e);return!isNaN(r)&&!isNaN(i)&&r%i===0},not_in:function(t,e){return!t||!e.includes(String(t))},not_regex:function(t,[e]){if(!t)return!0;try{return!new RegExp(e).test(String(t))}catch{return console.warn(`Invalid regex pattern: ${e}`),!1}},nullable:function(t){return!0},numeric:r,password:function(t,[e=8]){if(!t)return!0;const r=Number(e);if(t.length<r)return!1;const i=/[a-z]/.test(t),n=/[A-Z]/.test(t),s=/[0-9]/.test(t),a=/[!@#$%^&*(),.?":{}|<>]/.test(t);return i&&n&&s&&a},present:function(t){return void 0!==t},prohibited:function(t){return null==t||("string"==typeof t?0===t.trim().length:!!Array.isArray(t)&&0===t.length)},regex:function(t,[e]){if(!t)return!0;try{return new RegExp(e).test(String(t))}catch{return console.warn(`Invalid regex pattern: ${e}`),!1}},required:e,required_if:function(t,[r,i],n){const s=n[r];return String(s)!==String(i)||e(t)},required_unless:function(t,[r,...i],n){const s=n[r];return!!i.some(t=>String(s)===String(t))||e(t)},required_with:function(t,r,i){return!r.some(t=>{const e=i[t];return null!=e&&""!==e})||e(t)},required_with_all:function(t,r,i){return!r.every(t=>{const e=i[t];return null!=e&&""!==e})||e(t)},required_without:function(t,r,i){return!r.some(t=>{const e=i[t];return null==e||""===e})||e(t)},required_without_all:function(t,r,i){return!r.every(t=>{const e=i[t];return null==e||""===e})||e(t)},same:function(t,[e],r){return!t||t===r[e]},size:function(t,[e]){return!t||(e=Number(e),"string"==typeof t?t.length===e:"number"==typeof t?t===e:!Array.isArray(t)||t.length===e)},starts_with:function(t,e){if(!t)return!0;const r=String(t);return e.some(t=>r.startsWith(t))},string:function(t){return null==t||"string"==typeof t},timezone:function(t){if(!t)return!0;try{return Intl.DateTimeFormat(void 0,{timeZone:t}),!0}catch{return!1}},ulid:function(t){return!t||/^[0-7][0-9A-HJKMNP-TV-Z]{25}$/i.test(String(t))},uppercase:function(t){return!t||String(t)===String(t).toUpperCase()},url:function(t){if(!t)return!0;try{return new URL(t),!0}catch{return!1}},uuid:function(t){return!t||/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(String(t))}});class o{constructor(){this.rules=new Map,this.registerDefaultRules()}registerDefaultRules(){for(const[t,e]of Object.entries(u))this.register(t,e)}register(t,e){if("function"!=typeof e)throw new Error(`Validator for rule "${t}" must be a function`);this.rules.set(t,e)}validate(t,e,r=[],i={},n=""){const s=this.rules.get(t);if(!s)return console.warn(`Validation rule "${t}" not found`),!0;try{return s(e,r,i,n)}catch(e){return console.error(`Error executing validation rule "${t}":`,e),!1}}has(t){return this.rules.has(t)}getAllRules(){return Array.from(this.rules.keys())}unregister(t){return this.rules.delete(t)}}function l(t){if("string"!=typeof t)return{name:t,params:[]};const e=t.split(":");return{name:e[0],params:e[1]?e[1].split(","):[]}}function h(t){const e={};for(const[r,i]of Object.entries(t))Array.isArray(i)?e[r]=i:e[r]="string"==typeof i?i.split("|"):[i];return e}const c={required:"The :attribute field is required.",email:"The :attribute must be a valid email address.",min:"The :attribute must be at least :min characters.",max:"The :attribute may not be greater than :max characters.",between:"The :attribute must be between :min and :max.",numeric:"The :attribute must be a number.",integer:"The :attribute must be an integer.",alpha:"The :attribute may only contain letters.",alpha_dash:"The :attribute may only contain letters, numbers, dashes and underscores.",alpha_num:"The :attribute may only contain letters and numbers.",url:"The :attribute must be a valid URL.",same:"The :attribute and :other must match.",different:"The :attribute and :other must be different.",confirmed:"The :attribute confirmation does not match.",in:"The selected :attribute is invalid.",not_in:"The selected :attribute is invalid.",boolean:"The :attribute field must be true or false.",accepted:"The :attribute must be accepted.",declined:"The :attribute must be declined.",size:"The :attribute must be :size.",digits:"The :attribute must be :digits digits.",digits_between:"The :attribute must be between :min and :max digits.",date:"The :attribute is not a valid date.",before:"The :attribute must be a date before :date.",after:"The :attribute must be a date after :date.",before_or_equal:"The :attribute must be a date before or equal to :date.",after_or_equal:"The :attribute must be a date after or equal to :date.",regex:"The :attribute format is invalid.",string:"The :attribute must be a string.",nullable:"The :attribute field is optional.",array:"The :attribute must be an array.",starts_with:"The :attribute must start with one of the following: :values.",ends_with:"The :attribute must end with one of the following: :values.",lowercase:"The :attribute must be lowercase.",uppercase:"The :attribute must be uppercase.",ip:"The :attribute must be a valid IP address.",ipv4:"The :attribute must be a valid IPv4 address.",ipv6:"The :attribute must be a valid IPv6 address.",json:"The :attribute must be a valid JSON string.",uuid:"The :attribute must be a valid UUID.",gt:"The :attribute must be greater than :other.",gte:"The :attribute must be greater than or equal to :other.",lt:"The :attribute must be less than :other.",lte:"The :attribute must be less than or equal to :other.",required_if:"The :attribute field is required when :other is :value.",required_with:"The :attribute field is required when :values is present.",required_without:"The :attribute field is required when :values is not present.",ulid:"The :attribute must be a valid ULID.",mac_address:"The :attribute must be a valid MAC address.",ascii:"The :attribute must only contain single-byte alphanumeric characters and symbols.",hex_color:"The :attribute must be a valid hexadecimal color.",password:"The :attribute must be at least :min characters and contain uppercase, lowercase, numbers, and symbols.",max_digits:"The :attribute must not have more than :max digits.",min_digits:"The :attribute must have at least :min digits.",decimal:"The :attribute must have :decimal decimal places.",multiple_of:"The :attribute must be a multiple of :value.",active_url:"The :attribute is not a valid URL.",timezone:"The :attribute must be a valid timezone.",date_equals:"The :attribute must be a date equal to :date.",date_format:"The :attribute does not match the format :format.",not_regex:"The :attribute format must not match the given pattern.",doesnt_start_with:"The :attribute may not start with one of the following: :values.",doesnt_end_with:"The :attribute may not end with one of the following: :values.",present:"The :attribute field must be present.",filled:"The :attribute field must have a value.",prohibited:"The :attribute field is prohibited.",distinct:"The :attribute field has a duplicate value.",required_unless:"The :attribute field is required unless :other is in :values.",required_with_all:"The :attribute field is required when :values are present.",required_without_all:"The :attribute field is required when none of :values are present.",accepted_if:"The :attribute must be accepted when :other is :value.",declined_if:"The :attribute must be declined when :other is :value."};class d{constructor(t,e={}){this.schema=t,this.options={realtime:!0,validateOnBlur:!0,validateOnInput:!0,validateOnSubmit:!0,messages:{},...e},this.ruleEngine=new o,this.errors={},this.touched={},this.values={},this.customMessages={...c,...this.options.messages}}validateField(t,e,r={}){const i=this.schema[t];if(!i||!Array.isArray(i))return null;delete this.errors[t];for(const n of i){const{name:i,params:s}=l(n);if(!this.ruleEngine.validate(i,e,s,r,t)){this.errors[t]=this.getErrorMessage(t,i,s);break}}return this.errors[t]||null}validateAll(t){this.errors={},this.values=t;for(const e in this.schema){const r=t[e];this.validateField(e,r,t)}return{isValid:0===Object.keys(this.errors).length,errors:this.errors}}getErrorMessage(t,e,r){const i=`${t}.${e}`;if(this.customMessages[i])return this.formatMessage(this.customMessages[i],t,r);if(this.customMessages[e])return this.formatMessage(this.customMessages[e],t,r);const n=c[e]||"The :attribute field is invalid.";return this.formatMessage(n,t,r)}formatMessage(t,e,r=[]){let i=t.replace(":attribute",this.formatFieldName(e));return r.length>0&&(i=i.replace(":min",r[0]),i=i.replace(":size",r[0]),i=i.replace(":other",r[0]),i=i.replace(":date",r[0]),i=i.replace(":digits",r[0]),i=i.replace(":value",r[0])),r.length>1&&(i=i.replace(":max",r[1])),r.forEach((t,e)=>{i=i.replace(`:param${e}`,t)}),r.length>0&&(i=i.replace(":values",r.join(", "))),i}formatFieldName(t){return t.replace(/_/g," ").replace(/\b\w/g,t=>t.toUpperCase())}touch(t){this.touched[t]=!0}isTouched(t){return!!this.touched[t]}getAllValues(){return this.values}clearErrors(){this.errors={}}clearFieldError(t){delete this.errors[t]}getErrors(){return this.errors}hasErrors(){return Object.keys(this.errors).length>0}getFieldError(t){return this.errors[t]||null}reset(){this.errors={},this.touched={},this.values={}}}class m extends d{constructor(t,e,r={}){if(super(h(e||{}),r),this.formElement="string"==typeof t?document.querySelector(t):t,!this.formElement)throw new Error(`Form element not found: ${t}`);this.inputs={},this.errorElements={};const i=this.formElement.getAttribute("data-schema");i&&!e?this.loadSchemaFromFile(i):this.init()}async loadSchemaFromFile(t){try{const e=await fetch(t),r=await e.json();this.schema=h(r),this.init()}catch(e){throw console.error("Failed to load schema:",e),new Error(`Could not load schema from ${t}`)}}init(){for(const t in this.schema){const e=this.formElement.querySelector(`[name="${t}"]`);e&&(this.inputs[t]=e,this.attachEventListeners(t,e))}this.options.validateOnSubmit&&(this.handleSubmitBound=t=>this.handleSubmit(t),this.formElement.addEventListener("submit",this.handleSubmitBound))}attachEventListeners(t,e){if(this.options.validateOnBlur){const r=()=>{this.touch(t),this.handleBlur(t)};e.addEventListener("blur",r),e._blurHandler=r}if(this.options.validateOnInput){const r=()=>{this.isTouched(t)&&this.handleInput(t)};e.addEventListener("input",r),e._inputHandler=r}}handleBlur(t){const e=this.inputs[t],r=this.getInputValue(e),i=this.getAllFormValues(),n=this.validateField(t,r,i);this.displayError(t,n)}handleInput(t){const e=this.inputs[t],r=this.getInputValue(e),i=this.getAllFormValues(),n=this.validateField(t,r,i);this.displayError(t,n)}handleSubmit(t){t.preventDefault();const e=this.getAllFormValues(),r=this.validateAll(e);for(const t in this.schema){const e=this.errors[t]||null;this.displayError(t,e),this.touch(t)}r.isValid?this.options.onSuccess?this.options.onSuccess(e):this.formElement.submit():(this.options.onError&&this.options.onError(r.errors),this.focusFirstError())}getInputValue(t){if(!t)return"";if("checkbox"===t.type)return t.checked;if("radio"===t.type){const e=this.formElement.querySelector(`[name="${t.name}"]:checked`);return e?e.value:""}return"file"===t.type?t.files:t.value}getAllFormValues(){const t={};for(const e in this.inputs)t[e]=this.getInputValue(this.inputs[e]);return t}getAllValues(){return this.getAllFormValues()}displayError(t,e){const r=this.inputs[t];if(r)if(this.removeErrorDisplay(t),e&&null!==e){r.classList.add("invalid","error"),r.classList.remove("valid");const i=this.createErrorElement(e);this.errorElements[t]=i;const n=this.options.errorContainer?r.closest(this.options.errorContainer):r.parentElement;n?n.appendChild(i):r.insertAdjacentElement("afterend",i)}else this.isTouched(t)&&r.value&&(r.classList.add("valid"),r.classList.remove("invalid","error"))}createErrorElement(t){const e=document.createElement("span");return e.className=this.options.errorClass||"error-message",e.textContent=t,e.style.color="red",e.style.fontSize="0.875rem",e.style.marginTop="0.25rem",e.style.display="block",e}removeErrorDisplay(t){const e=this.errorElements[t];e&&e.parentElement&&(e.parentElement.removeChild(e),delete this.errorElements[t]);const r=this.inputs[t];r&&r.classList.remove("invalid","error")}focusFirstError(){for(const t in this.errors){const e=this.inputs[t];if(e){e.focus();break}}}validate(){const t=this.getAllFormValues();return this.validateAll(t)}reset(){super.reset();for(const t in this.inputs)this.removeErrorDisplay(t),this.inputs[t].classList.remove("valid","invalid","error")}destroy(){this.handleSubmitBound&&this.formElement.removeEventListener("submit",this.handleSubmitBound);for(const t in this.inputs){const e=this.inputs[t];e._blurHandler&&e.removeEventListener("blur",e._blurHandler),e._inputHandler&&e.removeEventListener("input",e._inputHandler),this.removeErrorDisplay(t)}this.inputs={},this.errorElements={}}}return t.RuleEngine=o,t.ValidaJS=m,t.Validator=d,t.default=m,Object.defineProperty(t,"__esModule",{value:!0}),t}({});