Skip to content

Forms pt BR

Edgar Mesquita edited this page Aug 15, 2026 · 4 revisions

Formulários

🌐 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.

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.

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.

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.

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():

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.

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:

[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:

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:

[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

Clone this wiki locally