Validatori Custom
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 confn(funzione di validazione) eparameterized(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', { /* ... */ })