[elixir! #0002] [译] 在Phoenix中实现动态表单 by José Valim
今天我们将要学习如何在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
属性和合法检查,并显示所有的输入错误。
我们旨在不添加第三方依赖,就能在我们自己的应用中使用很少的代码实现这些。这样当我们的应用变化时,就能随意地修改和扩展它们。
设置
在构建我们的input
helper之前,让我们生成一个新的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
查看一下默认的表单。
编写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
现在提交错误的表单,你会看到每个标签和输入框都呈绿色(成功)或是红色(出错)。
合法检查
我们可以使用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_type
和Phoenix.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中,我们可以使用框架自带的抽象来实现,使得我们可以在完全控制生成的样式的同时实现大部分功能。