Validators

Validatori Custom

Come creare e registrare validatori personalizzati tramite ValidationService.registerValidator.

Panoramica

Il sistema di validazione di @pzeta/vue-form è estensibile tramite ValidationService.registerValidator. Puoi registrare validatori personalizzati con lo stesso contratto degli validatori built-in, utilizzarli nelle regole pipe-separated e fornire messaggi i18n personalizzati.

API di registrazione

ValidationService.registerValidator

ValidationService.registerValidator(name: string, validator: Validator): void

Parametri:

  • name — nome del validatore, utilizzato nella stringa di regole (es. 'myValidator')
  • validator — oggetto con fn (funzione di validazione) e parameterized (booleano)

Tipo Validator:

interface Validator {
  fn: (value: unknown, param?: string) => ValidationResult
  parameterized: boolean  // true se il validatore usa sintassi "nome:parametro"
}

interface ValidationResult {
  isValid: boolean
  errors: string[]  // chiavi i18n, formato: "namespace.key|param1:val1,param2:val2"
}

Esempi di registrazione

Validatore senza parametri

// Registrazione
ValidationService.registerValidator('notEmpty', {
  fn: (value) => {
    const isValid = value !== '' && value !== null && value !== undefined
    return {
      isValid,
      errors: isValid ? [] : ['validation.custom.notEmpty']
    }
  },
  parameterized: false
})

// Uso
ValidationService.validate('hello', 'notEmpty')  // { isValid: true, errors: [] }
ValidationService.validate('', 'notEmpty')        // { isValid: false, errors: ['validation.custom.notEmpty'] }

// In schema DynamicFormLayouts
{ name: 'campo', type: 'text', validation: 'notEmpty' }

Validatore con parametro

// Registrazione
ValidationService.registerValidator('divisibleBy', {
  fn: (value, divisor) => {
    const num = Number(value)
    const div = Number(divisor)
    if (Number.isNaN(num) || Number.isNaN(div) || div === 0) {
      return { isValid: false, errors: ['validation.custom.divisibleByInvalidParam'] }
    }
    const isValid = num % div === 0
    return {
      isValid,
      errors: isValid ? [] : [`validation.custom.divisibleBy|divisor:${divisor}`]
    }
  },
  parameterized: true
})

// Uso
ValidationService.validate(10, 'divisibleBy:5')  // valido: 10 / 5 = 2
ValidationService.validate(7, 'divisibleBy:3')   // non valido: 7 % 3 !== 0

// In schema DynamicFormLayouts
{ name: 'quantita', type: 'number', validation: 'required|integer|divisibleBy:10' }

Validatore con logica asincrona (pattern wrapper)

Per validatori che richiedono logica complessa (es. verifica unicità via API), usare un wrapper sincrono con stato condiviso o un approccio a due stadi:

// Pre-validazione (es. risultato da API memorizzato in reactive store)
ValidationService.registerValidator('uniqueEmail', {
  fn: (value) => {
    // La verifica effettiva è avvenuta altrove; qui si legge il risultato
    const isValid = !takenEmails.has(String(value))
    return {
      isValid,
      errors: isValid ? [] : ['validation.custom.emailTaken']
    }
  },
  parameterized: false
})

Naming delle chiavi i18n

Le chiavi errore dei validatori custom seguono la convenzione:

validation.custom.<nomeErrore>

Per errori con parametri:

validation.custom.<nomeErrore>|param1:valore1,param2:valore2

Esempi:

// Errore semplice
errors: ['validation.custom.notEmpty']

// Errore con parametro divisore
errors: [`validation.custom.divisibleBy|divisor:${divisor}`]

// Errore con due parametri
errors: [`validation.custom.rangeBetween|min:${min},max:${max}`]

Il framework i18n interpreta la chiave prima del | per trovare il messaggio, e i parametri dopo | per la sostituzione dei placeholder.


Utility: ValidationService.getAvailableValidators

ValidationService.getAvailableValidators(): string[]

Restituisce la lista di tutti i nomi di validatori registrati (built-in + custom).

const validators = ValidationService.getAvailableValidators()
// => ['required', 'min', 'max', 'email', ..., 'divisibleBy', 'uniqueEmail']

// Utile per debugging e tooling
console.log('Validatori disponibili:', validators.join(', '))

Best practice

Gestire il valore vuoto

Tutti i validatori built-in usano skipEmpty per considerare i valori vuoti come validi (eccetto required). Replicare questo comportamento nei validatori custom per coerenza:

import { skipEmpty } from '@pzeta/vue-form'

ValidationService.registerValidator('minWords', {
  fn: (value, param) => {
    const empty = skipEmpty(value)
    if (empty) return empty  // valore vuoto → valido

    const wordCount = String(value).trim().split(/\s+/).length
    const min = parseInt(param || '1', 10)
    const isValid = wordCount >= min
    return {
      isValid,
      errors: isValid ? [] : [`validation.custom.minWords|min:${min}`]
    }
  },
  parameterized: true
})

Validare il tipo prima della logica

ValidationService.registerValidator('italianPhone', {
  fn: (value) => {
    const empty = skipEmpty(value)
    if (empty) return empty

    if (typeof value !== 'string') {
      return { isValid: false, errors: ['validation.common.mustBeString'] }
    }

    const phone = value.replace(/\D/g, '')
    const isValid = /^3\d{8,9}$/.test(phone)
    return {
      isValid,
      errors: isValid ? [] : ['validation.custom.italianMobile']
    }
  },
  parameterized: false
})

Struttura file consigliata per validatori custom

// src/validators/custom.ts
import { ValidationService, skipEmpty } from '@pzeta/vue-form'

// Registra tutti i validatori custom in una funzione
export function registerCustomValidators(): void {
  ValidationService.registerValidator('divisibleBy', { /* ... */ })
  ValidationService.registerValidator('uniqueEmail', { /* ... */ })
  ValidationService.registerValidator('italianPhone', { /* ... */ })
}
// main.ts o plugin.ts
import { registerCustomValidators } from '@/validators/custom'

registerCustomValidators()

Non sovrascrivere i validatori built-in

Registrare un validatore con lo stesso nome di uno built-in lo sovrascrive. Usare nomi con namespace per evitare conflitti:

// Da evitare
ValidationService.registerValidator('email', { /* ... */ })  // sovrascrive il built-in!

// Corretto
ValidationService.registerValidator('emailWithDomain', { /* ... */ })
ValidationService.registerValidator('businessEmail', { /* ... */ })