Stratégies Avancées de Validation des Formulaires avec NiceGUI

La validation des entrées utilisateur est un aspect fondamental du développement d'applications web modernes. Elle garantit l'intégrité des données et la stabilité du système. NiceGUI, un framework web léger basé sur Python, propose une API intuitive pour la gestion des composants interactifs front-end. Son mécanisme de validation pour les champs de texte (ui.text_input) est particulièrement efficace, permettant aux développeurs de fournir un feedback instantané aux utilisateurs avant la soumission des données, améliorant ainsi l'expérience globale.

1. Principes de base de la validation dans NiceGUI

Le composant ui.text_input de NiceGUI gère la validation des entrées dynamiquement via son paramètre validation. Ce paramètre attend un dictionnaire où chaque clé est un message d'erreur et chaque valeur est une fonction callable (par exemple, une expression lambda ou une fonction définie) qui retourne un booléen. Lorsque l'utilisateur modifie l'entrée, NiceGUI invoque automatiquement ces fonctions de validation. Si l'une d'elles retourne False, le message d'erreur correspondant s'affiche sous le champ.

Exemple de définition de règles de validation :

from nicegui import ui

def est_valide_nom_utilisateur(valeur: str) -> bool:
    """Vérifie que le nom d'utilisateur n'est pas vide et a une longueur minimale."""
    return bool(valeur) and len(valeur) >= 4

ui.text_input(
    label='Nom d\'utilisateur',
    placeholder='Entrez votre nom',
    validation={
        'Ce champ est obligatoire': lambda v: bool(v),
        'Minimum 4 caractères requis': est_valide_nom_utilisateur, # Réutilisation d'une fonction nommée
        'Ne peut contenir de chiffres': lambda v: not any(char.isdigit() for char in v)
    }
).classes('w-64') # Ajout d'une classe pour une légère modification structurelle.

ui.run()

Scénarios de validation courants :

Le tableau ci-dessous illustre des exemples de validation courante pour les champs de texte :

Type de besoin Logique ou expression régulière (Python) Description
Format e-mail import re; re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', value) Vérifie la conformité à un format d'e-mail standard.
Numéro de téléphone value.isdigit() and len(value) == 10 Vérifie si la valeur est composée de 10 chiffres (exemple pour la France).
Mot de passe fort re.search(r'[A-Z]', value) and re.search(r'[a-z]', value) and re.search(r'\d', value) and len(value) >= 8 S'assure de la présence de majuscules, minuscules, chiffres et d'une longueur minimale.

2. Mécanismes de validation fondamentaux

2.1 Fonctionnement des validateurs intégrés dans NiceGUI

Le système de validation de NiceGUI est déclenché par les événements d'entrée. Dès qu'un champ avec une règle de validation est modifié, les fonctions associées sont exécutées, et le feedback est mis à jour en temps réel.

from nicegui import ui

def est_age_valide(valeur: str) -> bool:
    """Vérifie si l'âge est un nombre valide et supérieur à 18."""
    try:
        age = int(valeur)
        return age >= 18
    except ValueError:
        return False

ui.number(
    label='Votre Âge',
    placeholder='Entrez votre âge',
    validation={
        'L\'âge doit être un nombre': lambda v: str(v).isdigit() if v is not None else False,
        'Vous devez avoir au moins 18 ans': lambda v: est_age_valide(str(v)) if v is not None else False
    }
)
ui.run()

2.2 Liaison manuelle et feedback en temps réel

Pour des contrôles plus fins ou des logiques complexes, il est possible de lier manuellement des fonctions de validation aux événements des champs et d'utiliser la méthode set_error() du composant pour afficher ou masquer les messages d'erreur.

from nicegui import ui

def valider_cle_produit(input_field: ui.text_input, valeur: str):
    """Valide une clé produit et met à jour l'état du champ."""
    if not valeur:
        input_field.set_error('La clé produit ne peut être vide.')
    elif not (len(valeur) == 5 and valeur.isalnum()):
        input_field.set_error('La clé doit être alphanumérique et de 5 caractères.')
    else:
        input_field.set_error(None) # Efface l'erreur

cle_produit_saisie = ui.text_input(label='Clé produit', placeholder='XXXXX')
# Utilisation de l'événement 'input' pour une validation en temps réel
cle_produit_saisie.on('input', lambda e: valider_cle_produit(cle_produit_saisie, e.value))
ui.run()

2.3 Gestion des champs obligatoires et des valeurs vides

La gestion des champs obligatoires est cruciale. Une simple vérification de non-vide peut être effectuée pour s'assurer que l'utilisateur a fourni une entrée.

from nicegui import ui

def est_champ_obligatoire(valeur: str | None) -> bool:
    """Vérifie si une valeur n'est pas vide après nettoyage des espaces."""
    return bool(valeur and valeur.strip())

ui.text_input(
    label='Nom d\'entreprise (obligatoire)',
    validation={'Ce champ est requis': est_champ_obligatoire}
)

ui.text_input(
    label='Code postal (facultatif)',
    placeholder='(Laissé vide si non applicable)'
)
ui.run()

2.4 Contraintes de longueur et de format pour les chaînes de caractères

Les contraintes sur la longueur et le format des chaînes garantissent que les données respectent les exigences structurelles. Python, avec son module re, offre une puissante capacité de vérification par expressions régulières.

import re
from nicegui import ui

def valider_code_produit_longueur(code: str) -> bool:
    return 3 <= len(code) <= 12

def valider_code_produit_format(code: str) -> bool:
    # Exemple de format: 2 lettres majuscules, 3 chiffres, 2 lettres majuscules (ex: AB123CD)
    return bool(re.fullmatch(r'^[A-Z]{2}\d{3}[A-Z]{2}$', code))

produit_id_input = ui.text_input(
    label='Code Produit Unique',
    placeholder='Ex: AB123CD',
    validation={
        'Doit contenir entre 3 et 12 caractères': valider_code_produit_longueur,
        'Format invalide (ex: AB123CD)': valider_code_produit_format
    }
)
ui.run()

2.5 Retour visuel de l'état de validation et expérience utilisateur

NiceGUI intègre un feedback visuel automatique : un champ invalide est encadré de rouge et le message d'erreur s'affiche en dessous. Cela rend l'interaction intuitive pour l'utilisateur sans nécessiter de code supplémentaire pour la gestion des styles.

3. Développement de règles de validation personnalisées

3.1 Création de validateurs de règles métier composites

Pour des scénarios métier complexes, il est souvent nécessaire de combiner plusieurs règles. Une fonction peut générer ou agréger ces règles pour un champ donné.

from nicegui import ui

def obtenir_regles_validation_profil() -> dict:
    """Génère un ensemble de règles de validation pour un champ de profil."""
    
    def valider_non_vide(valeur):
        return bool(valeur)
    
    def valider_longueur_minimale(valeur, longueur=5):
        return len(valeur) >= longueur
    
    def valider_commence_par_lettre(valeur):
        return bool(valeur) and valeur[0].isalpha()

    return {
        'Ce champ ne peut être vide': valider_non_vide,
        'Minimum 5 caractères requis': lambda v: valider_longueur_minimale(v, 5),
        'Doit commencer par une lettre': valider_commence_par_lettre
    }

ui.text_input(
    label='Nom du profil',
    validation=obtenir_regles_validation_profil()
)
ui.run()

3.2 Traitement réactif pour la validation asynchrone

Lorsqu'une validation nécessite un appel à une API distante (ex: vérification de disponibilité de nom d'utilisateur), il est essentiel d'implémenter un mécanisme de "débouncing" pour éviter des requêtes excessives. NiceGUI, combiné avec asyncio et ui.timer, permet de gérer cela efficacement.

import asyncio
from nicegui import ui

# Simule un appel API asynchrone pour la disponibilité d'un nom d'utilisateur
async def verifier_disponibilite_nom_utilisateur_api(username: str) -> bool:
    print(f"Vérification asynchrone pour: {username}...")
    await asyncio.sleep(0.8) # Simule un délai réseau
    utilisateurs_pris = ['admin', 'utilisateur', 'testeur']
    return username not in utilisateurs_pris

async def valider_champ_utilisateur_async(e):
    username = e.value
    input_field = e.sender

    input_field.set_error(None) # Efface l'erreur précédente immédiatement

    # Annule le minuteur précédent pour le débouncing
    if hasattr(input_field, '_debounce_timer'):
        input_field._debounce_timer.cancel()

    async def validation_differee():
        if not username:
            input_field.set_error('Le nom d\'utilisateur ne peut être vide.')
            return

        disponible = await verifier_disponibilite_nom_utilisateur_api(username)
        if not disponible:
            input_field.set_error('Ce nom d\'utilisateur est déjà pris.')
        else:
            input_field.set_error(None) # S'assure que l'erreur est effacée si valide

    # Planifie la validation après un court délai
    input_field._debounce_timer = ui.timer(0.5, validation_differee, once=True)

ui.label('Enregistrement Utilisateur').classes('text-lg font-bold')
champ_nom_utilisateur = ui.text_input(
    label='Nom d\'utilisateur',
    placeholder='Entrez un nom unique'
).on('input', valider_champ_utilisateur_async) # Utilise l'événement 'input'

ui.run()

3.3 Prise en charge multilingue et gestion des messages d'erreur internationalisés

Pour les applications internationales, les messages d'erreur doivent être localisés. Une approche consiste à stocker les messages dans des dictionnaires Python, puis à les récupérer en fonction de la langue de l'utilisateur.

from nicegui import ui

# Dictionnaire simulant une base de données de messages d'erreur localisés
MESSAGES_ERREUR_I18N = {
    'fr': {
        'champ_requis': 'Ce champ est obligatoire.',
        'email_invalide': 'Veuillez entrer une adresse e-mail valide.',
        'longueur_min_6': 'Minimum 6 caractères requis.',
        'age_mineur': 'Vous devez avoir au moins 18 ans.'
    },
    'en': {
        'champ_requis': 'This field is required.',
        'email_invalide': 'Please enter a valid email address.',
        'longueur_min_6': 'Minimum 6 characters required.',
        'age_mineur': 'You must be at least 18 years old.'
    }
}

# Simule la langue de l'utilisateur
langue_actuelle = 'fr'

def obtenir_message_localise(cle: str) -> str:
    """Récupère un message d'erreur localisé pour la langue actuelle."""
    return MESSAGES_ERREUR_I18N.get(langue_actuelle, {}).get(cle, f"Erreur inconnue: {cle}")

ui.text_input(
    label='Email',
    validation={
        obtenir_message_localise('champ_requis'): lambda v: bool(v),
        obtenir_message_localise('email_invalide'): lambda v: '@' in v and '.' in v if v else True
    }
)
ui.run()

4. Construction d'un système de validation de formulaires avancé

4.1 Stratégies pour la validation inter-champs

Dans des formulaires complexes, la validation d'un champ peut dépendre de la valeur d'un autre champ. NiceGUI permet de gérer cela en attachant des gestionnaires d'événements on('input') qui déclenchent la re-validation des champs liés.

from nicegui import ui

champ_mot_de_passe = None
champ_confirmer_mot_de_passe = None

def valider_mots_de_passe_correspondants(valeur_confirmation: str) -> bool:
    """Vérifie si le mot de passe de confirmation correspond au mot de passe principal."""
    return champ_mot_de_passe.value == valeur_confirmation if champ_mot_de_passe else False

def gerer_changement_mot_de_passe():
    """Déclenche la re-validation du champ de confirmation si le mot de passe principal change."""
    if champ_confirmer_mot_de_passe.value: # Seulement si le champ de confirmation a une valeur
        # Ré-exécute les validateurs du champ de confirmation
        validation_echec = not valider_mots_de_passe_correspondants(champ_confirmer_mot_de_passe.value)
        champ_confirmer_mot_de_passe.set_error(
            'Les mots de passe ne correspondent pas.' if validation_echec else None
        )

with ui.card().classes('w-96 p-4'):
    ui.label('Créer un nouveau mot de passe').classes('text-xl font-bold mb-4')

    champ_mot_de_passe = ui.text_input(
        'Nouveau mot de passe', password=True, password_toggle_button=True
    ).on('input', gerer_changement_mot_de_passe).classes('w-full mb-2')

    champ_confirmer_mot_de_passe = ui.text_input(
        'Confirmer mot de passe', password=True, password_toggle_button=True,
        validation={
            'Les mots de passe ne correspondent pas': valider_mots_de_passe_correspondants
        }
    ).classes('w-full mb-2')

ui.run()

4.2 Validation par lot au niveau du formulaire et génération de rapports

Pour les formulaires à plusieurs champs, il est souvent préférable de déclencher une validation globale lors de la soumission. Cela implique d'itérer sur tous les champs, d'exécuter leurs validateurs et de collecter les erreurs dans un rapport structuré.

from nicegui import ui
from typing import Optional, Dict

form_fields: Dict[str, ui.text_input] = {} # Dictionnaire pour stocker les références aux composants

def executer_validation_champ(champ_comp: ui.text_input) -> Optional[str]:
    """Exécute les validateurs d'un champ NiceGUI et retourne le premier message d'erreur ou None."""
    if champ_comp.validation:
        for message, validator_func in champ_comp.validation.items():
            if not validator_func(champ_comp.value or ''): # Passe la valeur actuelle du champ
                return message # Retourne le premier message d'erreur
    return None

def soumettre_formulaire_global():
    tous_valides = True
    rapport_erreurs = {}

    for nom_champ, input_comp in form_fields.items():
        erreur_champ = executer_validation_champ(input_comp)
        if erreur_champ:
            tous_valides = False
            rapport_erreurs[nom_champ] = erreur_champ
            input_comp.set_error(erreur_champ) # Marque visuellement l'erreur
        else:
            input_comp.set_error(None) # Efface les erreurs précédentes

    if tous_valides:
        ui.notify('Formulaire soumis avec succès !', type='positive')
        print({name: field.value for name, field in form_fields.items()})
    else:
        ui.notify('Veuillez corriger les erreurs du formulaire.', type='negative')
        print("Rapport d'erreurs:", rapport_erreurs)

with ui.card().classes('w-96 p-4'):
    ui.label('Formulaire d\'Inscription').classes('text-xl font-bold mb-4')

    form_fields['nom'] = ui.text_input('Votre Nom', validation={'Requis': lambda v: bool(v)}).classes('w-full mb-2')
    form_fields['email'] = ui.text_input(
        'Email', validation={
            'Requis': lambda v: bool(v),
            'Format invalide': lambda v: '@' in str(v) and '.' in str(v) if v else False
        }
    ).classes('w-full mb-2')
    form_fields['age'] = ui.number('Âge', validation={'Requis': lambda v: v is not None, 'Mineur': lambda v: v >= 18 if v is not None else False}).classes('w-full mb-4')

    ui.button('Soumettre', on_click=soumettre_formulaire_global).classes('w-full')

ui.run()

4.3 Mécanisme de validation à la demande pour les éléments de formulaire dynamiques

Les formulaires peuvent inclure des champs qui apparaissent ou disparaissent en fonction de l'interaction utilisateur. Il est essentiel de ne valider que les champs actuellement visibles et pertinents. NiceGUI facilite cela avec bind_visibility_from et en intégrant la logique de visibilité directement dans les validateurs.

from nicegui import ui

def soumettre_formulaire_dynamique():
    erreurs_trouvees = False

    # Validation du champ principal (toujours visible)
    if not champ_nom_principal.value:
        champ_nom_principal.set_error('Le nom est obligatoire.')
        erreurs_trouvees = True
    else:
        champ_nom_principal.set_error(None)

    # Validation du champ secondaire uniquement s'il est visible et obligatoire
    if checkbox_afficher_secondaire.value:
        if not champ_secondaire_optionnel.value:
            champ_secondaire_optionnel.set_error('Ce champ secondaire est obligatoire si affiché.')
            erreurs_trouvees = True
        else:
            champ_secondaire_optionnel.set_error(None)
    # Si le champ n'est pas affiché, on s'assure qu'il n'y a pas d'erreur affichée
    else:
        champ_secondaire_optionnel.set_error(None)
    
    if not erreurs_trouvees:
        ui.notify('Formulaire dynamique valide !', type='positive')
        print(f"Nom: {champ_nom_principal.value}, Secondaire: {champ_secondaire_optionnel.value if checkbox_afficher_secondaire.value else 'N/A'}")
    else:
        ui.notify('Veuillez corriger les erreurs du formulaire.', type='negative')


with ui.card().classes('p-4 w-96'):
    ui.label('Formulaire Dynamique').classes('text-lg font-bold mb-2')

    champ_nom_principal = ui.text_input('Nom Principal', validation={'Obligatoire': lambda v: bool(v)}).classes('w-full mb-2')

    checkbox_afficher_secondaire = ui.checkbox('Afficher le champ secondaire').classes('mb-2')
    
    # Le champ secondaire est initialement caché et sa visibilité est liée à la checkbox
    champ_secondaire_optionnel = ui.text_input('Champ Secondaire (optionnel)',
        validation={'Obligatoire si affiché': lambda v: bool(v) if checkbox_afficher_secondaire.value else True}
    ).classes('w-full mb-2')
    champ_secondaire_optionnel.bind_visibility_from(checkbox_afficher_secondaire, 'value')

    ui.button('Valider le Formulaire', on_click=soumettre_formulaire_dynamique).classes('w-full')

ui.run()

4.4 Conception d'une interaction de validation accessible et conviviale au clavier

L'accessibilité est primordiale. Les composants NiceGUI sont conçus pour être accessibles par défaut. Il est crucial d'utiliser des labels clairs et des messages d'erreur descriptifs. Pour la navigation au clavier, NiceGUI gère l'ordre de tabulation de manière logique. Bien qu'il n'y ait pas de méthode directe focus() pour déplacer le curseur vers le premier champ en erreur après une validation de soumission depuis le backend, l'indication visuelle et textuelle des erreurs reste claire pour tous les utilisateurs, y compris ceux utilisant des lecteurs d'écran.

Étiquettes: NiceGUI Python ValidationFormulaire DéveloppementWeb UIUX

Publié le 7 août à 02h27