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/