Advanced — Optional Helper: Form Class

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.

The 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>
Playground

✓ Credentials valid

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>
Playground

✓ Profile saved

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>
Playground

✓ Terms accepted

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>
Playground

✓ Password set