eQuantic.UIeQuantic.UI
Docs
Playground
GitHub
Docspt-BR
Formulários
Edit this page
6 min read
🌐 Esta página em: English · Português
Um formulário no eQuantic.UI é um modelo, não um widget. Os valores, as flags, as regras, os erros e o único submit que pode estar em voo vivem todos em eQuantic.UI.Primitives, lógica pura com zero dependências e sem pixels, e os componentes por cima só sabem qual dos dois é dono de quê.
POR QUE UMA CAMADA DE MODELO? Porque um formulário é quase todo aritmética sobre estado: se um erro já pode ser mostrado, se há algo por salvar, se um segundo clique pode submeter de novo. Costure isso num widget e só dá para testar clicando. Mantenha aqui e testa-se afirmando, e o mesmo C# roda no servidor, no browser pelo gêmeo transpilado, e numa janela nativa.
Declarando um formulário
Desde 0.2.0-preview.29
Uma página guarda um FormController, declara os campos dela, e assina uma vez. Todo valor, flag e erro chega por esse único evento, então nada na página rastreia estado uma 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 devolve o campo, e é isso que torna possível uma regra entre campos: Rules.Matches(password) guarda o outro campo e o lê na hora de validar, então julga o que está na tela em vez do que estava lá quando o formulário foi montado.
Calado até você sair do campo
Dois pares de propriedades carregam todo o comportamento de um formulário respeitoso, e cada par são duas perguntas diferentes:
Pergunta
Serve para
Touched
o usuário já SAIU deste campo?
se um erro pode ser mostrado
Dirty
o valor difere daquele com que o formulário abriu?
"descartar alterações?"
Error
o que está errado agora
sempre atual, mesmo antes de alguém digitar
VisibleError
o que o campo pode DIZER
ao que um componente se liga
A consequência é o timing que dá para ver no sample: digite um endereço quebrado e nada fica vermelho, saia do campo e fica, corrija e o vermelho some na tecla que corrige. Um formulário renderizado no servidor chega calado pelo mesmo motivo: toda regra já rodou, e ninguém tocou em nada ainda.
1
2
3
4
5
6
var email = form.Field("email")!;
form.Set("email", "ana@");
email.Error; // "Enter a valid email address.", calculado na hora
email.VisibleError; // null, o cursor ainda está na caixa
form.Touch("email");
email.VisibleError; // "Enter a valid email address."
As regras
Required, MinLength, MaxLength, Email, Range, Matches, e Custom para aquela que esta lista não tem. Toda regra menos Required passa num valor vazio: um campo opcional em branco não está mal formatado, está ausente, e "Digite um e-mail válido" embaixo de uma caixa opcional vazia é o alarme falso mais comum em validação de formulário.
Uma regra é um predicado mais a mensagem dela, e é isso que a deixa cruzar para o browser: System.ComponentModel.DataAnnotations valida por reflexão, e reflexão é exatamente o que o transpilador não consegue levar.
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 == '-'))]);
Um formulário que muda de forma
Desde 0.2.0-preview.29
Formulário de verdade tem mais de um caminho por dentro, e a validação condicional chega como duas peças que se compõem, não como um segundo motor.
Rules.When torna qualquer regra condicional: ela envolve, então toda regra acima já é condicional de graça. Enquanto a condição é falsa a regra vale vacuamente: nada foi pedido, então o campo se cala em vez de guardar a última resposta que deu.
relevantWhen desliga o CAMPO inteiro. Um endereço de entrega num pedido que vai ser retirado na loja não é um campo com regra que passa, é um campo que ninguém está perguntando: não guarda erro, não pode invalidar o formulário, e a página pode simplesmente não desenhá-lo. O que foi digitado sobrevive à pergunta sumir e voltar.
1
2
3
4
5
6
7
8
9
10
11
var kind = _form.Add("kind", "personal");
// A regra vai e volta; o campo fica.
_form.Add("taxNumber", rules: [Rules.When(() => kind.Value == "company", Rules.Required())]);
// O campo em si não está sendo perguntado enquanto a caixa está desmarcada.
_form.Add("phone", relevantWhen: () => _callMe,
rules: [Rules.Required("We need a number to call you on."), Rules.MinLength(9)]);
// …e a página o desenha só enquanto ele se aplica:
if (_form.Field("phone") is { Relevant: true })
card.Add(new FormInput(_form, "phone", "Phone"));
Uma condição lê o que ela capturar. Quando isso é outro campo, nada mais é preciso: mudar qualquer valor re-roda as regras de todos os outros campos. Quando é estado que o formulário não possui (um checkbox na página, um plano escolhido num passo anterior), a página avisa com Revalidate():
1
2
3
4
5
card.Add(new Checkbox(_callMe, () => SetState(() =>
{
_callMe = !_callMe;
_form.Revalidate(); // a condição mora fora do formulário, então o formulário é avisado
}), "Call me instead of emailing"));
A superfície
Desde 0.2.0-preview.29
Dois componentes ligam o modelo aos pixels, e são finos de propósito. FormInput tem três fios e mais nada: digitar chama Set, sair chama Touch, e o que ele desenha é VisibleError. FormSubmit lê do controller todo o resto.
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() });
O botão de submit continua clicável enquanto o formulário é inválido, e essa escolha merece defesa: um submit desabilitado é a forma mais comum de fazer um formulário parecer quebrado, porque o campo que está errado costuma ser um que o usuário nunca visitou. Apertar é o que revela a resposta, e o SubmitAsync toca em todos os campos primeiro.
O modelo já disse
Desde 0.2.0-preview.29
Um modelo anotado com System.ComponentModel.DataAnnotations já descreve quase todo o formulário. Marque com [FormModel] e o build escreve o 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(); // gerado
A leitura acontece em tempo de build, e tem que ser: DataAnnotations valida por REFLEXÃO, que é exatamente o que o transpilador não leva para o browser. O que é emitido são chamadas ORDINÁRIAS para as mesmas Rules acima, então não existe um segundo motor de validação, então o formulário gerado e um escrito à mão são o mesmo objeto. Um modelo adota a ponte sem a página mudar, e um formulário cresce além dela sem reescrita:
1
2
3
4
5
public FormScreen()
{
// …o campo que nenhuma anotação descreve, adicionado ao formulário que o gerador construiu.
_form.Add("Phone", relevantWhen: () => _callMe, rules: [Rules.Required(), Rules.MinLength(9)]);
}
Os campos têm o nome da PROPRIEDADE ("Email"), que é também o que o model state do servidor reporta, então o ApplyServerErrors cai no campo certo sem nada no meio.
Levados: Required, EmailAddress, MinLength, MaxLength, StringLength (os dois limites), Range, RegularExpression e Compare. O ErrorMessage ganha da mensagem da própria regra, porque um app que escreveu uma escreveu para ser mostrada.
Ditos em voz alta em vez de descartados: um tipo de propriedade que nenhuma caixa de texto transporta (EQ3103), uma anotação sem regra correspondente (EQ3104), um [Compare] apontando para uma propriedade que não existe (EQ3105). Todos avisos: um formulário que carrega a maior parte de um modelo ainda vale a pena, e o servidor aplica o resto de qualquer jeito. É opt-in, porque um modelo anotado para uma API não é automaticamente um formulário.
Submeter, e o veredito do servidor
SubmitAsync dá ao chamador três garantias que ele reimplementaria em toda página: um formulário inválido nunca chega ao handler (em vez disso todo erro é revelado), um segundo submit é recusado enquanto o primeiro está em voo (o clique duplo que cobra o cartão duas vezes), e um throw vira SubmitError em vez de exceção não tratada, porque a rede falhar é coisa que acontece com formulário, não um crash.
As regras do cliente são uma cortesia. A validação que conta roda onde os dados moram, e ApplyServerErrors é como a resposta dela volta para o campo a que pertence:
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(); // os valores atuais viram a nova base: nada sujo, nada gritando
}
Cercas
Datas, enums e o que mais uma caixa de texto não transporta ficam fora da ponte de DataAnnotations: são reportados (EQ3103) e deixados para um campo escrito à mão, porque uma data precisa de um seletor e de uma cultura antes de precisar de uma regra.
Mensagens são strings simples, não chaves de recurso. Um app que localiza as mensagens dele passa strings já localizadas, porque o resx é dele. Veja Localização para o chrome do próprio SDK, que é o único texto que este framework traduz por você.
Relacionados
Componentes write-once: como uma única biblioteca de componentes alcança o browser e uma janela nativa.
Design System: o input, o checkbox e o botão de que a superfície é feita.
Integração com o servidor: [ServerAction], que é como um formulário alcança o servidor.