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

迭代棱镜
• 阅读 2613

原文

今天我们将要学习如何在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中,我们可以使用框架自带的抽象来实现,使得我们可以在完全控制生成的样式的同时实现大部分功能。

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
4年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
3年前
Uber准备放弃自动驾驶,转手卖给前谷歌无人车CTO,估值曾被孙正义炒到72.5亿美元
!(https://oscimg.oschina.net/oscnet/0fe7cb00a0cf4872b022342d1e21d47e.png)杨净发自凹非寺量子位报道|公众号QbitAI最新消息,Uber要出售无人驾驶部门(ATG)了。据TechCrunch报道,Uber有意向出售,而也有人愿意买。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
迭代棱镜
迭代棱镜
Lv1
古台摇落后,秋日望乡心。
文章
9
粉丝
0
获赞
0