Desenvolvimento

11 jan, 2017

Formulários dinâmicos com Phoenix

Publicidade

Hoje vamos aprender a construir formulários em Phoenix que utilizam a nossa informação de schema para mostrar dinamicamente os campos de entrada adequados com validações, erros, e assim por diante. Nosso objetivo é suportar a seguinte API em nossos templates:

<%= input f, :name %>
<%= input f, :address %>
<%= input f, :date_of_birth %>
<%= input f, :number_of_children %>
<%= input f, :notifications_enabled %>

Cada entrada gerada terá a marcação e as classes adequadas (usaremos Bootstrap nesse exemplo), incluindo os atributos HTML adequados, como require para campos e validações necessárias, e mostrando qualquer erro de entrada.

O objetivo é construir essa base em nossas próprias aplicações em poucas linhas de código, sem dependências de terceiros, o que nos permite personalizá-la e estendê-la como desejado quando houver mudanças em nossa aplicação.

Configurando

Antes de construir o nosso input helper, vamos primeiro gerar um novo recurso que usaremos como um template para experimentação (se você não tiver uma aplicação Phoenix acessível, execute mix phoenix.new seu_app antes do comando abaixo):

mix phoenix.gen.html User users name address date_of_birth:datetime number_of_children:integer notifications_enabled:boolean

Siga as instruções após o comando acima ser executado e, em seguida, abra o template de formulário em “web/templates/user/form.html.eex”. Devemos ver uma lista de entradas, tais como:

<div class="form-group">
  <%= label f, :address, class: "control-label" %>
  <%= text_input f, :address, class: "form-control" %>
  <%= error_tag f, :address %>
</div>

O objetivo é substituir cada grupo acima por uma linha única <%=input f, field %>.

Adicionando validações changeset

Ainda no template “form.html.eex”, podemos ver que um formulário Phoenix opera em Ecto changesets:

<%= form_for @changeset, @action, fn f -> %>

Portanto, se queremos mostrar automaticamente validações em nossos formulários, o primeiro passo é declarar essas validações em nosso changeset. Abra “web/models/user.ex” e vamos adicionar algumas novas validações no fim da função changeset:

|> validate_length(:address, min: 3)
|> validate_number(:number_of_children, greater_than_or_equal_to: 0)

Além disso, antes de fazer quaisquer alterações em nosso formulário, vamos iniciar o servidor com mix phoenix.server e acessar http://localhost:4000/users/new para ver o formulário padrão funcionando.

Escrevendo a função input

Agora que temos um conjunto de base de código, estamos prontos para implementar a função de entrada.

O módulo YourApp.InputHelpers

Nossa função input será definida em um módulo chamado YourApp.InputHelpers (onde YourApp é o nome da sua aplicação) que vamos colocar em um novo arquivo em “web/views/input_helpers.ex”. Vamos defini-lo:

defmodule YourApp.InputHelpers do
  use Phoenix.HTML

  def input(form, field) do
    "Not yet implemented"
  end
end

Observe que usamos Phoenix.HTML na parte superior do módulo para importar as funções do projeto Phoenix.HTML. Contamos com essas funções para construir o markup mais tarde.

Se queremos que a nossa função input esteja automaticamente disponível em todas as views, precisamos adicioná-la explicitamente à lista das importações na seção “def view” do nosso arquivo “web/web.ex”:

import YourApp.Router.Helpers
import YourApp.ErrorHelpers
import YourApp.InputHelpers # Let's add this one
import YourApp.Gettext

Com o módulo definido e devidamente importado, vamos mudar a nossa função “form.html.eex”, para usar as novas funções input. Em vez de 5 divs “form-group”:

<div class="form-group">
  <%= label f, :address, class: "control-label" %>
  <%= text_input f, :address, class: "form-control" %>
  <%= error_tag f, :address %>
</div>

Devemos ter 5 chamadas de entrada:

<%= input f, :name %>
<%= input f, :address %>
<%= input f, :date_of_birth %>
<%= input f, :number_of_children %>
<%= input f, :notifications_enabled %>

Phoenix live-reload deve recarregar automaticamente a página e devemos ver “Not yet implemented” (ainda não implementado) aparecer 5 vezes.

Mostrando a entrada

A primeira funcionalidade que iremos implementar é renderizar adequadamente as entradas como antes. Para isso, vamos usar a função Phoenix.HTML.Form.input_type, que recebe um formulário e um campo nome e retorna qual tipo de entrada devemos usar. Por exemplo, para :name, ele irá retornar :text_input. Para :date_of_birth, ele vai fornecer :datetime_select. Podemos usar o retornado do atom para enviar para Phoenix.HTML.Form e construir a nossa entrada:

def input(form, field) do
  type = Phoenix.HTML.Form.input_type(form, field)
  apply(Phoenix.HTML.Form, type, [form, field])
end

Salve o arquivo e veja as entradas aparecerem na página!

Encapsulamento, rótulos e erros

Agora vamos dar o próximo passo e mostrar os rótulos e as mensagens de erro, tudo encapsulado em uma div:

def input(form, field) do
  type = Phoenix.HTML.Form.input_type(form, field)

  content_tag :div do
    label = label(form, field, humanize(field))
    input = apply(Phoenix.HTML.Form, type, [form, field])
    error = YourApp.ErrorHelpers.error_tag(form, field) || ""
    [label, input, error]
  end
end

Usamos content_tag para construir a div encapsulada e a função existente YourApp.ErrorHelpers.error_tag que o Phoenix gera para cada nova aplicação que constrói uma tag de erro com a marcação adequada.

Adicionando classes Bootstrap

Finalmente, vamos adicionar algumas classes HTML para espelhar a marcação gerada pelo Bootstrap:

def input(form, field) do
  type = Phoenix.HTML.Form.input_type(form, field)

  wrapper_opts = [class: "form-group"]
  label_opts = [class: "control-label"]
  input_opts = [class: "form-control"]

  content_tag :div, wrapper_opts do
    label = label(form, field, humanize(field), label_opts)
    input = apply(Phoenix.HTML.Form, type, [form, field, input_opts])
    error = YourApp.ErrorHelpers.error_tag(form, field)
    [label, input, error || ""]
  end
end

E é isso! Agora estamos gerando a mesma marcação que o Phoenix originalmente gerou. Tudo em 14 linhas de código. Mas nós não terminamos ainda, vamos levar as coisas para o próximo nível, personalizando ainda mais a nossa função de entrada.

Personalizando as entradas

Agora que já alcançamos a paridade com o código de marcação que o Phoenix gera, podemos ainda estendê-lo e personalizá-lo de acordo com as necessidades de nossa aplicação.

Wrapper colorido

Uma melhoria de UX útil é, se um formulário tiver erros, automaticamente empacotar cada campo de acordo com o estado de sucesso ou erro. Vamos reescrever o wrapper_opts para o seguinte:

wrapper_opts = [class: "form-group #{state_class(form, field)}"]

E definir a função privada state_class da seguinte forma:

defp state_class(form, field) do
  cond do
    # The form was not yet submitted
    !form.source.action -> ""
    form.errors[field] -> "has-error"
    true -> "has-success"
  end
end

Agora envie o formulário com erros, e você deve ver cada rótulo e entrada envolto em verde (em caso de sucesso) ou vermelho (em caso de erro de entrada).

Validações de entrada

Podemos usar a função Phoenix.HTML.Form.input_validations para recuperar as validações em nossos changesets como atributos de entrada e, em seguida, fazer o merge em nossas input_opts. Adicione as duas linhas a seguir depois de onde a variável input_opts é definida (e antes da chamada content_tag):

validations = Phoenix.HTML.Form.input_validations(form, field)
input_opts = Keyword.merge(validations, input_opts)

Após as alterações acima, se você tentar enviar o formulário sem preencher o campo “Endereço”, que impôs um comprimento de 3 caracteres, o navegador não vai permitir que o formulário seja enviado. Nem todo mundo é fã de validações do navegador e, nesse caso, você tem controle direto se quiser incluí-los ou não.

Neste ponto, vale a pena mencionar que tanto Phoenix.HTML.Form.input_type quanto Phoenix.HTML.Form.input_validations são definidos como parte do protocolo Phoenix.HTML.FormData. Isso significa que se você decidir usar outra coisa além de Ecto changesets  para lançar e validar dados de entrada, toda a funcionalidade que nós construímos até agora ainda funcionará. Para aqueles interessados em aprender mais, eu recomendo dar uma olhada no projeto Phoenix.Ecto e aprender como a integração entre Ecto e Phoenix é feita simplesmente implementando protocolos expostos pelo Phoenix.

Opções por entrada

A última mudança que vamos acrescentar à nossa função input é a capacidade de passar as opções por entrada. Por exemplo, para uma determinada entrada dada, nós podemos não querer usar o tipo flexionado por input_type. Podemos adicionar opções para lidar com esses casos:

def input(form, field, opts \\ []) do
  type = opts[:using] || Phoenix.HTML.Form.input_type(form, field)

Isso significa que agora podemos controlar quais funções usar a partir de Phoenix.HTML.Form para construir a nossa entrada:

<%= input f, :new_password, using: :password_input %>

Também não precisamos ficar restritos às entradas suportadas pelo Phoenix.HTML.Form. Por exemplo, se você deseja substituir a entrada :datetime_select que vem com o Phoenix por um datepicker customizado, você pode empacotar a criação de entrada em uma função e padronizar a entrada igual a que você deseja personalizar.

Vamos ver como as nossas funções input se parecem com todos os recursos até agora, incluindo suporte para entradas personalizadas (validações de entrada foram deixadas de fora):

defmodule YourApp.InputHelpers do
  use Phoenix.HTML

  def input(form, field, opts \\ []) do
    type = opts[:using] || Phoenix.HTML.Form.input_type(form, field)

    wrapper_opts = [class: "form-group #{state_class(form, field)}"]
    label_opts = [class: "control-label"]
    input_opts = [class: "form-control"]

    content_tag :div, wrapper_opts do
      label = label(form, field, humanize(field), label_opts)
      input = input(type, form, field, input_opts)
      error = YourApp.ErrorHelpers.error_tag(form, field)
      [label, input, error || ""]
    end
  end

  defp state_class(form, field) do
    cond do
      # The form was not yet submitted
      !form.source.action -> ""
      form.errors[field] -> "has-error"
      true -> "has-success"
    end
  end

  # Implement clauses below for custom inputs.
  # defp input(:datepicker, form, field, input_opts) do
  #   raise "not yet implemented"
  # end

  defp input(type, form, field, input_opts) do
    apply(Phoenix.HTML.Form, type, [form, field, input_opts])
  end
end

E, em seguida, uma vez que você implementar seu próprio :datepicker, basta adicionar ao seu template:

<%= input f, :date_of_birth, using: :datepicker %>

Como a sua aplicação é dona do código, você sempre terá o controle sobre os tipos de entradas e como elas são personalizadas. Felizmente, o Phoenix é enviado com funcionalidades suficientes para nos dar um inicio rápido, sem comprometer a nossa capacidade para refinar a nossa camada de apresentação mais tarde.

Resumindo

Este artigo mostrou como podemos alavancar as conveniências expostas no Phoenix.HTML para criar dinamicamente formulários usando as informações que já temos especificadas em nossos schemas. Embora o exemplo acima tenha utilizado o schema de usuário, que mapeia diretamente para uma tabela de banco de dados, o Ecto 2.0 nos permite usar schemas para mapear qualquer fonte de dados, portanto, a função input pode ser usada para a validação de formulários de busca, páginas de login, e assim por diante, sem alterações.

Enquanto nós desenvolvemos projetos como o SimpleForm para lidar com esses problemas em nossos projetos Rails, com o Phoenix podemos chegar muito longe usando abstrações mínimas que são enviadas como parte do framework, permitindo obter a maioria das funcionalidades enquanto temos o controle total sobre a marcação gerada.

 

***

José Valim faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: http://blog.plataformatec.com.br/2016/09/dynamic-forms-with-phoenix/