Validation

Vue d'ensemble

rs-grid valide une édition de cellule avant qu'elle soit validée. La validation s'exécute en deux couches, vérifiées dans l'ordre :

  1. Règles déclaratives (ColumnDef::rules: Vec<ValidationRule>) — la façon recommandée de valider. Les règles intégrées couvrent les cas courants (Required, MinLength, MaxLength, Range, OneOf), plus une échappatoire Custom.
  2. Validateur historique (ColumnDef::validator: Option<CellValidator>) — une closure libre, toujours prise en charge pour compatibilité ascendante. Il ne s'exécute qu'une fois toutes les règles passées.
StartEdit → l'utilisateur saisit → ValidateEdit (à chaque frappe, live) → CommitEdit
  → règles (dans l'ordre) → validateur historique (le cas échéant)
      Ok(())   → patch écrit → on_change()
      Err(msg) → InvalidEditMode décide de la suite

La validation est appliquée à l'intérieur de GridState::apply, pas seulement au niveau de l'interface — elle s'applique donc à tout consommateur (tests natifs, rendu canvas, tout futur renderer personnalisé). Elle est aussi vérifiée en dehors d'une session d'édition : coller ignore les cellules cibles invalides (voir Validation au collage ci-dessous), et le rendu canvas signale toute cellule dont la valeur actuelle échoue la validation même si elle n'a jamais été éditée (voir Indicateur au repos ci-dessous).

Ajouter des règles

Le sucre syntaxique sur ColumnDef couvre les cas courants :

use rs_grid_core::column::ColumnDef;

let name = ColumnDef::new("name", "Nom", 160.0).required();

let bio = ColumnDef::new("bio", "Bio", 240.0)
    .with_min_length(10)
    .with_max_length(500);

let age = ColumnDef::new("age", "Âge", 80.0).with_range(0.0, 130.0);

let status = ColumnDef::new("status", "Statut", 120.0)
    .with_allowed_values(vec!["active".into(), "inactive".into()]);

Ou définissez la liste complète directement avec ValidationRule, par exemple pour une règle Custom :

use rs_grid_core::{
    column::ColumnDef,
    validation::ValidationRule,
};

let sku = ColumnDef::new("sku", "SKU", 120.0).with_rules(vec![
    ValidationRule::required(),
    ValidationRule::Custom(CellValidator::new(|v| {
        v.starts_with("SKU-")
            .then_some(())
            .ok_or_else(|| "Doit commencer par SKU-".to_string())
    })),
]);

Variantes de ValidationRule :

VarianteRejette quand
RequiredLa valeur est vide
MinLength(usize)Moins de caractères que le minimum
MaxLength(usize)Plus de caractères que le maximum
Range(f64, f64)La valeur ne se parse pas en nombre, ou sort de min..=max
OneOf(Vec<String>)La valeur n'est pas l'une des chaînes autorisées
Custom(CellValidator)La closure encapsulée retourne Err

Les règles s'exécutent dans l'ordre, la première échec l'emporte. Une fois toutes les règles passées, le validateur historique (s'il est défini) s'exécute toujours :

pub fn validate_value(&self, value: &str) -> Result<(), String>

Annuler ou bloquer : InvalidEditMode

GridModel::invalid_edit_mode (par défaut Revert) contrôle ce qui se passe quand un CommitEdit échoue à la validation :

ModeComportement
Revert (défaut)La session d'édition se termine et la cellule revient à sa valeur précédente — comme avant l'existence de la validation.
BlockLa session d'édition reste ouverte avec l'erreur attachée, pour que l'utilisateur puisse corriger la valeur sur place au lieu de la perdre.

Le définir à la construction :

use rs_grid_core::{GridModelBuilder, validation::InvalidEditMode};

let model = GridModelBuilder::new(columns, Box::new(data))
    .invalid_edit_mode(InvalidEditMode::Block)
    .build();

ou le basculer à l'exécution avec GridCommand::SetInvalidEditMode(mode).

Les deux modes émettent CommandOutput::ValidationError { row, col_key, message }.

Retour live pendant la saisie

GridCommand::ValidateEdit { value } revérifie la valeur en cours d'édition sans valider — c'est un no-op sans édition active et ça ne crée aucune entrée d'annulation. rs-grid-web le déclenche automatiquement à chaque frappe, si bien que l'édition en cours reflète toujours l'état de validation courant, pas seulement la dernière tentative de validation.

Lire l'état live à la demande :

pub fn validation_error(&self) -> Option<(u64, String, String)>

Some((row, col_key, message)) tant que l'édition en cours est invalide, None sinon.

Écouter la validation

Il existe deux callbacks — choisissez selon le moment où vous devez réagir :

CallbackSe déclencheÀ utiliser pour
on_validation_state_changedChaque StartEdit / ValidateEdit / CommitEdit / CancelEdit — donc à chaque frappeUne interface de validation live et personnalisée (tooltip, bannière, icône) qui suit la valeur au fil de la saisie
on_validation_errorSeulement quand un CommitEdit est rejetéUne notification ponctuelle (toast, log) quand une tentative de sauvegarde échoue
Leptos
Vanilla JS
Dioxus
Yew
let validation_state = RwSignal::new(None::<(u64, String, String)>);

view! {
    <GridCanvas
        model=model
        on_validation_state_changed=Some(Box::new(move |state| {
            validation_state.set(state);
        }))
        on_validation_error=Some(Box::new(move |_row, col, msg| {
            log::warn!("[{col}] rejeté : {msg}");
        }))
    />
}

rs-grid n'impose pas de widget d'erreur de validation (pas de composant tooltip intégré) — il expose l'état brut pour que vous puissiez en construire un avec votre propre framework/CSS.

Positionner une interface de validation personnalisée

GridCanvas::cell_client_rect(row, col_key) retourne le rectangle en espace client de la cellule (left, top, width, height) en pixels CSS relatifs à la page — prêt à positionner un tooltip ou une bannière en position: fixed à côté de la cellule en erreur :

if let Some((row, col_key, message)) = canvas.validation_error() {
    if let Some((left, top, _w, height)) = canvas.cell_client_rect(row, &col_key) {
        show_tooltip(left, top + height + 4.0, &message); // juste sous la cellule
    }
}

Retourne None si col_key n'existe pas. C'est de la géométrie pure — ça ne vérifie pas si la cellule est actuellement visible dans le viewport.

Indicateur au repos

Une cellule peut être invalide sans jamais avoir été éditée — par exemple chargée ainsi depuis la source de données. Le rendu canvas vérifie la valeur actuelle de chaque cellule visible via ColumnDef::validate_value et, en cas d'échec, dessine une bordure thémée (Theme::invalid_cell_border / --rs-grid-invalid-cell-border) autour d'elle — aucun clic ni session d'édition requis. C'est indépendant du style invalide de l'éditeur en ligne décrit plus haut (et ça ne l'attend pas) ; ça se déclenche uniquement à partir de la valeur actuelle de la cellule.

Un invalid_cell_border transparent (alpha 0) désactive entièrement l'indicateur, même convention que locked_cell_bg.

Info-bulle au survol des cellules invalides

Survoler une cellule invalide au repos affiche une info-bulle — un seul élément DOM réutilisé pour toutes les cellules (pas un par cellule invalide, donc le coût est identique qu'il y ait une seule cellule invalide ou des milliers), positionné automatiquement sur la cellule survolée. rs-grid ne dessine aucun visuel par défaut : la classe est entièrement à vous, ce qui permet de reproduire directement l'info-bulle de daisyUI :

canvas.set_validation_tooltip_class(Some(
    "tooltip tooltip-open tooltip-error".into(),
));

tooltip-open (ou un modificateur équivalent toujours ouvert) est indispensable — cet élément ne reçoit jamais de vrai :hover de la souris (il est posé au-dessus du canvas avec pointer-events: none), donc rs-grid gère lui-même l'ouverture/fermeture via son propre attribut display plutôt que de compter sur :hover en CSS. Sans classe configurée, l'élément existe mais reste invisible.

Le message de validation est exposé via l'attribut standard data-tip, que daisyUI (et tout CSS utilisant content: attr(data-tip)) lit directement — inutile de dupliquer le texte du message soi-même.

Pour vérifier la validité au repos d'une cellule en dehors du survol (par exemple pour un indicateur personnalisé), utilisez canvas.cell_validation_error(row, col_key) -> Option<String> — indépendant de toute session d'édition active, contrairement à validation_error() plus haut.

Validation au collage

GridCommand::PasteAt valide chaque valeur entrante avant de l'écrire dans la cellule cible — une cellule dont la valeur collée échoue la validation est silencieusement ignorée (l'écriture n'a pas lieu), tandis que le reste du bloc collé s'applique normalement. Ça reflète le comportement d'Excel et AG Grid sur le même cas : aucun des deux ne bloque un collage entièrement, et quand un rejet existe il est toujours par cellule, jamais tout-ou-rien.

GridCommand::CutSelection vide les cellules en y écrivant une chaîne vide — ça reste une écriture, donc c'est validé de la même façon : une cellule dont les règles rejettent une valeur vide (ex. .required()) garde sa valeur d'origine plutôt que d'être vidée. Le texte copié dans le presse-papiers n'est pas affecté dans un cas comme dans l'autre — le couper copie toujours les valeurs d'origine en premier.

Repli tooltip natif title

Par défaut, l'<input> d'édition en ligne reçoit un attribut title natif reflétant le message de validation courant — un tooltip navigateur zero-config qui ne nécessite aucune intégration. Désactivez-le une fois votre propre interface branchée, pour que les deux ne se concurrencent pas :

canvas.set_native_validation_tooltip(false);

Comportement

  • La validation s'exécute sur les éditions utilisateur (CommitEdit / ValidateEdit) et sur PasteAt (voir Validation au collage ci-dessus). Les écritures programmatiques directes via GridModel::set_cell la contournent toujours — c'est l'API de plus bas niveau et elle n'a volontairement aucune couche de validation propre.
  • Les éditeurs Select sont validés aussi — la valeur de l'option sélectionnée est vérifiée comme n'importe quelle autre.
  • Les règles et le validateur historique sont synchrones. Pour une validation asynchrone (ex. vérification serveur), validez de façon optimiste puis revenez en arrière avec GridCommand::Undo si la vérification échoue côté serveur.
  • Le mode Revert ne crée aucune entrée d'annulation ; le mode Block non plus, puisque la session d'édition ne se ferme jamais en cas d'échec.

Voir aussi