Prévient l'écran quand la saisie d'un numéro change sous ses pieds

« 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 <noreply@anthropic.com>
This commit is contained in:
mathieu
2026-08-21 23:41:16 +02:00
co-authored by Claude Opus 5
parent fe36091bf0
commit e46fa99e3b
3 changed files with 96 additions and 13 deletions
+29 -5
View File
@@ -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).
@@ -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.
*@
<div class="champs-numero">
<label class="champ">
<span class="champ-libelle">Numéro</span>
<input class="champ-saisie" type="text" placeholder="43" @bind="Saisie.Numero"
<input class="champ-saisie" type="text" placeholder="43" @bind="Numero"
@bind:event="oninput" @onkeydown="SurTouche" autocomplete="off" />
</label>
@@ -20,7 +26,7 @@
<label class="champ">
<span class="champ-libelle">Note <span class="champ-aide">(facultatif)</span></span>
<input class="champ-saisie" type="text" @bind="Saisie.Note" @bind:event="oninput"
<input class="champ-saisie" type="text" @bind="Note" @bind:event="oninput"
placeholder="dossier sur l'eau" autocomplete="off" />
</label>
</div>
@@ -107,13 +113,58 @@
<span class="champ-libelle">
Couverture <span class="champ-aide">(adresse d'une image, facultatif)</span>
</span>
<input class="champ-saisie" type="url" @bind="Saisie.CoverUrl" @bind:event="oninput"
<input class="champ-saisie" type="url" @bind="CoverUrl" @bind:event="oninput"
placeholder="https://…" autocomplete="off" />
</label>
@code {
[Parameter, EditorRequired] public AjoutNumeroRevue Saisie { get; set; } = default!;
/// <summary>
/// Signale au parent que la saisie a changé, pour qu'il se redessine.
/// </summary>
/// <remarks>
/// ⚠️ <b>Sans cela, un bouton du parent qui dépend de la saisie reste figé.</b> 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.
/// <para>
/// 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é.
/// </para>
/// </remarks>
[Parameter] public EventCallback OnChangement { get; set; }
/// <summary>
/// Les champs du numéro, servis par des propriétés plutôt que liés à <see cref="Saisie"/> :
/// c'est le seul endroit d'où prévenir le parent à coup sûr.
/// </summary>
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(); }
}
/// <remarks>
/// Le parent est prévenu sans être attendu : <c>InvokeAsync</c> ne fait que demander un
/// nouveau rendu, et un setter de propriété ne peut de toute façon rien attendre.
/// </remarks>
private void Notifier() => _ = OnChangement.InvokeAsync();
/// <summary>Identifiant du champ « à la une », pour que son libellé le désigne vraiment.</summary>
private readonly string _idChamp = $"une-{Guid.NewGuid():N}";
@@ -193,6 +244,7 @@
}
RemettreAZero();
Notifier();
}
/// <summary>Reprend un titre dans le champ : on le corrige là où on l'a écrit.</summary>
@@ -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();
}
/// <summary>
@@ -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<KeyboardEventArgs> OnEntree { get; set; }
+3 -2
View File
@@ -90,7 +90,7 @@ else
</p>
}
<ChampsNumeroRevue Saisie="_saisie" OnEntree="AjouterAsync" />
<ChampsNumeroRevue Saisie="_saisie" OnEntree="AjouterAsync" OnChangement="StateHasChanged" />
<div class="actions-formulaire">
<button type="button" class="bouton bouton-principal"
@@ -169,7 +169,8 @@ else
@if (ModeEdition && _edition == numero.Id)
{
<div class="carte-corps">
<ChampsNumeroRevue Saisie="_saisieEdition" OnEntree="EnregistrerNumeroAsync" />
<ChampsNumeroRevue Saisie="_saisieEdition" OnEntree="EnregistrerNumeroAsync"
OnChangement="StateHasChanged" />
<div class="actions-formulaire">
<button type="button" class="bouton bouton-principal"