Aller au contenu

Guides

Validation

La validation est attachée au spec, pas à l’UI. C’est cette seule décision qui permet au navigateur et à votre backend d’aboutir au même verdict à partir du même fichier — aucun schéma à synchroniser à la main, aucune règle qui n’existe que côté client.

Chaque brick porte un tableau validations. Ils sont compilés en JSON Schema (Ajv + ajv-errors + ajv-formats) au moment du rendu :

Validateur S’applique à value
required tout
minLength / maxLength chaîne nombre
pattern chaîne source de regex
email / url chaîne
min / max nombre nombre
minItems / maxItems tableau nombre
custom tout — (utilise customValidator)
Un email obligatoire et bien formé
{
"validations": [
{ "validator": "required" },
{ "validator": "email", "message": "Saisissez une adresse valide" }
]
}

message est optionnel et peut être localisé — voir messages et locales plus bas.

Quand les onze validateurs intégrés ne suffisent pas, custom exécute un extrait de code dans le bac à sable des règles :

// `value` est la valeur de ce champ, `dataMap` est tout le formulaire
return value !== dataMap.oldEmail || "Doit différer de l'adresse actuelle"

La valeur retournée décide du résultat :

Retour Résultat
true, undefined, null passe
une chaîne échoue, et la chaîne devient le message
une Error échoue avec error.message
tout le reste échoue avec le message configuré, ou le message par défaut intégré

Les messages se résolvent en cascade : d’abord le message de l’auteur, puis les messages par défaut de FormKrafter pour la locale active. L’anglais et le français sont fournis d’office.

Un message localisé
{ "validator": "required", "message": { "en": "Required", "fr": "Obligatoire" } }

Enregistrez d’autres langues au démarrage :

import { registerValidationMessages } from '@streamline-pulse/formkrafter-core'
registerValidationMessages('de', {
required: 'Pflichtfeld',
minLength: 'Mindestens {value} Zeichen',
})

Le placeholder {value} est interpolé avec le paramètre du validateur.

validateFormData est le point d’entrée backend. Il prend le même spec, le payload soumis et une locale optionnelle :

server/validate.ts
import { validateFormData } from '@streamline-pulse/formkrafter-core'
import type { BrickSpec } from '@streamline-pulse/formkrafter-core'
export function checkSubmission(spec: BrickSpec, payload: unknown) {
const { valid, errors } = validateFormData(spec, payload, 'fr')
if (!valid) throw new Error(JSON.stringify(errors))
return payload
}

Il reproduit trois comportements du frontend qu’un contrôle backend écrit à la main rate facilement :

  • Les lignes de collection sont validées individuellement, et remontent sous la forme contacts[0].email.
  • Les chaînes vides comptent comme absentes, si bien que required se comporte comme ce que l’utilisateur a vu.
  • Les champs cachés par une règle sont exclus. Un champ obligatoire à l’étape 3 qu’une règle masque ne peut jamais bloquer une soumission dont il ne fait pas partie.

Un spec peut porter des validations qui ne s’exécutent jamais. valid: true signifie alors rien n’a objecté, et non les règles sont passées — une nuance qui reste invisible jusqu’à ce que de mauvaises données soient déjà stockées. validateFormData la remonte via warnings :

const { valid, errors, warnings } = validateFormData(spec, payload)
if (warnings) {
console.warn(`${formId} accepte des soumissions sans certaines de ses règles :`)
for (const warning of warnings) console.warn(`${warning}`)
}

warnings est absent sur un spec sain : sa seule présence est le signal. Il ne change jamais valid — le verdict tient, il est simplement plus faible qu’il n’en a l’air. Deux causes le produisent : un champ portant des validations mais pas de dataType, ce qui ne laisse rien pour construire un schéma, et un schéma dont la compilation a échoué, par exemple un pattern à la regex invalide.

warnings attrape le problème au moment de la soumission. lintSpec l’attrape avant même que le spec ne soit stocké — il parcourt un spec et renvoie tout ce qui y est structurellement faux, sans aucune donnée :

import { lintSpec } from '@streamline-pulse/formkrafter-core'
for (const issue of lintSpec(spec)) {
console.error(`[${issue.code}] ${issue.path}${issue.message}`)
}
issue.code Signification
validations-without-data-type La brick a des règles mais pas de dataType : aucune n’est appliquée
input-without-key La valeur n’a nulle part où aller dans les données du formulaire
duplicate-key Deux bricks écrivent sur la même clé ; une valeur écrase l’autre
collection-without-children Une collection sans brick à répéter

Chaque problème porte code, path, key et un message lisible. Un tableau vide signifie que le spec est sain. C’est assez peu coûteux pour tourner en CI sur chaque spec que vous semez ou générez, et sortir en erreur sur un résultat non vide transforme toute une classe de bugs de données silencieux en build rouge.

Un projet de Streamline Pulse