eQuantic.UIeQuantic.UI
Docs
Playground
GitHub
DocsWrite-once
Formularios
Edit this page
7 min read
Un formulario en eQuantic.UI es un modelo, no un control. Los valores, los flags, las reglas, los errores y el único envío al que se le permite estar en vuelo viven todos en eQuantic.UI.Primitives, lógica pura con cero dependencias y sin píxeles, y los componentes que van encima solo saben cuál de los dos es dueño de qué.
¿POR QUÉ UNA CAPA DE MODELO? Porque un formulario es sobre todo aritmética sobre el estado: si un error puede mostrarse ya, si hay algo sin guardar, si un segundo clic puede volver a enviar. Conecta eso dentro de un control y solo se puede probar haciendo clic. Mantenlo aquí y se prueba afirmando, y el mismo C# corre en el servidor, en el navegador a través de su gemelo transpilado, y en una ventana nativa.
Declarar un formulario
Desde 0.2.0-preview.29
Una página sostiene un FormController, declara sus campos, y se suscribe una sola vez. Cada valor, flag y error llega por ese único evento, así que nada de la página rastrea el estado una segunda vez.
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 devuelve el campo, que es lo que hace posible una regla entre campos: Rules.Matches(password) sostiene el otro campo y lo lee en el momento de validar, así que juzga lo que está en pantalla y no lo que estaba cuando se construyó el formulario.
Callado hasta que te vas
Dos pares de propiedades llevan todo el comportamiento de un formulario considerado, y cada par son dos preguntas distintas:
Pregunta
Para qué se usa
Touched
¿ha SALIDO el usuario de este campo?
si un error puede mostrarse
Dirty
¿el valor difiere del que el formulario tenía al abrirse?
"¿descartar los cambios?"
Error
qué está mal ahora mismo
siempre actual, incluso antes de que alguien escriba
VisibleError
qué puede DECIR el campo
a lo que se enlaza un componente
La consecuencia es la temporización que puedes mirar en el ejemplo: escribe una dirección rota y nada se pone rojo, sal del campo y sí, arréglala y el rojo se va en la pulsación que la arregla. Un formulario renderizado en el servidor llega callado por la misma razón: cada regla ya corrió, y nadie ha tocado nada todavía.
1
2
3
4
5
6
var email = form.Field("email")!;
form.Set("email", "ana@");
email.Error; // "Enter a valid email address.", computado de inmediato
email.VisibleError; // null, el caret sigue en la caja
form.Touch("email");
email.VisibleError; // "Enter a valid email address."
Las reglas
Required, MinLength, MaxLength, Email, Range, Matches, y Custom para la que esta lista no tiene. Cada regla menos Required pasa con un valor vacío: un campo que es opcional y está en blanco no está mal formateado, está ausente, y "Introduce un email válido" bajo una caja opcional vacía es la falsa alarma más común en la validación de formularios.
Una regla es un predicado y su mensaje, que es lo que le permite cruzar al navegador en absoluto: System.ComponentModel.DataAnnotations valida por reflexión, y la reflexión es exactamente lo que el transpilador no puede llevar.
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 == '-'))]);
Un formulario que cambia de forma
Desde 0.2.0-preview.29
Los formularios de verdad tienen más de un camino por dentro, y la validación condicional llega como dos piezas que se componen, en lugar de un segundo motor.
Rules.When hace condicional cualquier regla: envuelve, así que cada regla de arriba es condicional gratis. Mientras la condición es falsa la regla se sostiene vacuamente: no se preguntó nada, así que el campo se calla en lugar de conservar la última respuesta que dio.
relevantWhen apaga el CAMPO entero. Una dirección de envío en un pedido que se recoge en tienda no es un campo con una regla que pasa, es un campo que nadie está preguntando: no sostiene error alguno, no puede hacer inválido el formulario, y una página puede dejarlo fuera por completo. Lo que se escribió sobrevive a que la pregunta se vaya y vuelva.
1
2
3
4
5
6
7
8
9
10
11
var kind = _form.Add("kind", "personal");
// La regla va y viene; el campo se queda.
_form.Add("taxNumber", rules: [Rules.When(() => kind.Value == "company", Rules.Required())]);
// El campo mismo no se está preguntando mientras la casilla está sin marcar.
_form.Add("phone", relevantWhen: () => _callMe,
rules: [Rules.Required("We need a number to call you on."), Rules.MinLength(9)]);
// …y la página lo dibuja solo mientras aplica:
if (_form.Field("phone") is { Relevant: true })
card.Add(new FormInput(_form, "phone", "Phone"));
Una condición lee lo que sea que capture. Cuando eso es otro campo, no hace falta nada más: cambiar cualquier valor vuelve a correr las reglas de todos los demás campos. Cuando es estado que el formulario no posee (una casilla de la página, un plan elegido en un paso anterior), la página lo dice con Revalidate():
1
2
3
4
5
card.Add(new Checkbox(_callMe, () => SetState(() =>
{
_callMe = !_callMe;
_form.Revalidate(); // la condición vive fuera del formulario, así que se le avisa
}), "Call me instead of emailing"));
La superficie
Desde 0.2.0-preview.29
Dos componentes enlazan el modelo con los píxeles, y son deliberadamente finos. FormInput es dueño de tres cables y de nada más: escribir llama a Set, salir llama a Touch, y lo que dibuja es VisibleError. FormSubmit lee el controlador para todo lo demás.
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() });
El botón de envío se queda pulsable mientras el formulario es inválido, y es una elección que vale defender: un envío deshabilitado es la manera más común de hacer que un formulario se sienta roto, porque el campo que está mal suele ser uno que quien lo usa nunca visitó. Pulsarlo es lo que revela la respuesta: SubmitAsync toca primero cada campo.
El modelo ya lo dijo
Desde 0.2.0-preview.29
Un modelo anotado con System.ComponentModel.DataAnnotations ya describe la mayor parte de un formulario. Márcalo con [FormModel] y el build escribe el controlador:
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(); // generado
La lectura ocurre en tiempo de build, y tiene que ser así: DataAnnotations valida por REFLEXIÓN, que es exactamente lo que el transpilador no puede llevar a un navegador. Lo que se emite son llamadas CORRIENTES a las mismas Rules de arriba, así que no hay un segundo motor de validación, y entonces el formulario generado y uno escrito a mano son el mismo objeto. Un modelo puede adoptar el puente sin que la página cambie, y un formulario puede crecer más allá de él sin una reescritura:
1
2
3
4
5
public FormScreen()
{
// …el campo que ninguna anotación puede describir, añadido al formulario que construyó el generador.
_form.Add("Phone", relevantWhen: () => _callMe, rules: [Rules.Required(), Rules.MinLength(9)]);
}
Los campos se llaman como la PROPIEDAD ("Email"), que es también lo que reporta el model state de un servidor, así que ApplyServerErrors aterriza en el campo correcto sin nada en medio.
Llevadas: Required, EmailAddress, MinLength, MaxLength, StringLength (los dos límites), Range, RegularExpression y Compare. ErrorMessage le gana al mensaje propio de la regla, porque una aplicación que escribió uno lo escribió para que se mostrara.
Dicho en voz alta en lugar de descartado: un tipo de propiedad que ninguna caja de texto hace round-trip (EQ3103), una anotación sin regla en la que convertirse (EQ3104), un [Compare] que nombra una propiedad que no está ahí (EQ3105). Todos avisos: un formulario que lleva la mayor parte de un modelo sigue valiendo la pena, y el servidor hace cumplir el resto de todos modos. Es opt-in, porque un modelo anotado para una API no es automáticamente un formulario.
Enviar, y el veredicto del servidor
SubmitAsync le da a quien llama tres garantías que de otro modo reimplementaría en cada página: un formulario inválido nunca llega al manejador (en su lugar se revela cada error), un segundo envío se rechaza mientras el primero está en vuelo (el doble clic que cobra una tarjeta dos veces), y un throw se vuelve SubmitError en lugar de una excepción no manejada, porque que la red falle es algo que los formularios hacen, no un crash.
Las reglas del lado del cliente son una cortesía. La validación que cuenta corre donde viven los datos, y ApplyServerErrors es cómo su respuesta vuelve al campo al que pertenece:
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(); // los valores actuales pasan a ser la nueva línea base: nada sucio, nada gritando
}
Vallas
•
Las fechas, los enums y cualquier otra cosa que una caja de texto no pueda hacer round-trip se quedan fuera del puente de DataAnnotations: se reportan (EQ3103) y se dejan para un campo escrito a mano, porque una fecha necesita un selector y una cultura antes de necesitar una regla.
•
Los mensajes son cadenas planas, no claves de recurso. Una aplicación que localiza sus mensajes pasa cadenas ya localizadas, porque es la aplicación la dueña de su resx. Vea Localización para el chrome propio del SDK, que es el único texto que este framework traduce en tu nombre.
Relacionado
•
Componentes write-once: cómo una sola biblioteca de componentes llega al navegador y a una ventana nativa.
•
Design System: el campo, la casilla y el botón de los que está construida la superficie.
•
Integración con el servidor: [ServerAction], que es cómo un formulario llega al servidor.