import type { Meta, StoryObj } from '@storybook/vue3' import { ref, watch, onMounted } from 'vue' import { VBtn, VForm } from 'vuetify/components' import SyTextField from '@/components/Customs/SyTextField/SyTextField.vue' import { fn } from '@storybook/test' import SyForm from '@/components/Customs/SyForm/SyForm.vue' import { getValidationDocumentation } from '@/composables/unifyValidation/documentationValidationProps' import type { FieldValidationProps } from '@/composables/unifyValidation/useValidation' const meta = { title: 'Composants/Formulaires/SyTextField/Validation', component: SyTextField, decorators: [ () => ({ template: '
', }), ], parameters: { layout: 'fullscreen', docs: { description: { component: `Exemples de validation pour le composant SyTextField`, }, }, }, argTypes: { ...getValidationDocumentation('string'), modelValue: { control: 'text', description: 'Valeur du champ texte', }, label: { control: 'text', description: 'Libellé du champ', }, }, args: { 'modelValue': '', 'label': 'Nom', 'required': false, 'errorMessages': null, 'warningMessages': null, 'successMessages': null, 'readonly': false, 'disabled': false, 'customRules': [], 'customWarningRules': [], 'customSuccessRules': [], 'isValidateOnBlur': true, 'onUpdate:modelValue': fn(), }, } as Meta export default meta type Story = StoryObj export const WithError: Story = { parameters: { a11y: { disable: true, }, sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { label: 'Adresse email', modelValue: 'not-an-email', customRules: [ { type: 'custom', options: { validate: (value: string) => { const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) if (!valid) { return 'L\'adresse email est invalide.' } return true }, fieldIdentifier: 'email', }, }, ], }, render: args => ({ components: { SyTextField }, setup() { const value = ref(args.modelValue) const fieldRef = ref<{ validateOnSubmit: () => Promise } | null>(null) watch(() => args.modelValue, (newValue) => { value.value = newValue }) onMounted(() => { fieldRef.value?.validateOnSubmit() }) return { args, value, fieldRef } }, template: `
`, }), } export const WithWarning: Story = { parameters: { a11y: { disable: true, }, sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { label: 'Nom d\'utilisateur', modelValue: 'ab', customWarningRules: [ { type: 'custom', options: { validate: (value: string) => value.length >= 3, warningMessage: 'Le nom d\'utilisateur est très court (moins de 3 caractères).', fieldIdentifier: 'username', }, }, ], }, render: args => ({ components: { SyTextField }, setup() { const value = ref(args.modelValue) const fieldRef = ref<{ validateOnSubmit: () => Promise } | null>(null) watch(() => args.modelValue, (newValue) => { value.value = newValue }) onMounted(() => { fieldRef.value?.validateOnSubmit() }) return { args, value, fieldRef } }, template: `
`, }), } export const WithSuccess: Story = { parameters: { a11y: { disable: true, }, sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { label: 'Adresse email', modelValue: 'exemple@domaine.fr', showSuccessMessages: true, customSuccessRules: [ { type: 'custom', options: { validate: (value: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value), successMessage: 'L\'adresse email est valide.', fieldIdentifier: 'email', }, }, ], }, render: args => ({ components: { SyTextField }, setup() { const value = ref(args.modelValue) const fieldRef = ref<{ validateOnSubmit: () => Promise } | null>(null) watch(() => args.modelValue, (newValue) => { value.value = newValue }) onMounted(() => { fieldRef.value?.validateOnSubmit() }) return { args, value, fieldRef } }, template: `
`, }), } export const WithCustomRules: Story = { parameters: { a11y: { disable: true, }, sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, render: args => ({ components: { SyTextField }, setup() { const value = ref(args.modelValue) watch(() => args.modelValue, (newValue) => { value.value = newValue }) const customRules = [ { type: 'custom', options: { validate: (value: string) => { if (value.length < 8) return false return true }, message: 'Le mot de passe doit contenir au moins 8 caractères.', fieldIdentifier: 'password', }, }, { type: 'custom', options: { validate: (value: string) => { if (!/[A-Z]/.test(value)) return false return true }, message: 'Le mot de passe doit contenir au moins une majuscule.', fieldIdentifier: 'password', }, }, ] return { args, value, customRules } }, template: ` `, }), } export const DisableErrorHandling: Story = { parameters: { a11y: { disable: true, }, docs: { description: { story: ` ### Désactivation de la gestion des erreurs Cette story illustre l'utilisation de la propriété \`disableErrorHandling\` qui permet de désactiver complètement la gestion et l'affichage des erreurs dans un champ, même si des règles de validation sont définies. `, }, }, sourceCode: [ { name: 'Template', code: ``, }, ], }, render: () => ({ components: { SyTextField }, setup() { const value1 = ref('') const value2 = ref('') const customRules = [ { type: 'custom', options: { validate: (value: string) => { if (!value || value.trim().length === 0) { return false } return true }, message: 'Ce champ est requis.', fieldIdentifier: 'field', }, }, ] return { value1, value2, customRules } }, template: `

Cette démonstration compare un SyTextField standard et un avec disableErrorHandling=true.

Validation normale

Sans gestion d'erreurs

Instructions :

  1. Cliquez dans un champ puis en dehors pour déclencher la validation
  2. Le champ de gauche affichera une erreur requise, mais pas celui de droite
`, }), } /** * Validation déclenchée à chaque frappe (isValidateOnBlur: false). */ export const ValidateOnInput: Story = { parameters: { docs: { description: { story: ` ### Validation à la saisie Lorsque \`isValidateOnBlur\` vaut \`false\`, la validation se déclenche à chaque modification de la valeur plutôt qu'à la perte de focus. Utile pour un retour immédiat à l'utilisateur. `, }, }, sourceCode: [ { name: 'Template', code: ``, }, { name: 'Script', code: ``, }, ], }, render: args => ({ components: { SyTextField }, setup() { const value = ref(args.modelValue) watch(() => args.modelValue, (newValue) => { value.value = newValue }) return { args, value } }, template: `

La validation se déclenche à chaque frappe (isValidateOnBlur="false").

`, }), } /** * Messages de validation injectés directement par le parent (errorMessages, warningMessages, successMessages). */ export const ExternalMessages: Story = { parameters: { docs: { description: { story: ` ### Messages externes Les props \`errorMessages\`, \`warningMessages\` et \`successMessages\` permettent d'injecter des messages depuis le parent sans déclencher de règle de validation. `, }, }, sourceCode: [ { name: 'Template', code: ``, }, { name: 'Script', code: ``, }, ], }, args: { showSuccessMessages: true, }, render: args => ({ components: { SyTextField, VBtn }, setup() { const value = ref(args.modelValue) watch(() => args.modelValue, (newValue) => { value.value = newValue }) const errorMessages = ref(null) const warningMessages = ref(null) const successMessages = ref(null) function setError() { errorMessages.value = ['Ce nom est déjà utilisé'] warningMessages.value = null successMessages.value = null } function setWarning() { errorMessages.value = null warningMessages.value = ['Ce nom ressemble à un nom générique'] successMessages.value = null } function setSuccess() { errorMessages.value = null warningMessages.value = null successMessages.value = ['Nom accepté par le serveur'] } function reset() { errorMessages.value = null warningMessages.value = null successMessages.value = null } return { args, value, errorMessages, warningMessages, successMessages, setError, setWarning, setSuccess, reset } }, template: `

Les messages ci-dessous sont injectés par le parent sans déclencher de règle de validation.

Simuler une erreur Simuler un avertissement Simuler un succès Réinitialiser
`, }), } export const VFormVuetifyValidation: Story = { parameters: { docs: { description: { story: ` ### Validation de style Vuetify En passant \`useVuetifyValidation="true"\`, le composant délègue la validation à Vuetify. Les règles sont de simples fonctions qui retournent \`true\` si la valeur est valide, ou un message d'erreur (chaîne de caractères) sinon — exactement comme avec la prop \`rules\` native de Vuetify. `, }, }, sourceCode: [ { name: 'Template', code: ``, }, { name: 'Script', code: ``, }, ], }, render: args => ({ components: { SyTextField, VBtn, VForm }, setup() { const value = ref(args.modelValue) watch(() => args.modelValue, (newValue) => { value.value = newValue }) const rules = [ (value: string) => !!value || 'Ce champ est requis', (value: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) || 'L\'adresse email est invalide', ] async function handleSubmit(e: Promise<{ valid: boolean }>) { const result = await e alert(result.valid ? 'Valeur valide !' : 'Veuillez corriger les erreurs.') } return { args, value, rules, handleSubmit } }, template: `

Les règles sont des fonctions Vuetify natives (value) => true | 'message'. Cliquez sur Valider ou quittez le champ pour déclencher la validation.

Valider
`, }), } export const SyFormValidation: Story = { parameters: { sourceCode: [ { name: 'Template', code: ``, }, { name: 'Script', code: ``, }, ], }, render: args => ({ components: { SyTextField, VBtn, SyForm }, setup() { const value = ref(args.modelValue) watch(() => args.modelValue, (newValue) => { value.value = newValue }) const customRules = [ { type: 'custom', options: { validate: (value: string) => { if (!value || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) { return false } return true }, message: 'L\'adresse email est invalide.', fieldIdentifier: 'email', }, }, ] function handleSubmit(e: { isValid: boolean }) { const isValid = e.isValid alert(isValid ? 'Valeur valide !' : 'Veuillez corriger les erreurs.') } return { args, value, customRules, handleSubmit } }, template: `
Valider
`, }), } export const SyFormVuetifyValidation: Story = { parameters: { sourceCode: [ { name: 'Template', code: ``, }, { name: 'Script', code: ``, }, ], }, render: args => ({ components: { SyTextField, VBtn, SyForm }, setup() { const value = ref(args.modelValue) watch(() => args.modelValue, (newValue) => { value.value = newValue }) const vuetifyRules = [ (value: string) => !!value || 'Ce champ est requis', (value: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) || 'L\'adresse email est invalide', ] function handleSubmit(e: { isValid: boolean }) { const isValid = e.isValid alert(isValid ? 'Valeur valide !' : 'Veuillez corriger les erreurs.') } return { args, value, vuetifyRules, handleSubmit } }, template: `
Valider
`, }), } export const VFormValidation: Story = { parameters: { sourceCode: [ { name: 'Template', code: ``, }, { name: 'Script', code: ``, }, ], }, render: args => ({ components: { SyTextField, VBtn, VForm }, setup() { const value = ref(args.modelValue) watch(() => args.modelValue, (newValue) => { value.value = newValue }) const textFieldRef = ref() async function handleSubmit() { if (textFieldRef.value) { const result = await textFieldRef.value.validateOnSubmit() alert(result ? 'Valeur valide !' : 'Veuillez corriger les erreurs.') } } return { args, value, textFieldRef, handleSubmit } }, template: `

Il faut privilégier l'utilisation de SyForm pour bénéficier de intégration.

Valider
`, }), } export const EmailValidation: Story = { parameters: { docs: { description: { story: ` ### Validation d'email Cette story montre un cas d'usage courant : la validation d'une adresse email. Le champ : - Est requis - Vérifie le format de l'email - Affiche un message de succès quand l'email est valide `, }, }, sourceCode: [ { name: 'Template', code: ``, }, ], }, render: args => ({ components: { SyTextField }, setup() { const value = ref(args.modelValue) watch(() => args.modelValue, (newValue) => { value.value = newValue }) return { args, value } }, template: ` `, }), }