From e46fa99e3bb5be8560c5dd9522fc171fb362905a Mon Sep 17 00:00:00 2001
From: mathieu
Date: Fri, 21 Aug 2026 23:41:16 +0200
Subject: [PATCH] =?UTF-8?q?Pr=C3=A9vient=20l'=C3=A9cran=20quand=20la=20sai?=
=?UTF-8?q?sie=20d'un=20num=C3=A9ro=20change=20sous=20ses=20pieds?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
« Ajouter ce numéro » restait grisé quel que soit le numéro tapé, et ne se
dégrisait qu'après un détour par « Modifier la revue » puis « Enregistrer ».
Ce n'était pas une validation fautive : Blazor ne redessine que le composant
qui a traité l'événement, et le bouton vit dans l'écran quand le champ vit
dans le composant. Le détour redessinait le parent, rien de plus.
Les champs du numéro passent donc tous par une propriété qui invoque
OnChangement — y compris ceux dont aucun bouton ne dépend aujourd'hui, pour
que la correction ne s'oublie pas au premier champ ajouté.
Co-Authored-By: Claude Opus 5
---
CLAUDE.md | 34 +++++++--
.../Composants/ChampsNumeroRevue.razor | 70 +++++++++++++++++--
MaBibli.Client/Pages/Revue.razor | 5 +-
3 files changed, 96 insertions(+), 13 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 717fd31..f7f2819 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -3205,11 +3205,35 @@ titres **intacts**, un article ajouté à un numéro existant par l'écran d'éd
qui se recharge bien d'un numéro à l'autre. Aucun débordement horizontal à 320 px
(`scrollWidth` = 320).
-⚠️ **Défaut voisin constaté au passage, NON corrigé** : le bouton « Ajouter ce numéro » vit dans
-`Revue.razor` alors que le champ « Numéro » vit dans `ChampsNumeroRevue`. Une frappe ne
-re-rendant que le composant qui l'a traitée, le bouton **reste désactivé** tant que le parent ne
-se redessine pas — seul `Entrée` (qui appelle `OnEntree`, donc le parent) enregistre le numéro.
-À traiter.
+### ⚠️ Un champ dans l'enfant, le bouton dans le parent : le bouton reste figé
+
+Constaté au passage, puis remonté en usage : « Ajouter ce numéro » restait **grisé** quel que
+soit le numéro tapé, et ne se dégrisait qu'après un détour par « Modifier la revue » puis
+« Enregistrer ».
+
+**Blazor ne redessine que le composant qui a traité l'événement.** Le champ « Numéro » vit dans
+`ChampsNumeroRevue`, le bouton dans `Revue.razor` : une frappe re-rendait l'enfant, jamais le
+parent, dont l'attribut `disabled` restait celui du dernier rendu — c'est-à-dire celui d'un
+champ vide. Le détour « débloquait » parce qu'il redessinait le parent, pas parce qu'il validait
+quoi que ce soit. `Entrée` marchait déjà, lui, puisqu'il passe par `OnEntree`, donc par le
+parent.
+
+⚠️ **Le remède est une notification, pas un contrôle de saisie** : `OnChangement`, un
+`EventCallback` que l'enfant invoque — invoquer un callback du parent le redessine, c'est
+exactement ce qui manquait.
+
+⚠️ **Aucun champ ne se lie plus directement à `Saisie`** : tous passent par une propriété du
+composant qui prévient. Y compris ceux dont aucun bouton ne dépend aujourd'hui — faire dépendre
+la correction de « lequel commande quoi » la ferait oublier au premier champ ajouté.
+
+**Règle à retenir au-delà de ce cas** : un composant de saisie qui écrit dans un objet **prêté
+par son parent** doit prévenir ce parent, sinon tout ce que le parent calcule à partir de cet
+objet (bouton désactivé, compteur, message) reste figé sur l'état d'avant. Le symptôme est
+trompeur — on cherche une validation fautive là où il n'y a qu'un rendu manquant.
+
+Vérifié en exécution, sur les **deux** formulaires (ajout et modification d'un numéro) : bouton
+grisé à champ vide, dégrisé dès la première frappe, regrisé si l'on efface, et le clic — sans
+`Entrée` — crée puis renomme bien le numéro.
617 tests au vert (618 avant : le test du découpage des unes n'a plus d'objet).
diff --git a/MaBibli.Client/Composants/ChampsNumeroRevue.razor b/MaBibli.Client/Composants/ChampsNumeroRevue.razor
index e09f49e..335be27 100644
--- a/MaBibli.Client/Composants/ChampsNumeroRevue.razor
+++ b/MaBibli.Client/Composants/ChampsNumeroRevue.razor
@@ -5,11 +5,17 @@
modification doivent proposer exactement les mêmes champs, sinon une couverture ou une une
saisies à la création ne seraient plus modifiables ensuite — ce qui est précisément le cas
d'usage (on ajoute un numéro le jour où on l'achète, on en recopie la une plus tard).
+
+ ⚠️ Aucun champ ne se lie DIRECTEMENT à « Saisie » : chacun passe par une propriété qui
+ prévient le parent (OnChangement). Blazor ne redessine que le composant qui a traité
+ l'événement — une frappe ici ne re-rendait donc pas l'écran qui porte le bouton
+ « Ajouter ce numéro », et celui-ci restait grisé jusqu'à ce qu'autre chose redessine le
+ parent (enregistrer la revue, par exemple). Voir CLAUDE.md.
*@
@@ -20,7 +26,7 @@
@@ -107,13 +113,58 @@
Couverture (adresse d'une image, facultatif)
-
@code {
[Parameter, EditorRequired] public AjoutNumeroRevue Saisie { get; set; } = default!;
+ ///
+ /// Signale au parent que la saisie a changé, pour qu'il se redessine.
+ ///
+ ///
+ /// ⚠️ Sans cela, un bouton du parent qui dépend de la saisie reste figé. Blazor ne
+ /// redessine que le composant ayant traité l'événement : « Ajouter ce numéro » vit dans
+ /// l'écran de la revue, le champ « Numéro » vit ici, et le bouton restait donc désactivé
+ /// tant que rien d'autre ne re-rendait le parent — constaté en usage, où il fallait passer
+ /// par « Modifier la revue » puis « Enregistrer » pour le débloquer.
+ ///
+ /// Tous les champs préviennent, pas seulement le numéro : aucun autre bouton n'en dépend
+ /// aujourd'hui, mais faire dépendre la correction de « lequel commande quoi » la referait
+ /// oublier au premier champ ajouté.
+ ///
+ ///
+ [Parameter] public EventCallback OnChangement { get; set; }
+
+ ///
+ /// Les champs du numéro, servis par des propriétés plutôt que liés à :
+ /// c'est le seul endroit d'où prévenir le parent à coup sûr.
+ ///
+ private string Numero
+ {
+ get => Saisie.Numero;
+ set { Saisie.Numero = value; Notifier(); }
+ }
+
+ private string? Note
+ {
+ get => Saisie.Note;
+ set { Saisie.Note = value; Notifier(); }
+ }
+
+ private string? CoverUrl
+ {
+ get => Saisie.CoverUrl;
+ set { Saisie.CoverUrl = value; Notifier(); }
+ }
+
+ ///
+ /// Le parent est prévenu sans être attendu : InvokeAsync ne fait que demander un
+ /// nouveau rendu, et un setter de propriété ne peut de toute façon rien attendre.
+ ///
+ private void Notifier() => _ = OnChangement.InvokeAsync();
+
/// Identifiant du champ « à la une », pour que son libellé le désigne vraiment.
private readonly string _idChamp = $"une-{Guid.NewGuid():N}";
@@ -193,6 +244,7 @@
}
RemettreAZero();
+ Notifier();
}
/// Reprend un titre dans le champ : on le corrige là où on l'a écrit.
@@ -212,6 +264,7 @@
// Le titre qu'on modifiait vient peut-être de disparaître, et les rangs suivants ont
// glissé : reprendre la modification écrirait sur le mauvais titre.
RemettreAZero();
+ Notifier();
}
///
@@ -226,9 +279,14 @@
private DateTime? Parution
{
get => Saisie.DateParution?.ToLocalTime().Date;
- set => Saisie.DateParution = value is { } date
- ? DateTime.SpecifyKind(date, DateTimeKind.Local).ToUniversalTime()
- : null;
+ set
+ {
+ Saisie.DateParution = value is { } date
+ ? DateTime.SpecifyKind(date, DateTimeKind.Local).ToUniversalTime()
+ : null;
+
+ Notifier();
+ }
}
[Parameter] public EventCallback OnEntree { get; set; }
diff --git a/MaBibli.Client/Pages/Revue.razor b/MaBibli.Client/Pages/Revue.razor
index 7f04f3f..5f61f1b 100644
--- a/MaBibli.Client/Pages/Revue.razor
+++ b/MaBibli.Client/Pages/Revue.razor
@@ -90,7 +90,7 @@ else