[elixir! #0002] [译] 在Phoenix中实现动态表单 by José Valim

jifangege 2019-06-20

原文

今天我们将要学习如何在Phoenix中使用我们的schema信息来动态地构建带有合法性检查,报错等功能的输入框。我们的目标是在模板中支持下列API:

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

生成的每个表单会有合适的样式和类别(在本例中我们会使用Bootstrap),包括合适的HTML属性,例如对于必填的输入框有required属性和合法检查,并显示所有的输入错误。

我们旨在不添加第三方依赖,就能在我们自己的应用中使用很少的代码实现这些。这样当我们的应用变化时,就能随意地修改和扩展它们。

设置

在构建我们的inputhelper之前,让我们生成一个新的resource,作为我们试验的对象(如果你手头上没有Phoenix应用,先运行mix phoenix.new your_app):

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

在完成了例行操作之后,打开“web/templates/user/form.html.eex”文件,我们会看到如下的输入列表:

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

我们的目标是用一行<%= input f, field %>,取代上面的每个group。

添加changeset合法检查

还是在“form.html.eex"模板里,可以看到一个队Ecto changesets的操作:

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

因此,如果要在表单中自动展示合法检查,第一步就是在changeset里声明这些合法检查。打开“web/models/user.ex”,在changeset函数的末尾添加一些新的合法检查:

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

在做进一步修改之前,先让我们运行mix phoenix.server,并访问ttp://localhost:4000/users/new查看一下默认的表单。

[elixir! #0002] [译] 在Phoenix中实现动态表单  by José Valim

编写input函数

我们已经设置好了基础代码,现在让我们来实现input函数。

YourApp.InputHelpers 模块

我们的input函数会被定义在一个名为YourApp.InputHelpers的模块中(这里的YourApp是你的应用名),我们把这个模块放在新文件“web/views/input_helpers.ex”里。我们这样定义:

defmodule YourApp.InputHelpers do
  use Phoenix.HTML

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

注意,我们在模块的顶部use了Phoenix.HTML来从Phoenix.HTML项目中导入函数。我们将在之后依赖这些函数来构建样式。

如果想让input函数在所有views都可用,我们需要在“web/web.ex”文件中的“def view”里的imports列表中添加它:

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

定义并导入了模块之后,让我们修改“form.html.eex”函数来使用新的input函数。先删除5个“form-group” div:

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

添加5个输入调用:

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

Phoenix会自动刷新页面,然后我们会看到“Not yet implemented”重复5次。

显示输入

我们首先要实现的是渲染像之前一样的表单。我们会用到Phoenix.HTML.From.input_type 函数,它会接受一个表名和内容名并返回我们应该使用的输入类型。例如,对于:name,它会返回:text_input。对于:date_of_birth,它会返回:datetime_select。我们可以让返回的原子在Phoenix.HTML.Form模块中被调用,以此构建我们的输入:

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

包装,标签和错误提示

下一步,让我们来显示标签和错误提示,它们都包装在一个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

我们使用content_tag来构建div包装,还使用了Phoenix为每个新应用生成的用于构建错误提示样式的YourApp.ErrorHelpers.error_tag函数。

添加Bootstrap类

最后,让我们添加一些HTML类,来映射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

很好!我们已经生成了与原来相同的样式。只用了14行代码。但我们还没有完成,让我们更进一步地自定义我们的input函数。

自定义输入

现在我们可以根据应用的需求来对input做进一步的扩展。

为包装上色

为增强用户体验,如果格式出现错误,自动为每个输入框套用不同的样式。让我们将wrapper_opts重写为:

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

并定义私有函数state_class

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

现在提交错误的表单,你会看到每个标签和输入框都呈绿色(成功)或是红色(出错)。

[elixir! #0002] [译] 在Phoenix中实现动态表单  by José Valim

合法检查

我们可以使用Phoenix.HTML.Form.input_validations函数来从changesets里获取合法性,并将它们作为输入属性合成到我们的input_opts中。将下面两行添加到input_opts变量的定义之后(content_tag调用之前):

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

完成以上修改之后,如我们试图在没有填写“Address”的情况下提交表单,浏览器不会允许表单被提交,因为我们设定了长度至少3个字符。不是每个人都喜欢浏览器的合法性检查,因此你可以直接地控制是否使用它们。

顺便提一下,Phoenix.HTML.Form.input_typePhoenix.HTML.Form.input_validations是作为Phoenix.HTML.FormData协议的一部分被定义的。这意味着如果你决定在Ecto changesets中使用一些别的东西来对输入的数据进行调用和检查,所有我们之前构建的功能依然可以运行。如果想要进一步学习它们,我建议去看看Phoenix.Ecto项目并通过简单地实现一些Phoenix中的协议来学习Ecto和Phoenix是如何集成在一起的。

单个输入设置

最后,我们要为input函数添加对单个输入进行设置的功能。例如,对于给定的输入,我们可能不想它的类型被input_type影响。我们可以添加一个选项来解决:

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

这意味着我们现在可以控制使用Phoenix.HTML.Form中的哪个函数来构建我们的输入:

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

我们也不必受限于Phoenix.HTML.Form所支持的输入样式。例如,如果你想要用自定义的日期选择器来替换:datetime_select输入,只需要将其包装到一个函数中,然后模式匹配你想要自定义的输入。

让我们来看看现在的input函数是什么样子,包括对自定义输入的支持(省略了输入合法检查):

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

当你实现了你自己的:datepicker之后,只需要在你的模板中加入:

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

当你的应用有了这些代码之后,你就能控制输入的类型和自定义样式。幸运的是Phoenix搭载了做够多的功能,帮助我们快速开始,而不需要受限于我们修改表现层的能力。

总结

这篇文章展示了如何使用我们已经在schemas中确定了的信息,并借助Phoenix.HTML,来动态地构建表格。尽管这个例子应用于直接映射到数据库的User schema,Ecto 2.0 允许我们使用schemas来映射到任何数据源,所以input函数可以不加修改地应用于检查搜索表格,登录页面,等等。

我们已经开发了例如Simple Form等项目来解决Rails项目中的这类的问题,在Phoenix中,我们可以使用框架自带的抽象来实现,使得我们可以在完全控制生成的样式的同时实现大部分功能。

相关推荐