Construa um formulário de várias etapas

Plugin Oficial de Várias Etapas

A partir da versão 1.0.0-beta.15, o FormKit inclui um plugin oficial de primeira parte que cria um tipo de entrada multi-step.

Embora ainda haja valor em entender como construir uma entrada de várias etapas por conta própria — se você está procurando a maneira mais fácil de usar uma entrada de várias etapas em seu projeto, confira o plugin oficial de várias etapas do FormKit — é gratuito e de código aberto!

Poucas interações na web causam tanto desprazer quanto ser confrontado com um grande e intimidador formulário. Formulários de várias etapas — às vezes chamados de "wizards" — podem aliviar essa dor dividindo um grande formulário em etapas menores e mais abordáveis — mas eles também podem ser complicados de construir.

Neste guia, vamos percorrer a construção de um formulário de várias etapas com o FormKit e ver como podemos proporcionar uma experiência de usuário elevada com código mínimo. Vamos começar!

API de Composição

Este guia pressupõe que você esteja familiarizado com a API de Composição do Vue.

Requisitos

Vamos começar definindo os requisitos para nosso formulário de várias partes:

  • Mostrar ao usuário em qual etapa eles estão atualmente em relação a todas as etapas necessárias.
  • Permitir que o usuário navegue para qualquer etapa do formulário à vontade.
  • Mostrar feedback imediato se cada etapa passou em todas as validações de frontend ✅.
  • Agregar dados do formulário de todas as etapas em um único objeto para envio.
  • Exibir quaisquer erros de backend retornados nos campos apropriados e também na etapa apropriada.

Criando um formulário básico

Primeiro, vamos criar um formulário básico sem etapas para que tenhamos conteúdo para trabalhar. Nosso exemplo será uma aplicação fictícia para receber uma bolsa, então organizaremos o formulário em 3 seções — "Informações de Contato", "Informações da Organização" e "Aplicação". Estes se tornarão as etapas completas do formulário mais tarde.

Incluiremos uma mistura de regras de validação para cada entrada, e limitaremos cada seção a 1 pergunta por enquanto até termos a estrutura completa no lugar. Por fim, para os propósitos deste guia, vamos exibir os dados coletados do formulário no final de cada exemplo:

Carregar exemplo ao vivo

Dividindo o formulário em seções

Agora que temos uma estrutura definida, vamos dividir o formulário em seções distintas.

Para começar, vamos envolver cada seção de entradas com um grupo (<FormKit type="group" />) para que possamos validar cada grupo independentemente. Os grupos FormKit são poderosos porque estão cientes do estado de validação de todos os seus descendentes sem afetar a marcação do seu formulário.

Um grupo se torna válido quando todos os seus filhos (e os filhos deles) são válidos:

<!-- Mostrando apenas um único grupo aqui por brevidade -->
<FormKit type="group" name: "contactInfo">
  <FormKit type="email" label="*Endereço de email" validation="required|email" />
</FormKit>
...

No nosso caso, também vamos querer um HTML de envolvimento. Vamos colocar cada grupo em uma seção de "etapa" que podemos mostrar e ocultar condicionalmente:

<!-- Mostrando apenas um único grupo aqui por brevidade -->
<section v-show="step === 'contactInfo'">
  <FormKit type="group" name: "contactInfo">
    <FormKit type="email" label="*Endereço de email" validation="required|email" />
  </FormKit>
</section>
...

Em seguida, vamos introduzir uma interface de usuário de navegação para que possamos alternar entre cada etapa:

// por enquanto, defina manualmente os nomes das etapas
const stepNames = ['contactInfo', 'organizationInfo', 'application']
<!-- Configure a interface de usuário de navegação por abas. Ao clicar, mude a etapa -->
<ul class="steps">
  <li
    v-for="stepName in stepNames"
    class="step"
    @click="step = stepName"
    :data-step-active="step === stepName"
  >
    {{ camel2title(panel) }}
  </li>
</ul>

Veja como fica quando juntamos tudo:

Estilos não incluídos

O CSS para formulários de várias etapas — como as abas neste exemplo — não está incluído no tema padrão Genesis. Os estilos foram escritos personalizadamente para este exemplo e você precisará fornecer os seus próprios.

Carregar exemplo ao vivo

Está começando a parecer um verdadeiro formulário de várias etapas! Ainda há mais trabalho a ser feito, pois temos alguns problemas:

  • A validade de cada etapa individual não está sendo mostrada.
  • Quando há validações em uma aba que não é a "etapa atual", elas não podem ser vistas.

Vamos abordar o primeiro problema.

Rastreando a validade para cada etapa

FormKit já rastreia a validade do group por padrão. Só precisaremos capturar esses dados para que possamos usá-los em nossa interface de usuário.

Um conceito importante para lembrar sobre o FormKit é que todo componente <FormKit> tem um nó central correspondente, que por sua vez tem um objeto node.context reativo. Este objeto context rastreia a validade do nó em context.state.valid. Como mencionado acima, um group se torna válido quando todos os seus descendentes são válidos. Com isso em mente, vamos construir um objeto que armazena a validade reativa de cada um dos grupos.

Vamos aproveitar a funcionalidade de plugin do FormKit para fazer este trabalho. Embora o termo "plugin" possa parecer intimidante, os plugins no FormKit são apenas funções de configuração que são chamadas quando um nó é criado. Os plugins são herdados por todos os descendentes (como filhos dentro de um grupo).

Aqui está nosso plugin personalizado, chamado stepPlugin:

// nosso plugin e nosso código de template farão uso de 'steps'
const steps = reactive({})

const stepPlugin = (node) => {
  // só executa para <FormKit type="group" />
  if (node.props.type == 'group') {
    // construir nosso objeto steps
    steps[node.name] = steps[node.name] || {}

    // adicionar a validade reativa do grupo atual
    node.on('created', () => {
      steps[node.name].valid = toRef(node.context.state, 'valid')
    })

    // Parar a herança do plugin para nós descendentes.
    // Só nos importamos com os grupos de nível superior
    // que representam as etapas.
    return false
  }
}

O objeto steps reativo resultante do nosso plugin acima se parece com isto:

{
  contactInfo: { valid: false },
  organizationInfo: { valid: false }
  application: { valid: false }
}

Para usar nosso plugin, vamos adicioná-lo ao nosso formulário raiz <FormKit type="form" />. Isso significa que todo grupo de nível superior em nosso formulário herdará o plugin:

<FormKit type="form" :plugins="[stepPlugin]"> ... restante do formulário </FormKit>

Mostrando validade

Agora que nosso template tem acesso em tempo real ao estado de validade de cada grupo através do nosso plugin, vamos escrever a interface do usuário para mostrar esses dados na barra de navegação de etapas.

Também não precisamos mais definir manualmente nossas etapas, pois nosso plugin está armazenando dinamicamente o nome de todos os grupos no objeto steps. Vamos adicionar um atributo data-step-valid="true" a cada etapa se ela for válida para que possamos direcionar com CSS:

<ul class="steps">
  <li
    v-for="(step, stepName) in steps"
    class="step"
    @click="activeStep = stepName"
    :data-step-valid="step.valid"
    :data-step-active="activeStep === stepName"
  >
    {{ camel2title(stepName) }}
  </li>
</ul>

Com essas atualizações, nosso formulário agora é capaz de informar ao usuário quando eles preencheram corretamente todos os campos em uma determinada etapa!

Também faremos algumas outras melhorias:

  • Extraia a "lógica da etapa" para um Vue composable para que possa ser reutilizado em outro lugar.
  • Crie um arquivo utils.js para nossas funções utilitárias.
  • Defina a 1ª etapa que encontramos como a activeStep.
Carregar exemplo ao vivo

Exibindo erros

A exibição de erros é mais matizada. Embora o usuário possa não estar ciente, existem na verdade 2 tipos de erros que precisamos tratar e comunicar ao usuário:

  • Erros de falha nas regras de validação frontend (messages do tipo validation)
  • Erros de backend (messages do tipo error)

O FormKit usa sua loja de mensagens para rastrear ambos os tipos de erros/mensagens.

Com nosso plugin já em vigor, é relativamente simples adicionar rastreamento para ambos:

const stepPlugin = (node) => {
  ...
  // Armazene ou atualize a contagem de mensagens de validação bloqueantes.
  // FormKit emite o evento "count:blocking" (com a contagem) cada vez
  // que a contagem muda.
  node.on('count:blocking', ({ payload: count }) => {
    steps[node.name].blockingCount = count
  })

  // Armazene ou atualize a contagem de mensagens de erro de backend.
  node.on('count:errors', ({ payload: count }) => {
    steps[node.name].errorCount = count
  })
  ...
}
Mensagens de validação bloqueantes vs erros

FormKit faz uma distinção entre mensagens de validação frontend (messages do tipo validation), e erros (messages do tipo error).

Vamos atualizar nosso exemplo para mostrar ambos os tipos de erros com os seguintes requisitos:

  • Sempre mostraremos a contagem de erros de backend se eles existirem.
  • Só mostraremos a contagem de erros de validação frontend se o usuário visitar e depois sair (desfocar) de um grupo - pois não queremos confrontá-los com a interface de erro se eles ainda estiverem em andamento.

Adicionando um evento de desfoque de grupo

Como "desfocar um grupo" não existe em HTML, vamos introduzi-lo em nosso plugin com um array chamado visitedSteps. Aqui está o código relevante:

import { watch } from 'vue'
import { getNode, createMessage } from '@formkit/core'

const stepPlugin = (node) => {
  ...
  const activeStep = ref('')
  const visitedSteps = ref([]) // rastrear etapas visitadas

  // Observar nossa activeStep e armazenar etapas visitadas
  watch(activeStep, (newStep, oldStep) => {
    if (oldStep && !visitedSteps.value.includes(oldStep)) {
      visitedSteps.value.push(oldStep)
    }
    // Acionar a exibição de validação nos campos se um grupo foi visitado
    visitedSteps.value.forEach((step) => {
      const node = getNode(step)

      // o método node.walk() percorre todos os descendentes do nó atual
      // e executa a função fornecida.
      node.walk((n) => {
        n.store.set(
          createMessage({
            key: 'submitted',
            value: true,
            visible: false
          })
        )
      })
    })
  })
  ...
}

Você pode estar se perguntando por que estamos percorrendo todos os descendentes de uma determinada etapa (node.walk()) e criando mensagens com uma chave de submitted e valor de true? Quando um usuário tenta enviar um formulário, é assim que o FormKit informa a si mesmo que todas as entradas estão em um estado submitted. Neste estado, o FormKit força qualquer mensagem de validação bloqueante a aparecer. Estamos acionando manualmente a mesma coisa em nosso evento de "desfoque de grupo".

A interface de erro

Usaremos a mesma interface para ambos os tipos de erros, já que os usuários finais realmente não se importam com a distinção. Aqui está nosso HTML atualizado, que exibe uma bolha vermelha com a soma total dos erros errorCount + blockingCount:

<li v-for="(step, stepName) in steps" class="step" ...>
  <span
    v-if="checkStepValidity(stepName)"
    class="step--errors"
    v-text="step.errorCount + step.blockingCount"
  />
  {{ camel2title(stepName) }}
</li>

Estamos quase na linha de chegada! Aqui está nosso formulário atual — que agora pode informar ao usuário quando eles preencheram corretamente ou incorretamente cada etapa:

Carregar exemplo ao vivo

Envio do formulário e recebimento de erros

A última peça do quebra-cabeça é enviar o formulário e lidar com quaisquer erros que recebemos do servidor backend. Vamos simular o backend para os propósitos deste guia.

Enviamos o formulário adicionando um manipulador @submit ao <FormKit type="form">:

<FormKit type="form" @submit="submitApp"> ... restante do formulário</FormKit>

E aqui está nosso manipulador de envio:

const submitApp = async (formData, node) => {
  try {
    const res = await axios.post(formData)
    node.clearErrors()
    alert('Sua aplicação foi enviada com sucesso!')
  } catch (err) {
    node.setErrors(err.formErrors, err.fieldErrors)
  }
}

Observe que o FormKit passa ao nosso manipulador de envio 2 argumentos úteis: os dados do formulário em um único objeto pronto para solicitação (que estamos chamando de formData), e o núcleo subjacente do formulário node, que podemos usar para limpar erros ou definir quaisquer erros retornados usando os auxiliares node.clearErrors() e node.setErrors(), respectivamente.

setErrors() recebe 2 argumentos: erros de nível de formulário e erros específicos de campo. Nosso backend falso retorna a resposta err que usamos para definir quaisquer erros.

Então, o que acontece se o usuário estiver na etapa 3 (Aplicação) quando enviar, e houver erros de nível de campo em uma etapa oculta? Felizmente, desde que os nós existam no DOM, o FormKit é capaz de colocar esses erros adequadamente. É por isso que usamos um v-show para as etapas em vez de v-if — O nó DOM precisa existir para ter erros definidos no nó FormKit correspondente.

Juntando tudo

E Voilà! 🎉 Terminamos! Além do nosso manipulador de envio, adicionamos alguns outros toques de UI e UX a este formulário final para torná-lo mais real:

  • Adicionados botões Anterior / Próximo para navegação de etapas.
  • Adicionado um backend falso ao utils.js que retorna erros.
  • O botão de envio do formulário agora está desativado até que todo o formulário esteja em um estado válido.
  • Adicionado algum texto adicional ao formulário para melhor simular uma UI do mundo real.

Aqui está — um formulário de várias etapas totalmente funcional:

Carregar exemplo ao vivo
Quer ver como é construído usando FormKit Schema?Confira o Playground

Maneiras de melhorar

Claro, sempre há maneiras de melhorar qualquer coisa, e este formulário não é exceção. Aqui estão algumas ideias:

  • Salve o estado do formulário em window.localStorage para que o estado do formulário do usuário seja mantido mesmo que ele saia acidentalmente.
  • Pré-popule quaisquer valores de formulário conhecidos para que o usuário não precise preencher dados conhecidos.
  • Adicione um indicador de status "ainda não enviado" para alertar o usuário de que ainda precisa enviar.

Cobrimos muitos tópicos neste guia e esperamos que você tenha aprendido mais sobre o FormKit e como usá-lo para facilitar a criação de formulários de várias etapas!

Quer usar uma entrada de várias etapas em seu projeto?Experimente o plugin oficial