Ir para o conteúdo
Trilha
Capítulos

Receitas

Formulário em passos

Onde mora o passo 1 enquanto a pessoa está no passo 2, e o que acontece quando o rascunho vence.

ctx --pack · est. 1248 tokens est. · llms.txt da receita

Três telas, uma pergunta difícil — e não é o HTML: onde mora o passo 1 enquanto a pessoa está no passo 2? O que se escreve no lugar é uma página cheia de <input type="hidden"> (que o primeiro upload quebra), ou uma linha meio preenchida no banco (que todo relatório depois precisa aprender a ignorar), ou uma tela enorme com tudo.

O c.Draft é a resposta do framework: um rascunho com nome, guardado de uma requisição para a outra, com prazo.

Uma struct por passo#

// WizardStep1 is what a multi-step form actually needs: one struct per step,
// each with only its own rules. A single struct with every field cannot be
// validated halfway — step one would fail on an address nobody has typed —
// and what people do instead is drop the rules until the last screen, where a
// message about a field three screens back is useless.
type WizardStep1 struct {
	Name  string `form:"name"  validate:"required,max=80"`
	Email string `form:"email" validate:"required,email"`
}

// WizardStep2 is the second screen.
type WizardStep2 struct {
	Postcode string `form:"postcode" validate:"required"`
	City     string `form:"city"     validate:"required"`
}

// Wizard is what travels between the screens.
type Wizard struct {
	Who   WizardStep1 `json:"who"`
	Where WizardStep2 `json:"where"`
}

Esta é a parte que vale copiar. Uma struct só, com todos os campos e todas as tags validate, não dá para conferir pela metade: o passo 1 falharia no endereço que ninguém digitou ainda, e a saída que as pessoas acham é largar as regras até a última tela — onde uma mensagem sobre um campo de três telas atrás não serve para nada.

O fim de um passo#

// WizardPost is the end of a step: load what is there, bind only this step,
// save, move on. A 422 here costs nothing that was typed on an earlier screen,
// because the earlier screens are in the draft and not in this form.
func WizardPost(c *trilha.Ctx) error {
	var w Wizard
	_ = c.Draft("signup").Load(&w) // nothing yet on the first screen
	if err := c.Bind(&w.Who); err != nil {
		return err // FieldErrors: 422 with the messages next to the fields
	}
	if err := c.Draft("signup").Save(w, 30*time.Minute); err != nil {
		return err
	}
	return c.Redirect("/signup/where")
}

Carrega, faz o bind só deste passo, salva, segue. Um 422 aqui não custa nada do que foi digitado antes, porque as telas anteriores estão no rascunho e não neste formulário.

Toda tela depois da primeira#

// WizardResume is every screen after the first. No draft is not a failure: it
// is somebody whose draft expired, or who typed the address of step two
// directly, and the answer is step one — not an empty form that would lose
// whatever they filled in here.
func WizardResume(c *trilha.Ctx) (h.Node, error) {
	var w Wizard
	if err := c.Draft("signup").Load(&w); err != nil {
		return nil, c.Redirect("/signup/who")
	}
	return h.Div(ui.Steps(wizardSteps, 2), wizardForm(c, w)), nil
}

Não ter rascunho não é falha. É alguém cujo rascunho venceu, ou que digitou o endereço do passo 2 direto, ou que terminou este assistente ontem — e a resposta é o passo 1, não um formulário vazio que perderia o que ela preenchesse aqui.

O fim#

// WizardFinish is where the draft becomes a record and stops existing.
func WizardFinish(c *trilha.Ctx) error {
	var w Wizard
	if err := c.Draft("signup").Load(&w); err != nil {
		return c.Redirect("/signup/who")
	}
	if err := saveSignup(c, w); err != nil {
		return err
	}
	c.Draft("signup").Clear()
	return c.Redirect("/signup/done")
}

Clear antes do redirect, não depois: rascunho que virou registro precisa deixar de existir, ou a próxima visita ao passo 2 retoma o que já aconteceu.

Onde o rascunho mora#

Abaixo de 2 KB de JSON ele é um cookie assinado — nada para configurar, nada para limpar, e vence sozinho. Acima disso ele precisa do Config.Drafts, uma interface de três métodos sobre o que a app já roda; sem ela, o Save devolve um erro citando esse campo em vez de mandar um cookie que o navegador descartaria em silêncio.

O cookie é da própria pessoa, então um rascunho é invisível para os outros sem uma linha de código sobre dono, e quem assina é o TRILHA_SECRET: sem segredo, o Save avisa.

O indicador#

O ui.Steps(passos, atual) desenha onde a pessoa está. Passo já feito é link; o atual leva aria-current="step"; os da frente são texto — um assistente em que o passo 3 está a um clique é um assistente cujos passos não precisavam ser em ordem.

O fluxo inteiro roda no examples/cadastro.