A copy-paste helper class that wires multiple fields, blur events, and submit logic in one place. Not part of the package — use it as a starting point and adapt it to your needs.
Form class is not included in @andresclua/validate. Copy Form.js into your project and import the validators from the library as usual.
// copy this file into your project as Form.js
import {
isString, isEmail, isNumber,
isSelect, isCheckbox, isRadio, isFile,
} from '@andresclua/validate'
class Form {
constructor({
element, fields,
submitButtonSelector = null,
beforeSubmit = null,
onSubmit = null,
onError = null,
validators = {},
}) {
if (!element) throw new Error('A form element is required.')
this.fields = fields || []
this.beforeSubmit = beforeSubmit
this.onSubmit = onSubmit
this.onError = onError
this.DOM = {
form: element,
submitButton: submitButtonSelector
? element.querySelector(submitButtonSelector)
: null,
}
this.validators = {
isString, isEmail, isNumber,
isSelect, isCheckbox, isRadio, isFile,
...validators,
}
this._listeners = []
this.init()
this.events()
}
init() { this.initializeFields() }
events() { this.initializeSubmit() }
getValidator(fn) {
if (typeof fn === 'function') return fn
if (typeof fn === 'string') {
const v = this.validators[fn]
if (!v) throw new Error(`Validator "${fn}" is not registered.`)
return v
}
throw new Error('validationFunction must be a function or a string key.')
}
runField(field) {
const { element, elements, validationFunction, config } = field
const validator = this.getValidator(validationFunction)
if (elements) {
const result = validator({ elements, config })
this.updateFieldState(field, result)
return result
}
if (!(element instanceof Element))
throw new Error('Field must have `element` or `elements`.')
if (element.tagName === 'INPUT' && element.type === 'file') {
const result = validator({ element: element.files?.[0] || null, config })
this.updateFieldState(field, result)
return result
}
const result = validator({ element: element.value, config })
this.updateFieldState(field, result)
return result
}
initializeFields() {
this.fields.forEach((field) => {
if (field.on === null) return
const eventType = field.on || 'blur'
const target = field.element || field.elements?.[0]
if (!target) throw new Error('Each field must have `element` or `elements`.')
const handler = () => this.runField(field)
target.addEventListener(eventType, handler)
this._listeners.push({ target, event: eventType, handler })
})
}
updateFieldState(field, result) {
const baseEl = field.element || field.elements?.[0]
if (!(baseEl instanceof Element)) return
const formGroup = baseEl.closest('.c--form-group-a')
let errorSpan = formGroup?.querySelector('.c--form-error-a')
if (!errorSpan && formGroup) {
errorSpan = document.createElement('span')
errorSpan.classList.add('c--form-error-a')
formGroup.appendChild(errorSpan)
}
if (field.elements) {
if (errorSpan) errorSpan.textContent = result.isValid ? '' : result.errorMessage
return
}
const isFile = baseEl.tagName === 'INPUT' && baseEl.type === 'file'
const wrapper = isFile ? baseEl : baseEl.closest('.c--form-input-a')
const baseClass = isFile ? 'c--form-file-a' : 'c--form-input-a'
if (!wrapper) return
if (result.isValid) {
wrapper.classList.remove(`${baseClass}--error`)
wrapper.classList.add(`${baseClass}--valid`)
if (errorSpan) errorSpan.textContent = ''
} else {
wrapper.classList.add(`${baseClass}--error`)
wrapper.classList.remove(`${baseClass}--valid`)
if (errorSpan) errorSpan.textContent = result.errorMessage
}
}
validateAllFields() {
const invalidFields = []
this.fields.forEach((field) => {
const result = this.runField(field)
if (!result?.isValid)
invalidFields.push({ field, errorMessage: result?.errorMessage || 'Invalid' })
})
return invalidFields
}
initializeSubmit() {
if (this.DOM.submitButton) {
const handler = (e) => { e.preventDefault(); this.handleValidation() }
this.DOM.submitButton.addEventListener('click', handler)
this._listeners.push({ target: this.DOM.submitButton, event: 'click', handler })
}
const formHandler = (e) => { e.preventDefault(); this.handleValidation() }
this.DOM.form.addEventListener('submit', formHandler)
this._listeners.push({ target: this.DOM.form, event: 'submit', handler: formHandler })
}
handleValidation() {
if (this.beforeSubmit && this.beforeSubmit() === false) return
const invalidFields = this.validateAllFields()
if (invalidFields.length === 0) { if (this.onSubmit) this.onSubmit() }
else { if (this.onError) this.onError(invalidFields) }
}
destroy() {
this._listeners.forEach(({ target, event, handler }) =>
target.removeEventListener(event, handler))
this._listeners = []
}
}
export default Form
Form constructor options:
element Element The <form> element to validate
fields array Array of field configurations (see below)
submitButtonSelector string? CSS selector for submit button inside the form
beforeSubmit fn? Called before validation — return false to cancel
onSubmit fn? Called when all fields are valid
onError fn? Called with array of invalid fields
Field configuration:
element Element The input / textarea / select DOM element
elements NodeList For checkbox / radio groups (use instead of element)
validationFunction string|fn Built-in name ("isEmail", "isString"…) or custom fn
config object? Validator config passed directly to the validator
on string? Event type ("blur", "input", "change") — null = submit only
Login — two string fields, different rules
import Form from './Form.js'
new Form({
element: document.getElementById('login-form'),
fields: [
{
element: document.querySelector('#username'),
validationFunction: 'isString',
config: {
required: true,
minLength: 3,
maxLength: 20,
customMessage: {
required: 'Username is required.',
minLength: 'Min 3 characters.',
maxLength: 'Max 20 characters.'
}
},
on: 'blur'
},
{
element: document.querySelector('#password'),
validationFunction: 'isString',
config: {
required: true,
minLength: 8,
customMessage: {
required: 'Password is required.',
minLength: 'At least 8 characters.'
}
},
on: 'blur'
}
],
submitButtonSelector: '#login-btn',
onSubmit: () => console.log('Credentials valid'),
onError: (invalid) => console.log('Fix:', invalid)
})
<form id="login-form" novalidate>
<div class="c--form-group-a">
<label class="c--label-a" for="username">Username</label>
<div class="c--form-input-a">
<input class="c--form-input-a__item" type="text" id="username">
</div>
<span class="c--form-error-a"></span>
</div>
<div class="c--form-group-a">
<label class="c--label-a" for="password">Password</label>
<div class="c--form-input-a">
<input class="c--form-input-a__item" type="password" id="password">
</div>
<span class="c--form-error-a"></span>
</div>
<button id="login-btn" type="submit">Login</button>
</form>
Bio — name (short) + bio (textarea), different minLength
import Form from './Form.js'
new Form({
element: document.getElementById('bio-form'),
fields: [
{
element: document.querySelector('#name'),
validationFunction: 'isString',
config: {
required: true,
minLength: 2,
maxLength: 30,
customMessage: {
required: 'Name is required.',
minLength: 'Min 2 characters.',
maxLength: 'Max 30 characters.'
}
},
on: 'blur'
},
{
element: document.querySelector('#bio'), // <textarea> works too
validationFunction: 'isString',
config: {
required: true,
minLength: 40,
customMessage: {
required: 'Bio is required.',
minLength: 'Write at least 40 characters.'
}
},
on: 'blur'
}
],
submitButtonSelector: '#save-btn',
onSubmit: () => console.log('Profile saved'),
onError: (invalid) => console.log('Fix:', invalid)
})
<form id="bio-form" novalidate>
<div class="c--form-group-a">
<label class="c--label-a" for="name">Name</label>
<div class="c--form-input-a">
<input class="c--form-input-a__item" type="text" id="name">
</div>
<span class="c--form-error-a"></span>
</div>
<div class="c--form-group-a">
<label class="c--label-a" for="bio">Bio</label>
<div class="c--form-input-a">
<textarea class="c--form-input-a__item" id="bio"></textarea>
</div>
<span class="c--form-error-a"></span>
</div>
<button id="save-btn" type="submit">Save</button>
</form>
Text challenge + checkbox — button unlocks when both conditions are met
import Form from './Form.js'
const textInput = document.querySelector('#match-text')
const acceptCb = document.querySelector('#accept-cb')
const submitBtn = document.querySelector('#submit-btn')
// Enable button only when both conditions are satisfied in real time
function check() {
submitBtn.disabled = !(textInput.value === 'Lorem ipsum' && acceptCb.checked)
}
textInput.addEventListener('input', check)
acceptCb.addEventListener('change', check)
submitBtn.disabled = true // start locked
new Form({
element: document.getElementById('match-form'),
fields: [
{
element: textInput,
validationFunction: 'isString',
config: {
required: true,
pattern: /^Lorem ipsum$/,
customMessage: { pattern: 'Type exactly: Lorem ipsum' }
},
on: 'input'
},
{
elements: document.querySelectorAll('#accept-cb'),
validationFunction: 'isCheckbox',
config: {
minRequired: 1,
customMessage: { minRequired: 'You must accept the terms.' }
},
on: 'change'
}
],
submitButtonSelector: '#submit-btn',
onSubmit: () => console.log('Terms accepted'),
onError: (invalid) => console.log('Fix:', invalid)
})
<form id="match-form" novalidate>
<div class="c--form-group-a">
<label class="c--label-a" for="match-text">Type exactly: Lorem ipsum</label>
<div class="c--form-input-a">
<input class="c--form-input-a__item" type="text" id="match-text" autocomplete="off">
</div>
<span class="c--form-error-a"></span>
</div>
<div class="c--form-group-a">
<div class="c--form-checkbox-a">
<input type="checkbox" class="c--form-checkbox-a__item" id="accept-cb">
<label class="c--form-checkbox-a__title" for="accept-cb">
I accept the terms and conditions
</label>
</div>
<span class="c--form-error-a"></span>
</div>
<button id="submit-btn" type="submit" disabled>Submit</button>
</form>
Password confirm — custom validator comparing two fields
import Form from './Form.js'
const passwordInput = document.querySelector('#password')
// Custom validator: confirm must match password
const matchPassword = ({ element: value }) => ({
isValid: !!value && value === passwordInput.value,
errorMessage: value ? 'Passwords do not match.' : 'Please confirm your password.'
})
new Form({
element: document.getElementById('pw-form'),
fields: [
{
element: passwordInput,
validationFunction: 'isString',
config: {
required: true,
minLength: 8,
customMessage: {
required: 'Password is required.',
minLength: 'At least 8 characters.'
}
},
on: 'blur'
},
{
element: document.querySelector('#confirm'),
validationFunction: matchPassword, // custom fn, no config needed
config: {},
on: 'blur'
}
],
submitButtonSelector: '#set-password-btn',
onSubmit: () => console.log('Password set'),
onError: (invalid) => console.log('Fix:', invalid)
})
<form id="pw-form" novalidate>
<div class="c--form-group-a">
<label class="c--label-a" for="password">Password</label>
<div class="c--form-input-a">
<input class="c--form-input-a__item" type="password" id="password">
</div>
<span class="c--form-error-a"></span>
</div>
<div class="c--form-group-a">
<label class="c--label-a" for="confirm">Confirm password</label>
<div class="c--form-input-a">
<input class="c--form-input-a__item" type="password" id="confirm">
</div>
<span class="c--form-error-a"></span>
</div>
<button id="set-password-btn" type="submit">Set password</button>
</form>