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.