《HTML标准》解读:详解表单及相关元素

274 阅读7分钟

本文为HTML标准解读系列文章,其他文章详见这里

form元素是HTML表单的容器元素,HTML标准对它的含义是这么定义的:

The form element represents a hyperlink that can be manipulated through a collection of form-associated elements.

form元素代表了一个超链接,这个超链接可以使用表单相关元素的集合进行操作。

这里我加粗了两个关键词:

  • 超链接:表示form元素可以导航到其他的页面,所以它也有其他超链接元素(aarea)所带有的特点,如使用target属性声明新页面使用的浏览上下文、使用rel属性声明超链接的语义。关于这一部分,我在《详解HTML链接元素》做了充分的讲解。
  • 表单相关元素的集合:并不是所有form元素的子代都属于表单相关元素。可充当表单相关元素的有:buttonfieldsetinputobjectoutputselecttextareaimg以及表单相关的自定义元素,而其他的像label元素并不在这个集合之内。form元素可以通过elements属性访问它内部的表单相关元素的集合,反过来,表单相关元素可以通过form属性访问它所归属的form元素。

在表单相关元素中,你可能不太熟悉的是「表单相关的自定义元素」。当我们定义一个自定义元素的时候,可以使用static formAssociated = true表示这是一个表单相关的自定义元素,它会继承表单相关元素的语义以及功能:

// 声明表单相关的自定义元素
class MyCheckbox extends HTMLElement {
  static formAssociated = true;
  // ...
}

基于篇幅限制,我无法在这里展开表单相关的自定义元素的内容,让我们把注意力放回到更加核心的内容上。

可提交元素

在上面的form元素含义中,其实我省略后半句,完整的一句是这样的:

The form element represents a hyperlink that can be manipulated through a collection of form-associated elements, some of which can represent editable values that can be submitted to a server for processing.

form元素代表了一个超链接,这个超链接可以使用表单相关元素的集合进行操作,其中一些元素代表可编辑的值,且这些值可以提交到服务端进行处理。

这里的「一些元素」,指的是buttoninputselecttextarea以及表单相关的自定义元素,这些元素统称为「可提交元素(sumbittable elements)」,也只有这些元素可以作为表单的数据进行提交。

当表单进行提交的时候,浏览器会遍历表单内所有的可提交元素。如果一个可提交元素的name属性不为空,浏览器就会提取该元素的name属性值、value属性值,把这两个值结成对,称为一个「entry」。当所有可提交元素遍历完成后,就会得到一个「entry列表」,这个entry列表就是最终提交的数据的基础。

举个例子,在下面这个简易的登陆框中:

<form action="/login" method="POST">
    <p><label>用户名:<input type="text" name="username" required></label></p>
    <p><label>密码:<input type="password" name="password" required></label></p>
    <p><input type=submit name="login_type" value="vip"></p>
</form>

最终得到的entry列表一共有三个entry:

username:   "waterfish"
password:   123456
login_type: "vip"

对于一些特定类型的可提交元素,构建entry的过程略有不同:

  • 对于type=file的input元素,会给选中的文件创建一个File对象,作为entry的value值。
  • 对于type=checkbox以及type=radio的input元素,如果没有声明value属性,value值使用字符串"on"代替。
  • 对于type=image的input元素,会生成两个entry,一个对应图片x轴的坐标位置,一个对应y轴的坐标位置。
  • 对于select元素,每一个被选中的option都会创建一个entry。
  • 对于表单相关的自定义元素,使用其自定义的提交值。

如果你感兴趣,可以查看标准对于构建entry列表的完整过程

校验用户输入

实际上,在创建entry列表之前,浏览器会先对所有可提交元素的值的合法性做一次校验。有的校验规则由元素本身的类型决定,比如对于input[type=email],浏览器会检查它的值是否符合邮件地址格式。而开发者也可以自己声明一些校验规则,比如在下面这个例子中,required属性表示这个空格是必填:

<p><label>用户名:<input type="text" name="username" required></label></p>

在提交表单前,你也可以先自己使用脚本去校验每一个控件的输入是否合法,具体有以下方式:

  • form.checkValidity():如果form里面所有控件都合法,返回true,否则返回false。

  • element.checkValidity():如果控件合法则返回true,否则返回false。

  • element.validationMessage:如果控件有错误信息,返回错误信息。

  • element.validity.[错误类型]:检查特定的错误类型,没错则返回false,否则返回true。具体有哪些错误类型我们马上会讲到。

    // 检查一个必填项是否有值
    input.validity.valueMissing
    

开发者可以给可提交元素添加的约束包括:

约束描述例子对应的错误类型
必填使用required属性表示,控件的值不能为空。<input type="text" name="username" required>valueMissing
输入的类型控件的值必须与其类型相匹配(Email、URL)<input type="email" name="email">typeMissmatch
输入的格式使用pattern属性表示,控件的值必须匹配一个正则表达式<input title="编号,必须是1到3位数" pattern="[0-9]{1,3}" name="number"/>patternMismatch
输入长度的限制使用maxlengthminlength属性表示<textarea title="自我介绍" name="introduction" minlength="30" maxlength="200"></textarea>tooLong /tooShort
输入范围的限制使用maxmin属性表示<input name="bday" type="date" max="1979-12-31">rangeUnderflow /rangeOverflow
输入的颗粒度使用step属性表示<input name="opacity" type="range" min=0 max=1 step=0.01>stepMissmatch

除此以外,你还可以通过脚本设置更加灵活的约束。当用户的输入不合法的时候,你可以通过element.setCustomValidity(error)来触发原生的表单报错机制,其错误类型为customError

<label>对程序员的刻板印象: <input type="text" oninput="check(this)"></label>
<script>
 function check(input) {
   if (input.value == "秃头" ||
       input.value == "直男" ||
       input.value == "格子衫") {
     input.setCustomValidity('输入错误:"' + input.value + '" 不是刻板印象。');
   } else {
     // 输入合法,重置错误信息
     input.setCustomValidity('');
   }
 }
</script>

还有一种上面没有提到的错误类型,叫badInput,当浏览器认为你还没有完成输入,不足以转为一个合法的值时,就会报这个错误。

提交表单

标准给我们列出了一个表单提交完整的过程,对于这一长串的东西,其实可以总结为三个步骤:

  1. 校验用户输入是否合法;
  2. 根据用户的输入构建entry列表;
  3. 创建新的页面导航。

前两个步骤,我们在上面已经讲完了,接下来只剩最后一个步骤了。

在创建新的页面导航前,浏览器会先查看form元素的method属性,method接受3个值:

  • get:表示HTTP GET方法;这时表单会把前面构建的entry列表进行序列化,并把序列化的结果以查询指令的形式拼接到URL上,然后导航到这个URL。如以下例子:

    <form action="https://example.com" method="get">
      <p><label>名称: <input name="name"></label></p>
      <p><input type="submit"></p>
    </form>
    <!-- 最终导航的URL: https://example.com/?name=waterfish -->
    
  • post:表示HTTP POST方法;这时表单会根据enctype属性值对entry列表进行不同形式的编码,编码的结果会作为POST请求的body进行提交。具体有三种编码形式:

    • application/x-www-form-urlencoded:这是默认值,会使用application/x-www-form-urlencoded编码算法对entry列表进行编码。
    • multipart/form-data:会使用multipart/form-data编码算法会entry列表进行编码,一般用在传输文件上。
    • text/plain:会使用text/plain编码算法进行编码。
  • dialog:表示关闭一个对话框,不会发起请求。如以下例子:

    <dialog id="bug-report">
     <form method=dialog>
      <p>同事说你的代码有bug。回复:</p>
      <button type=submit value="nobug1">你不会用。</button>
      <button type=submit value="nobug2">你的环境有问题。</button>
     </form>
    </dialog>
    <script>
     var bugReport = document.getElementById('bug-report');
     bugReport.showModal();
     bugReport.onclose = function (event) {
       if (bugReport.returnValue == 'nobug1') {
         // ...
       } else {
         // ...
       }
     };
    </script>
    

上面所说的都是针对协议名为HTTP(s)的URL,除了HTTP(s),表单的URL还可以使用ftpjavascriptdatamailto这些协议名。他们的行为与HTTP(s)会有所差异。比如,以下是一段外包公司写的HTML代码:

<form action="javascript:alert('清除完毕')" method="post">
    <fieldset>
        <legend> 清除缓存:请选择清除内容 </legend>
        <p><label> <input type=checkbox name="picture"> 图片缓存 </label></p>
        <p><label> <input type=checkbox name="files"> 文件缓存 </label></p>
        <p><label> <input type=checkbox name="history"> 历史记录 </label></p>
    </fieldset>
    <button type="submit">确定</button>
</form>

当协议名为javascript:的时候,浏览器会忽略entry列表,直接导航URL的内容。也就是说,上面这个表单实际上只做了一个弹窗显示功能。