eQuantic.UIeQuantic.UI
Docs
Playground
GitHub
DocsWrite-once
Formulare
Edit this page
6 min read
Ein Formular in eQuantic.UI ist ein Modell, kein Control. Die Werte, die Flags, die Regeln, die Fehler und das eine Submit, das in der Luft sein darf, leben alle in eQuantic.UI.Primitives, reine Logik mit null Abhängigkeiten und ohne Pixel, und die Komponenten darüber wissen nur, wem von beiden was gehört.
WARUM ÜBERHAUPT EINE MODELLSCHICHT? Weil ein Formular vor allem Arithmetik über Zustand ist: ob ein Fehler schon gezeigt werden darf, ob etwas ungespeichert ist, ob ein zweiter Klick noch einmal absenden darf. Verdrahten Sie das in ein Control, und es lässt sich nur durch Klicken testen. Behalten Sie es hier, und es wird durch Behaupten getestet, und dasselbe C# läuft auf dem Server, im Browser über seinen transpilierten Zwilling und in einem nativen Fenster.
Ein Formular deklarieren
Seit 0.2.0-preview.29
Eine Seite hält einen FormController, deklariert ihre Felder und abonniert einmal. Jeder Wert, jedes Flag und jeder Fehler kommt über dieses einzige Event, also verfolgt nichts auf der Seite den Zustand ein zweites Mal.
1
2
3
4
5
6
7
8
9
10
11
12
13
public sealed class SignUpPage : StatefulComponent
{
private readonly FormController _form = new();
public SignUpPage()
{
_form.Add("email", rules: [Rules.Required(), Rules.Email()]);
var password = _form.Add("password", rules: [Rules.Required(), Rules.MinLength(8)]);
_form.Add("confirm", rules: [Rules.Required(), Rules.Matches(password)]);
_form.Changed += () => SetState(() => { });
}
}
Add gibt das Feld zurück, und das ist, was eine feldübergreifende Regel möglich macht: Rules.Matches(password) hält das andere Feld und liest es zur Validierungszeit, es urteilt also über das, was auf dem Bildschirm ist, und nicht über das, was dort war, als das Formular gebaut wurde.
Still, bis Sie gehen
Zwei Paare von Eigenschaften tragen das ganze Verhalten eines rücksichtsvollen Formulars, und jedes Paar sind zwei verschiedene Fragen:
Frage
Wofür genutzt
Touched
hat der Nutzer dieses Feld VERLASSEN?
ob ein Fehler gezeigt werden darf
Dirty
weicht der Wert von dem ab, mit dem das Formular öffnete?
"Änderungen verwerfen?"
Error
was gerade jetzt falsch ist
immer aktuell, auch bevor jemand getippt hat
VisibleError
was das Feld SAGEN darf
woran eine Komponente sich bindet
Die Folge ist die Zeitlichkeit, die Sie im Beispiel beobachten können: Tippen Sie eine kaputte Adresse, und nichts wird rot; verlassen Sie das Feld, und es wird rot; korrigieren Sie sie, und das Rot geht beim Tastenanschlag, der sie korrigiert. Ein serverseitig gerendertes Formular kommt aus demselben Grund still an: Jede Regel ist schon gelaufen, und niemand hat noch etwas berührt.
1
2
3
4
5
6
var email = form.Field("email")!;
form.Set("email", "ana@");
email.Error; // "Enter a valid email address.", sofort berechnet
email.VisibleError; // null, der Caret steht noch in der Box
form.Touch("email");
email.VisibleError; // "Enter a valid email address."
Die Regeln
Required, MinLength, MaxLength, Email, Range, Matches und Custom für die, die diese Liste nicht hat. Jede Regel außer Required besteht bei einem leeren Wert: Ein Feld, das optional und leer ist, ist nicht schlecht formatiert, es ist abwesend, und "Geben Sie eine gültige E-Mail ein" unter einer leeren optionalen Box ist der häufigste Fehlalarm in der Formularvalidierung.
Eine Regel ist ein Prädikat und ihre Meldung, und das ist, was sie überhaupt in den Browser überqueren lässt: System.ComponentModel.DataAnnotations validiert über Reflexion, und Reflexion ist genau das, was der Transpiler nicht tragen kann.
1
2
3
form.Add("age", rules: [Rules.Range(18, 120, "You must be 18 or older.")]);
form.Add("slug", rules: [Rules.Custom("Lowercase letters and dashes only.",
value => value.All(c => char.IsAsciiLetterLower(c) || c == '-'))]);
Ein Formular, das seine Form ändert
Seit 0.2.0-preview.29
Echte Formulare haben mehr als einen Weg hindurch, und die bedingte Validierung kommt als zwei komponierbare Stücke, nicht als zweite Engine.
Rules.When macht jede Regel bedingt: Sie umhüllt, also ist jede Regel oben umsonst bedingt. Solange die Bedingung falsch ist, hält die Regel leer: Es wurde nichts gefragt, also wird das Feld still, statt die letzte Antwort zu behalten, die es gab.
relevantWhen schaltet das ganze FELD ab. Eine Lieferadresse an einer Bestellung, die im Laden abgeholt wird, ist kein Feld mit einer bestehenden Regel, es ist ein Feld, das niemand fragt: Es hält keinen Fehler, es kann das Formular nicht ungültig machen, und eine Seite darf es ganz auslassen. Was getippt wurde, überlebt das Verschwinden und Wiederkommen der Frage.
1
2
3
4
5
6
7
8
9
10
11
var kind = _form.Add("kind", "personal");
// Die Regel kommt und geht; das Feld bleibt.
_form.Add("taxNumber", rules: [Rules.When(() => kind.Value == "company", Rules.Required())]);
// Das Feld selbst wird nicht gefragt, solange die Box nicht angehakt ist.
_form.Add("phone", relevantWhen: () => _callMe,
rules: [Rules.Required("We need a number to call you on."), Rules.MinLength(9)]);
// …und die Seite zeichnet es nur, solange es gilt:
if (_form.Field("phone") is { Relevant: true })
card.Add(new FormInput(_form, "phone", "Phone"));
Eine Bedingung liest, was auch immer sie einfängt. Ist das ein anderes Feld, braucht es nichts weiter: Irgendeinen Wert zu ändern lässt die Regeln jedes anderen Feldes erneut laufen. Ist es Zustand, den das Formular nicht besitzt (eine Checkbox auf der Seite, ein in einem früheren Schritt gewählter Tarif), sagt die Seite das mit Revalidate():
1
2
3
4
5
card.Add(new Checkbox(_callMe, () => SetState(() =>
{
_callMe = !_callMe;
_form.Revalidate(); // die Bedingung lebt außerhalb des Formulars, dem wird es also gesagt
}), "Call me instead of emailing"));
Die Oberfläche
Seit 0.2.0-preview.29
Zwei Komponenten binden das Modell an Pixel, und sie sind absichtlich dünn. FormInput besitzt drei Drähte und sonst nichts: Tippen ruft Set, Verlassen ruft Touch, und was es zeichnet, ist VisibleError. FormSubmit liest für alles andere den Controller.
1
2
3
4
5
6
7
8
card.Add(new FormInput(_form, "email", "Email", placeholder: "you@example.com",
helper: "We never share it."));
card.Add(new FormInput(_form, "password", "Password", helper: "At least 8 characters")
{ Obscure = true });
var actions = new Row(gap: Space.S2);
actions.Add(new FormSubmit(_form, "Create account", Submit));
actions.Add(new Button("Reset", Variant.Ghost) { OnPressed = () => _form.Reset() });
Der Absendeknopf bleibt drückbar, während das Formular ungültig ist, und das ist eine Entscheidung, die es zu verteidigen lohnt: Ein deaktiviertes Submit ist die häufigste Art, ein Formular kaputt wirken zu lassen, denn das Feld, das falsch ist, ist meist eines, das die Nutzerin nie besucht hat. Es zu drücken ist, was die Antwort enthüllt: SubmitAsync berührt zuerst jedes Feld.
Das Modell hat es schon gesagt
Seit 0.2.0-preview.29
Ein Modell, das mit System.ComponentModel.DataAnnotations annotiert ist, beschreibt den größten Teil eines Formulars schon. Markieren Sie es mit [FormModel], und der Build schreibt den Controller:
1
2
3
4
5
6
7
8
9
[FormModel]
public sealed class SignUp
{
[Required, EmailAddress] public string Email { get; set; } = "";
[Required, MinLength(8)] public string Password { get; set; } = "";
[Required, Compare(nameof(Password))] public string Confirm { get; set; } = "";
}
private readonly FormController _form = SignUpForm.Create(); // generiert
Das Lesen geschieht zur Build-Zeit, und das muss so sein: DataAnnotations validiert über REFLEXION, und das ist genau das, was der Transpiler nicht in einen Browser tragen kann. Ausgegeben werden GEWÖHNLICHE Aufrufe derselben Rules von oben, es gibt also keine zweite Validierungs-Engine, und damit sind das generierte Formular und ein handgeschriebenes dasselbe Objekt. Ein Modell kann die Brücke übernehmen, ohne dass die Seite sich ändert, und ein Formular kann über sie hinauswachsen, ohne neu geschrieben zu werden:
1
2
3
4
5
public FormScreen()
{
// …das Feld, das keine Annotation beschreiben kann, dem Formular hinzugefügt, das der Generator baute.
_form.Add("Phone", relevantWhen: () => _callMe, rules: [Rules.Required(), Rules.MinLength(9)]);
}
Felder sind nach der EIGENSCHAFT benannt ("Email"), und das ist auch, was der Model State eines Servers meldet, sodass ApplyServerErrors ohne etwas dazwischen auf dem richtigen Feld landet.
Getragen: Required, EmailAddress, MinLength, MaxLength, StringLength (beide Grenzen), Range, RegularExpression und Compare. ErrorMessage gewinnt über die eigene Meldung der Regel, denn eine Anwendung, die eine geschrieben hat, hat sie zum Zeigen geschrieben.
Laut gesagt statt verworfen: ein Eigenschaftstyp, den keine Textbox hin und zurück bringt (EQ3103), eine Annotation ohne Regel, zu der sie werden könnte (EQ3104), ein [Compare], das eine nicht vorhandene Eigenschaft nennt (EQ3105). Alles Warnungen: Ein Formular, das den größten Teil eines Modells trägt, ist trotzdem etwas wert, und der Server erzwingt den Rest ohnehin. Es ist Opt-in, denn ein für eine API annotiertes Modell ist nicht automatisch ein Formular.
Absenden, und das Urteil des Servers
SubmitAsync gibt dem Aufrufer drei Garantien, die er sonst auf jeder Seite neu bauen würde: Ein ungültiges Formular erreicht den Handler nie (stattdessen wird jeder Fehler enthüllt), ein zweites Absenden wird abgelehnt, während das erste in der Luft ist (der Doppelklick, der eine Karte zweimal belastet), und ein Wurf wird SubmitError statt einer unbehandelten Ausnahme, denn dass das Netz ausfällt, ist etwas, das Formularen passiert, kein Absturz.
Clientseitige Regeln sind eine Höflichkeit. Die Validierung, die zählt, läuft dort, wo die Daten leben, und ApplyServerErrors ist, wie ihre Antwort zurück auf das Feld kommt, zu dem sie gehört:
1
2
3
4
5
6
7
8
9
10
11
12
[ServerAction]
public async Task<List<FieldError>> Register(string email) =>
await _users.Exists(email)
? [new FieldError("email", "That address is already registered.")]
: [];
private async Task Submit()
{
var verdict = await Register(_form.Field("email")!.Value);
if (verdict.Count > 0) { _form.ApplyServerErrors(verdict); return; }
_form.Accept(); // die aktuellen Werte werden die neue Grundlinie: nichts schmutzig, nichts ruft
}
Zäune
•
Datumsangaben, Enums und alles andere, was eine Textbox nicht hin und zurück bringt, bleiben außerhalb der DataAnnotations-Brücke: Sie werden gemeldet (EQ3103) und einem handgeschriebenen Feld überlassen, denn ein Datum braucht einen Picker und eine Kultur, bevor es eine Regel braucht.
•
Meldungen sind einfache Strings, keine Ressourcenschlüssel. Eine Anwendung, die ihre Meldungen lokalisiert, gibt lokalisierte Strings hinein, denn die Anwendung besitzt ihr resx. Siehe Lokalisierung für das eigene Chrome des SDK, und das ist der einzige Text, den dieses Framework für Sie übersetzt.
Verwandt
•
Write-once-Komponenten: wie eine Komponentenbibliothek den Browser und ein natives Fenster erreicht.
•
Design System: die Eingabe, die Checkbox und der Button, aus denen die Oberfläche gebaut ist.
•
Server-Integration: [ServerAction], und so erreicht ein Formular den Server.