红宝书之第十九章:表单脚本

360 阅读20分钟

表单基础

Web 表单在 HTML 中以

元素表示,在 JavaScript 中则以 HTMLFormElement 类型表示。HTMLFormElement 类型继承自 HTMLElement 类型,因此拥有与其他 HTML 元素一样的默认属性。不过,HTMLFormElement 也有自己的属性和方法 acceptCharset:服务器可以接收的字符集,等价于 HTML 的 accept-charset 属性。

  • action:请求的 URL,等价于 HTML 的 action 属性。
  • elements:表单中所有控件的 HTMLCollection。
  • enctype:请求的编码类型,等价于 HTML 的 enctype 属性。
  • length:表单中控件的数量。
  • method:HTTP 请求的方法类型,通常是"get"或"post",等价于 HTML 的 method 属性。
  • name:表单的名字,等价于 HTML 的 name 属性。
  • reset():把表单字段重置为各自的默认值。
  • submit():提交表单。
  • target:用于发送请求和接收响应的窗口的名字,等价于 HTML 的 target 属性。

有几种方式可以取得对元素的引用。最常用的是将表单当作普通元素为它指定一个 id 属性,从而可以使用 getElementById()来获取表单,比如:

let form = document.getElementById("form1");

此外,使用 document.forms 集合可以获取页面上所有的表单元素。然后,可以进一步使用数字索引或表单的名字(name)来访问特定的表单。比如:

// 取得页面中的第一个表单
let firstForm = document.forms[0];
// 取得名字为"form2"的表单
let myForm = document.forms["form2"];

较早的浏览器,或者严格向后兼容的浏览器,也会把每个表单的 name 作为 document 对象的属性。例如,名为"form2"的表单可以通过 document.form2 来访问。不推荐使用这种方法,因为容易出错,而且这些属性将来可能会被浏览器删除。

注意,表单可以同时拥有 id 和 name,而且两者可以不相同。

提交表单

表单是通过用户点击提交按钮或图片按钮的方式提交的。提交按钮可以使用 type 属性为"submit"的或元素来定义,图片按钮可以使用 type 属性为"image"的元素来定义。点击下面例子中定义的所有按钮都可以提交它们所在的表单:

<!-- 通用提交按钮 -->
<input type="submit" value="Submit Form">
<!-- 自定义提交按钮 -->
<button type="submit">Submit Form</button>
<!-- 图片按钮 -->
<input type="image" src="graphic.gif">

如果表单中有上述任何一个按钮,且焦点在表单中某个控件上,则按回车键也可以提交表单。(textarea 控件是个例外,当焦点在它上面时,按回车键会换行。)注意,没有提交按钮的表单在按回车键时不会提交。

以这种方式提交表单会在向服务器发送请求之前触发 submit 事件。这样就提供了一个验证表单数据的机会,可以根据验证结果决定是否真的要提交。阻止这个事件的默认行为可以取消提交表单。例如,下面的代码会阻止表单提交:

let form = document.getElementById("myForm");
form.addEventListener("submit", (event) => {
    // 阻止表单提交
    event.preventDefault();
});

调用 preventDefault()方法可以阻止表单提交。通常,在表单数据无效以及不应该发送到服务器时可以这样处理。

当然,也可以通过编程方式在 JavaScript 中调用 submit()方法来提交表单。可以在任何时候调用这个方法来提交表单,而且表单中不存在提交按钮也不影响表单提交。下面是一个例子:

let form = document.getElementById("myForm");
// 提交表单
form.submit();

通过 submit()提交表单时,submit 事件不会触发。因此在调用这个方法前要先做数据验证。

表单提交的一个最大的问题是可能会提交两次表单。如果提交表单之后没有什么反应,那么没有耐心的用户可能会多次点击提交按钮。结果是很烦人的(因为服务器要处理重复的请求),甚至可能造成损失(如果用户正在购物,则可能会多次下单)。解决这个问题主要有两种方式:在表单提交后禁用提交按钮,或者通过 onsubmit 事件处理程序取消之后的表单提交。

重置表单

用户单击重置按钮可以重置表单。重置按钮可以使用 type 属性为"reset"的

元素来创建,比如:

<!-- 通用重置按钮 -->
<input type="reset" value="Reset Form">
<!-- 自定义重置按钮 -->
<button type="reset">Reset Form</button>

这两种按钮都可以重置表单。表单重置后,所有表单字段都会重置回页面第一次渲染时各自拥有的值。如果字段原来是空的,就会变成空的;如果字段有默认值,则恢复为默认值。

用户单击重置按钮重置表单会触发 reset 事件。这个事件为取消重置提供了机会。例如,以下代码演示了如何阻止重置表单:

let form = document.getElementById("myForm");
form.addEventListener("reset", (event) => {
    event.preventDefault();
});

与表单提交一样,重置表单也可以通过 JavaScript 调用 reset()方法来完成,如下面的例子所示:

let form = document.getElementById("myForm");
// 重置表单
form.reset();

与 submit()方法的功能不同,调用 reset()方法会像单击了重置按钮一样触发 reset 事件。

注意 表单设计中通常不提倡重置表单,因为重置表单经常会导致用户迷失方向,如果意外触发则会令人感到厌烦。实践中几乎没有重置表单的需求。一般来说,提供一个取消按钮,让用户点击返回前一个页面,而不是恢复表单中所有的值来得更直观。

表单字段

表单元素可以像页面中的其他元素一样使用原生 DOM 方法来访问。此外,所有表单元素都是表单elements 属性(元素集合)中包含的一个值。这个 elements 集合是一个有序列表,包含对表单中所有字段的引用,包括所有、、、和元素。elements集合中的每个字段都以它们在 HTML 标记中出现的次序保存,可以通过索引位置和 name 属性来访问。以下是几个例子:

let form = document.getElementById("form1");
// 取得表单中的第一个字段
let field1 = form.elements[0];
// 取得表单中名为"textbox1"的字段
let field2 = form.elements["textbox1"];
// 取得字段的数量
let fieldCount = form.elements.length;

如果多个表单控件使用了同一个 name,比如像单选按钮那样,则会返回包含所有同名元素的HTMLCollection。比如,来看下面的 HTML 代码片段:

<form method="post" id="myForm">
    <ul>
        <li><input type="radio" name="color" value="red">Red</li>
        <li><input type="radio" name="color" value="green">Green</li>
        <li><input type="radio" name="color" value="blue">Blue</li>
    </ul>
</form>

这个 HTML 中的表单有 3 个单选按钮的 name 是"color",这个名字把它们联系在了一起。在访问elements["color"]时,返回的 NodeList 就包含这 3 个元素。而在访问 elements[0]时,只会返回第一个元素。比如:

let form = document.getElementById("myForm");
let colorFields = form.elements["color"];
console.log(colorFields.length); // 3 
let firstColorField = colorFields[0];
let firstFormField = form.elements[0];
console.log(firstColorField === firstFormField); // true

以上代码表明,使用 form.elements[0]获取的表单的第一个字段就是 form.elements["color"]中包含的第一个元素。

注意 也可以通过表单属性的方式访问表单字段,比如 form[0]这种使用索引和 form ["color"]这种使用字段名字的方式。访问这些属性与访问 form.elements 集合是一样的。这种方式是为向后兼容旧版本浏览器而提供的,实际开发中应该使用 elements。

表单字段的公共属性

元素以外,所有表单字段都有一组同样的属性。由于类型可以表示多种表单字段,因此某些属性只适用于特定类型的字段。除此之外的属性可以在任何表单字段上使用。以下列出了这些表单字段的公共属性和方法。

  • disabled:布尔值,表示表单字段是否禁用。
  • form:指针,指向表单字段所属的表单。这个属性是只读的。
  • name:字符串,这个字段的名字。
  • readOnly:布尔值,表示这个字段是否只读。
  • tabIndex:数值,表示这个字段在按 Tab 键时的切换顺序。
  • type:字符串,表示字段类型,如"checkbox"、"radio"等。
  • value:要提交给服务器的字段值。对文件输入字段来说,这个属性是只读的,仅包含计算机上某个文件的路径。

这里面除了 form 属性以外,JavaScript 可以动态修改任何属性。来看下面的例子:

let form = document.getElementById("myForm");
let field = form.elements[0];
// 修改字段的值
field.value = "Another value";
// 检查字段所属的表单
console.log(field.form === form); // true 
// 给字段设置焦点
field.focus();
// 禁用字段
field.disabled = true;
// 改变字段的类型(不推荐,但对<input>来说是可能的)
field.type = "checkbox";

这种动态修改表单字段属性的能力为任何时候以任何方式修改表单提供了方便。举个例子,Web 表单的一个常见问题是用户常常会点击两次提交按钮。在涉及信用卡扣款的情况下,这是个严重的问题,可能会导致重复扣款。对此,常见的解决方案是第一次点击之后禁用提交按钮。可以通过监听 submit事件来实现。比如下面这个例子:

// 避免多次提交表单的代码
let form = document.getElementById("myForm");
form.addEventListener("submit", (event) => {
    let target = event.target;
    // 取得提交按钮
    let btn = target.elements["submit-btn"];
    // 禁用提交按钮
    btn.disabled = true;
});

以上代码在表单的 submit 事件上注册了一个事件处理程序。当 submit 事件触发时,代码会取得提交按钮,然后将其 disabled 属性设置为 true。注意,这个功能不能通过直接给提交按钮添加onclick 事件处理程序来实现,原因是不同浏览器触发事件的时机不一样。有些浏览器会在触发表单的 submit 事件前先触发提交按钮的 click 事件,有些浏览器则会后触发 click 事件。对于先触发click 事件的浏览器,这个按钮会在表单提交前被禁用,这意味着表单就不会被提交了。因此最好使用表单的 submit 事件来禁用提交按钮。但这种方式不适用于没有使用提交按钮的表单提交。如前所述,只有提交按钮才能触发 submit 事件。

type 属性可以用于除

之外的任何表单字段。对于元素,这个值等于 HTML的 type 属性值。对于其他元素,这个 type 属性的值按照下表设置。

描 述示例 HTML类型的值
单选列表..."select-one"
多选列表..."select-multiple"
自定义按钮<button>......</button>"submit"
自定义非提交按钮<button type="button">...</button>"button"
自定义重置按钮<button type="reset">...</button>"reset"
自定义提交按钮<button type="submit">...</button>"submit

对于

元素,可以动态修改其 type 属性。但元素的 type 属性是只读的。 表单字段的公共方法 每个表单字段都有两个公共方法:focus()和 blur()。 focus()方法把浏览器焦点设置到表单字段,这意味着该字段会变成活动字段并可以响应键盘事件。focus()方法主要用来引起用户对页面中某个部分的注意。 比如,在页面加载后把焦点定位到表单中第一个字段就是很常见的做法。实现方法是监听 load 事件,然后在第一个字段上调用 focus(),如下所示: window.addEventListener("load", (event) => { document.forms[0].elements[0].focus(); }); 注意,如果表单中第一个字段是 type 为"hidden"的元素,或者该字段被 CSS 属性display 或 visibility 隐藏了,以上代码就会出错。

HTML5 为表单字段增加了 autofocus 属性,支持的浏览器会自动为带有该属性的元素设置焦点,而无须使用 JavaScript。比如:

< input type = "text"
autofocus >

为了让之前的代码在使用 autofocus 时也能正常工作,必须先检测元素上是否设置了该属性。如果设置了 autofocus,就不再调用 focus():

window.addEventListener("load", (event) => {
    let element = document.forms[0].elements[0];
    if (element.autofocus !== true) {
        element.focus();
        console.log("JS focus");
    }
});

大多数现代浏览器支持 autofocus 属性

focus()的反向操作是 blur(),其用于从元素上移除焦点。现在很少有用例需要调用 blur(),没有必要的情况下不要做这个骚操作了;

表单字段的公共事件

除了鼠标、键盘、变化和 HTML 事件外,所有字段还支持以下 3 个事件。

  • blur:在字段失去焦点时触发。
  • change:在和元素的 value 发生变化且失去焦点时触发,或者在元素中选中项发生变化时触发。
  • focus:在字段获得焦点时触发。

blur 和 focus 事件会因为用户手动改变字段焦点或者调用 blur()或 focus()方法而触发。这两个事件对所有表单都会一视同仁。

change 事件则不然,它会因控件不同而在不同时机触发。

  • 对于和元素,change 事件会在字段失去焦点,同时 value 自控件获得焦点后发生变化时触发。
  • 对于元素,change 事件会在用户改变了选中项时触发,不需要控件失去焦点。 用途: focus 和 blur 事件通常用于以某种方式改变用户界面,以提供可见的提示或额外功能(例如在文本框下面显示下拉菜单)。 change 事件通常用于验证用户在字段中输入的内容。比如,有的文本框可能只限于接收数值。 比如,有的文本框可能只限于接收数值。focus 事件可以用来改变控件的背景颜色以便更清楚地表明当前字段获得了焦点。blur 事件可以用于去掉这个背景颜色。而 change 事件可以用于在用户输入了非数值时把背景改为红色。以下代码展示了上述操作: let textbox = document.forms[0].elements[0]; textbox.addEventListener("focus", (event) => { console.log('focus') let target = event.target; if (target.style.backgroundColor != "red") { target.style.backgroundColor = "yellow"; } }); textbox.addEventListener("blur", (event) => { console.log('blur') let target = event.target; target.style.backgroundColor = /[^\d]/.test(target.value) ? "red" : ""; }); textbox.addEventListener("change", (event) => { console.log('change') let target = event.target; target.style.backgroundColor = /[^\d]/.test(target.value) ? "red" : ""; }); 这里的 onfocus 事件处理程序会把文本框的背景改为黄色,更清楚地表明它是当前活动字段。onblur 和 onchange 事件处理程序会在发现非数值字符时把背景改为红色。为测试非数值字符,这里使用了一个简单的正则表达式来检测文本框的 value。这个功能必须同时在 onblur 和 onchange 事件处理程序上实现,以确保无论文本框是否改变都能执行验证。 注意 blur 和 change 事件的关系并没有明确定义。在某些浏览器中,blur 事件会先于change 事件触发;在其他浏览器中,触发顺序则相反。因此不能依赖这两个事件触发的顺序,必须区分时要多加注意。 文本框编程 在 HTML 中有两种表示文本框的方式:单行使用元素,多行使用元素。

    input举个例子:要创建一个一次可显示 25 个字符,但最多允许显示 50 个字符的文本框,可以这样写:

    <input type="text" size="25" maxlength="50" value="initial value">
    

    textarea举个例子:

    < textarea rows = "25"
    cols = "5" > initial value < /textarea>
    

    除了标记中的不同,这两种类型的文本框都会在 value 属性中保存自己的内容。通过这个属性,可以读取也可以设置文本模式的值,如下所示:

    let textbox = document.forms[0].elements["textbox1"];
    console.log(textbox.value);
    textbox.value = "Some new value";
    

    选择文本

    两种文本框都支持一个名为 select()的方法,此方法用于全部选中文本框中的文本。

    下面来看一个例子:

    let textbox = document.forms[0].elements["textbox1"];
    textbox.select();
    

    作用:

    • 常见的复制粘贴功能
    • 让用户能够一次性删除所有默认内容

    select 事件

    触发条件:

    • 1,用户自己选择了文本
    • 2,调用了select()方法
    let textbox = document.forms[0].elements["textbox1"];
    textbox.addEventListener("select", (event) => {
        console.log(`Text selected: ${textbox.value}`);
    });
    

    取得选中文本

    扩展为文本框添加了两个属性:selectionStart 和 selectionEnd。这两个属性包含基于 0 的数值,分别表示文本选区的起点和终点(文本选区起点的偏移量和文本选区终点的偏移量)。因此,要取得文本框中选中的文本,可以使用以下代码:

    function getSelectedText(textbox) {
        return textbox.value.substring(textbox.selectionStart, textbox.selectionEnd);
    }
    

    因为 substring()方法是基于字符串偏移量的,所以直接传入 selectionStart 和 selectionEnd就可以取得选中的文本。

    老版本 IE 中有一个包含整个文档中文本选择信息的 document.selection 对象。

    function getSelectedText(textbox) {
        if (typeof textbox.selectionStart == "number") {
            return textbox.value.substring(textbox.selectionStart, textbox.selectionEnd);
        } else if (document.selection) {
            return document.selection.createRange().text;
        }
    }
    

    这个修改后的函数兼容在 IE 老版本中取得选中文本。注意 document.selection 是根本不需要textbox 参数的。

    部分选中文本

    HTML5 也为在文本框中选择部分文本提供了额外支持。现在,除了 select()方法之外,Firefox最早实现的 setSelectionRange()方法也可以在所有文本框中使用。这个方法接收两个参数:要选择的第一个字符的索引和停止选择的字符的索引(与字符串的 substring()方法一样)。下面是几个例子:

    textbox.value = "Hello world!"
    // 选择所有文本
    textbox.setSelectionRange(0, textbox.value.length); // "Hello world!" 
    // 选择前 3 个字符
    textbox.setSelectionRange(0, 3); // "Hel" 
    // 选择第 4~6 个字符
    textbox.setSelectionRange(4, 7); // "o w"
    

    如果想看到选择,则必须在调用 setSelectionRange()之前或之后给文本框设置焦点。这个方法在 IE9、Firefox、Safari、Chrome 和 Opera 中都可以使用。

    IE8 及更早版本支持通过范围部分选中文本。如下面的例子所示:

    textbox.value = "Hello world!";
    var range = textbox.createTextRange();
    // 选择所有文本
    range.collapse(true);
    range.moveStart("character", 0);
    range.moveEnd("character", textbox.value.length); // "Hello world!" 
    range.select();
    // 选择前 3 个字符
    range.collapse(true);
    range.moveStart("character", 0);
    range.moveEnd("character", 3);
    range.select(); // "Hel" 
    // 选择第 4~6 个字符
    range.collapse(true);
    range.moveStart("character", 4);
    range.moveEnd("character", 6);
    range.select(); // "o w"
    

    与其他浏览器一样,如果想要看到选中的效果,则必须让文本框获得焦点。

    部分选中文本对自动完成建议项等高级文本输入框是很有用的。

    输入过滤

    屏蔽字符

    如果想只屏蔽特定字符,则需要检查事件的 charCode 属性,以确定正确的回应方式。例如,下面就是只允许输入数字的代码:

    textbox.addEventListener("keypress", (event) => {
                if (!/\d/.test(String.fromCharCode(event.charCode))) {
                    event.preventDefault();
                }
            }
    

    这个例子先用String.fromCharCode()把事件的charCode 转换为字符串,再用正则表达式/\d/来测试。这个正则表达式匹配所有数字字符,如果测试失败就调用 preventDefault()屏蔽事件默认行为。这样就可以让文本框忽略非数字输入。

    在Firefox 中,所有触发 keypress 事件的非字符键的 charCode 都是 0,而在 Safari 3 之前这些键的charCode 都是 8。综合考虑这些情况,就是不能屏蔽 charCode 小于 10 的键。为此,上面的函数可以改进为:

    textbox.addEventListener("keypress", (event) => {
        if (!/\d/.test(String.fromCharCode(event.charCode)) && event.charCode > 9) {
            event.preventDefault();
        }
    });
    

    还有一个问题需要处理:复制、粘贴及涉及 Ctrl 键的其他功能。在除 IE 外的所有浏览器中,前面代码会屏蔽快捷键 Ctrl+C、Ctrl+V 及其他使用 Ctrl 的组合键。因此,最后一项检测是确保没有按下 Ctrl键,如下面的例子所示:

    textbox.addEventListener("keypress", (event) => {
        if (!/\d/.test(String.fromCharCode(event.charCode)) && event.charCode > 9 && !event.ctrlKey) {
            event.preventDefault();
        }
    });
    

    最后这个改动可以确保所有默认的文本框行为不受影响。这个技术可以用来自定义是否允许在文本框中输入某些字符。

    处理剪贴板

    以下是与剪贴板相关的 6 个事件。

    • beforecopy:复制操作发生前触发。
    • copy:复制操作发生时触发。
    • beforecut:剪切操作发生前触发。
    • cut:剪切操作发生时触发。
    • beforepaste:粘贴操作发生前触发。
    • paste:粘贴操作发生时触发。

    这是一个比较新的控制剪贴板访问的标准,事件的行为及相关对象会因浏览器而异。在 Safari、Chrome 和 Firefox 中,beforecopy、beforecut 和 beforepaste 事件只会在显示文本框的上下文菜单(预期会发生剪贴板事件)时触发,但 IE 不仅在这种情况下触发,也会在 copy、cut 和 paste 事件之前触发。无论是在上下文菜单中做出选择还是使用键盘快捷键,copy、cut 和 paste 事件在所有浏览器中都会按预期触发。

    剪贴板上的数据可以通过 window 对象(IE)或 event 对象(Firefox、Safari 和 Chrome)上的clipboardData 对象来获取。clipboardData 对象上有 3 个方法:getData()、setData()和 clearData();

    function getClipboardText(event) {
        var clipboardData = (event.clipboardData || window.clipboardData);
        return clipboardData.getData("text");
    }
    
    function setClipboardText(event, value) {
        if (event.clipboardData) {
            return event.clipboardData.setData("text/plain", value);
        } else if (window.clipboardData) {
            return window.clipboardData.setData("text", value);
        }
    }
    

    这里的 getClipboardText()函数相对简单,它只需要知道 clipboardData 对象在哪里,然后便可以通过"text"类型调用 getData()。相应的,setClipboardText()函数则要复杂一些。在确定clipboardData 对象的位置之后,需要根据实现以相应的类型(Firefox、Safari 和 Chrome 是"text/plain",而 IE 是"text")调用 setData()。

    如果文本框期待某些字符或某种格式的文本,那么从剪贴板中读取文本是有帮助的。比如,如果文本框只允许输入数字,那么就必须检查粘贴过来的值,确保其中只包含数字。在 paste 事件中,可以确定剪贴板上的文本是否无效,如果无效就取消默认行为,如下面的例子所示:

    textbox.addEventListener("paste", (event) => {
        let text = getClipboardText(event);
        if (!/^\d*$/.test(text)) {
            event.preventDefault();
        }
    });
    

    这个 onpaste 事件处理程序确保只有数字才能粘贴到文本框中。如果剪贴板中的值不符合指定模式,则取消粘贴操作。Firefox、Safari 和 Chrome 只允许在 onpaste 事件处理程序中访问 getData()方法。

    自动切换

    JavaScript 可以通过很多方式来增强表单字段的易用性。最常用的是在当前字段完成时自动切换到下一个字段。

    在美国,电话号码通常分为 3 个部分:区号、交换局号,外加 4 位数字。在网页中,可以通过 3 个文本框来表示这几个部分,比如:

    <input type="text" name="tel1" id="txtTel1" maxlength="3">
    <input type="text" name="tel2" id="txtTel2" maxlength="3">
    <input type="text" name="tel3" id="txtTel3" maxlength="4">
    

    为增加这个表单的易用性并加速数据输入,可以在每个文本框输入到最大允许字符数时自动把焦点切换到下一个文本框。因此,当用户在第一个文本框中输入 3 个字符后,就把焦点移到第二个文本框,当用户在第二个文本框中输入 3 个字符后,把焦点再移到第三个文本框。这种自动切换文本框的行为可以通过如下代码实现:

    function tabForward(event) {
        let target = event.target;
        if (target.value.length == target.maxLength) {
            let form = target.form;
            for (let i = 0, len = form.elements.length; i < len; i++) {
                if (form.elements[i] == target) {
                    if (form.elements[i + 1]) {
                        form.elements[i + 1].focus();
                    }
                    return;
                }
            }
        }
    }
    let inputIds = ["txtTel1", "txtTel2", "txtTel3"];
    for (let id of inputIds) {
        let textbox = document.getElementById(id);
        textbox.addEventListener("keyup", tabForward);
    }
    let textbox1 = document.getElementById("txtTel1");
    let textbox2 = document.getElementById("txtTel2");
    let textbox3 = document.getElementById("txtTel3");
    

    HTML5 约束验证 API

    HTML5为浏览器新增了在提交表单前验证数据的能力。

    验证会根据某些条件应用到表单字段。可以使用 HTML 标记指定对特定字段的约束,然后浏览器会根据这些约束自动执行表单验证。

    必填字段

    第一个条件是给表单字段添加 required 属性,如下所示:

    < input type = "text"
    name = "username"
    required >
    

    可以通过JavaScript 检测对应元素的 required 属性来判断表单字段是否为必填:

    let isUsernameRequired = document.forms[0].elements["username"].required;
    

    还可以使用下面的代码检测浏览器是否支持 required 属性:

    let isRequiredSupported = "required" in document.createElement("input");
    

    这行代码使用简单的特性检测来确定新创建的元素上是否存在 required 属性。

    注意,不同浏览器处理必填字段的机制不同。Firefox、Chrome、IE 和 Opera 会阻止表单提交并在相应字段下面显示有帮助信息的弹框,而 Safari 什么也不做,也不会阻止提交表单。

    更多输入类型

    HTML5 为元素增加了几个新的 type 值。其中两个新的输入类型是已经得到广泛支持的"email"和"url",二者都有浏览器提供的自定义验证。

    <input type="email" name="email">
    <input type="url" name="homepage">
    

    数值范围

    HTML5 还定义了其他几种新的输入元素类型,它们都是期待某种数值输入的,包括:"number"、"range"、"datetime"、"datetime-local"、"date"、"month"、"week"和"time"。并非所有主流浏览器都支持这些类型,因此使用时要当心

    输入模式

    HTML5 为文本字段新增了 pattern 属性。这个属性用于指定一个正则表达式,用户输入的文本必须与之匹配。

    例如,要限制只能在文本字段中输入数字,可以这样添加模式:

    <input type="text" pattern="\d+" name="count">
    

    注意模式的开头和末尾分别假设有^和$。这意味着输入内容必须从头到尾都严格与模式匹配。

    通过访问 pattern 属性可以读取模式:

    let pattern = document.forms[0].elements["count"].pattern;
    

    使用如下代码可以检测浏览器是否支持 pattern 属性:

    let isPatternSupported = "pattern" in document.createElement("input");
    

    检测有效性

    使用 checkValidity()方法可以检测表单中任意给定字段是否有效。这个方法在所有表单元素上都可以使用,如果字段值有效就会返回 true,否则返回 false。

    if (document.forms[0].elements[0].checkValidity()) {
        // 字段有效,继续
    } else {
        // 字段无效
    }
    

    要检查整个表单是否有效,可以直接在表单上调用 checkValidity()方法。这个方法会在所有字段都有效时返回 true,有一个字段无效就会返回 false:

    if (document.forms[0].checkValidity()) {
        // 表单有效,继续
    } else {
        // 表单无效
    }
    

    checkValidity()方法只会告诉我们字段是否有效,而 validity 属性会告诉我们字段为什么有效或无效。这个属性是一个对象,包含一系列返回布尔值的属性。

    • customError:如果设置了 setCustomValidity()就返回 true,否则返回 false。
    • patternMismatch:如果字段值不匹配指定的 pattern 属性则返回 true。
    • rangeOverflow:如果字段值大于 max 的值则返回 true。
    • rangeUnderflow:如果字段值小于 min 的值则返回 true。
    • stepMisMatch:如果字段值与 min、max 和 step 的值不相符则返回 true。
    • tooLong:如果字段值的长度超过了 maxlength 属性指定的值则返回 true。某些浏览器,如Firefox 4 会自动限制字符数量,因此这个属性值始终为 false。
    • typeMismatch:如果字段值不是"email"或"url"要求的格式则返回 true。
    • valid:如果其他所有属性的值都为 false 则返回 true。与 checkValidity()的条件一致。
    • valueMissing:如果字段是必填的但没有值则返回 true。

    因此,通过 validity 属性可以检查表单字段的有效性,从而获取更具体的信息,如下面的代码所示:

    if (input.validity && !input.validity.valid) {
        if (input.validity.valueMissing) {
            console.log("Please specify a value.")
        } else if (input.validity.typeMismatch) {
            console.log("Please enter an email address.");
        } else {
            console.log("Value is invalid.");
        }
    }
    

    禁用验证

    通过指定 novalidate 属性可以禁止对表单进行任何验证:

    <form method="post" action="/signup" novalidate>
        <!-- 表单元素 -->
    </form>
    

    这个值也可以通过 JavaScript 属性 noValidate 检索或设置,设置为 true 表示属性存在,设置为false 表示属性不存在:

    document.forms[0].noValidate = true; // 关闭验证
    

    如果一个表单中有多个提交按钮,那么可以给特定的提交按钮添加 formnovalidate 属性,指定通过该按钮无须验证即可提交表单:

    <form method="post" action="/foo">
        <!-- 表单元素 -->
        <input type="submit" value="Regular Submit">
        <input type="submit" formnovalidate name="btnNoValidate" value="Non-validating Submit">
    </form>
    

    在这个例子中,第一个提交按钮会让表单像往常一样验证数据,第二个提交按钮则禁用了验证,可以直接提交表单。我们也可以使用 JavaScript 来设置这个属性:

    // 关闭验证
    document.forms[0].elements["btnNoValidate"].formNoValidate = true;
    

    选择框编程

    选择框是使用<select>和<option>元素创建的。为方便交互,HTMLSelectElement 类型在所有表单字段的公共能力之外又提供了以下属性和方法。

    • add(newOption, relOption):在 relOption 之前向控件中添加新的<option>。
    • multiple:布尔值,表示是否允许多选,等价于 HTML 的 multiple 属性。
    • options:控件中所有<option>元素的 HTMLCollection。
    • remove(index):移除给定位置的选项。
    • selectedIndex:选中项基于 0 的索引值,如果没有选中项则为–1。对于允许多选的列表,始终是第一个选项的索引。
    • size:选择框中可见的行数,等价于 HTML 的 size 属性。

    选择框的 type 属性可能是"select-one"或"select-multiple",具体取决于 multiple 属性是否存在。当前选中项根据以下规则决定选择框的 value 属性。

    • 如果没有选中项,则选择框的值是空字符串。
    • 如果有一个选中项,且其 value 属性有值,则选择框的值就是选中项 value 属性的值。即使value 属性的值是空字符串也是如此。
    • 如果有一个选中项,且其 value 属性没有指定值,则选择框的值是该项的文本内容。
    • 如果有多个选中项,则选择框的值根据前两条规则取得第一个选中项的值。

    来看下面的选择框:

    <select name="location" id="selLocation">
        <option value="Sunnyvale, CA">Sunnyvale</option>
        <option value="Los Angeles, CA">Los Angeles</option>
        <option value="Mountain View, CA">Mountain View</option>
        <option value="">China</option>
        <option>Australia</option>
    </select>
    

    如果选中这个选择框中的第一项,则字段的值就是"Sunnyvale, CA"。如果文本为"China"的项被选中,则字段的值是一个空字符串,因为该项的 value 属性是空字符串。如果选中最后一项,那么字段的值是"Australia",因为该元素没有指定 value 属性。

    每个元素在 DOM 中都由一个 HTMLOptionElement 对象表示。HTMLOptionElement类型为方便数据存取添加了以下属性。

    • index:选项在 options 集合中的索引。
    • label:选项的标签,等价于 HTML 的 label 属性。
    • selected:布尔值,表示是否选中了当前选项。把这个属性设置为 true 会选中当前选项。
    • text:选项的文本。
    • value:选项的值(等价于 HTML 的 value 属性)。

    大多数属性是为了方便存取选项数据。可以使用常规 DOM 功能存取这些信息,只是效率比较低,如下面的例子所示:

    let selectbox = document.forms[0].elements["location"];
    // 不推荐
    let text = selectbox.options[0].firstChild.nodeValue; // 选项文本
    let value = selectbox.options[0].getAttribute("value"); // 选项值
    

    以上代码使用标准的 DOM 技术获取了选择框中第一个选项的文本和值。下面再比较一下使用特殊选项属性的代码:

    let selectbox = document.forms[0].elements["location"];
    // 推荐
    let text = selectbox.options[0].text; // 选项文本
    let value = selectbox.options[0].value; // 选项值
    

    最后强调一下,选择框的 change 事件与其他表单字段是不一样的。其他表单字段会在自己的值改变后触发 change 事件,然后字段失去焦点。而选择框会在选中一项时立即触发 change 事件。

    选项处理

    对于只允许选择一项的选择框,获取选项最简单的方式是使用选择框的 selectedIndex 属性,如下面的例子所示:

    let selectedOption = selectbox.options[selectbox.selectedIndex];
    

    这样可以获取关于选项的所有信息,比如:

    let selectedIndex = selectbox.selectedIndex;
    let selectedOption = selectbox.options[selectedIndex];
    console.log(`Selected index: ${selectedIndex}\n` +
        `Selected text: ${selectedOption.text}\n` +
        `Selected value: ${selectedOption.value}`);
    

    以上代码打印出了选中项的索引及其文本和值。

    对于允许多选的选择框,selectedIndex 属性就像只允许选择一项一样。设置 selectedIndex会移除所有选项,只选择指定的项,而获取 selectedIndex 只会返回选中的第一项的索引。

    如何取得多选的值:通过 selected 属性可以确定选择框中哪个选项被选中。要取得所有选中项,需要循环选项集合逐一检测 selected 属性,比如:

    function getSelectedOptions(selectbox) {
        let result = new Array();
        for (let option of selectbox.options) {
            if (option.selected) {
                result.push(option);
            }
        }
        return result;
    }
    

    添加选项

    可以使用 JavaScript 动态创建选项并将它们添加到选择框。

    方法一:可以使用 DOM 方法,如下所示:

    let newOption = document.createElement("option");
    newOption.appendChild(document.createTextNode("Option text"));
    newOption.setAttribute("value", "Option value");
    selectbox.appendChild(newOption);
    

    以上代码创建了一个新的元素,使用文本节点添加文本,设置其 value 属性,然后将其添加到选择框。添加到选择框之后,新选项会立即显示出来。

    方法二:Option 构造函数创建新选项,这个构造函数是 DOM 出现之前就已经得到浏览器支持的。

    Option 构造函数接收两个参数:text 和 value,其中 value 是可选的。虽然这个构造函数通常会创建 Object 的实例,但 DOM 合规的浏览器都会返回一个元素。这意味着仍然可以使用 appendChild()方法把这样创建的选项添加到选择框。比如下面的例子:

    let newOption = new Option("Option text", "Option value");
    selectbox.appendChild(newOption); // 在 IE8 及更低版本中有问题
    

    方法三:使用选择框的 add()方法

    DOM 规定这个方法接收两个参数:要添加的新选项和要添加到其前面的参考选项。如果想在列表末尾添加选项,那么第二个参数应该是 null。

    DOM 合规的浏览器要求必须传入第二个参数,因此在跨浏览器方法中不能只使用一个参数,传入 undefined 作为第二个参数可以保证在所有浏览器中都将选项添加到列表末尾。

    let newOption = new Option("Option text", "Option value");
    selectbox.add(newOption, undefined); // 最佳方案
    

    以上代码可以在所有版本的 IE 及 DOM 合规的浏览器中使用。如果不想在最后插入新选项,则应该使用 DOM 技术和 insertBefore()。

    移除选项

    与添加选项类似,移除选项的方法也不止一种。

    方法一:使用 DOM 的 removeChild()方法并传入要移除的选项,

    selectbox.removeChild(selectbox.options[0]); // 移除第一项
    

    方法二:使用选择框的 remove()方法。这个方法接收一个参数,即要移除选项的索引,

    selectbox.remove(0); // 移除第一项
    

    方法三:直接将选项设置为等于 null。这同样也是 DOM 之前浏览器实现的方式。

    selectbox.options[0] = null; // 移除第一项
    

    要清除选择框的所有选项,需要迭代所有选项并逐一移除它们,如下面例子所示:

    function clearSelectbox(selectbox) {
        for (let option of selectbox.options) {
            selectbox.remove(0);
        }
    }
    

    这个函数可以逐一移除选择框中的每一项。因为移除第一项会自动将所有选项向前移一位,所以这样就可以移除所有选项。

    移动和重排选项

    DOM 方法则可以直接将某个选项从第一个选择框移动到第二个选择框,只要对相应选项使用 appendChild()方法即可。

    let selectbox1 = document.getElementById("selLocations1");
    let selectbox2 = document.getElementById("selLocations2");
    selectbox2.appendChild(selectbox1.options[0]);
    

    要将选项移动到选择框中的特定位置,insertBefore()方法是最合适的。不过,要把选项移动到最后,还是 appendChild()方法比较方便

    下面的代码演示了将一个选项在选择框中前移一个位置:

    let optionToMove = selectbox.options[1];
    selectbox.insertBefore(optionToMove,
        selectbox.options[optionToMove.index - 1]);
    

    这个例子首先获得要移动选项的索引,然后将其插入之前位于它前面的选项之前,其中第二行代码适用于除第一个选项之外的所有选项。下面的代码则可以将选项向下移动一个位置:

    let optionToMove = selectbox.options[1];
    selectbox.insertBefore(optionToMove,
        selectbox.options[optionToMove.index + 2]);
    

    表单序列化

    在写代码之前,我们需要理解浏览器如何确定在提交表单时要把什么发送到服务器。

    • 字段名和值是 URL 编码的并以和号(&)分隔。
    • 禁用字段不会发送。
    • 复选框或单选按钮只在被选中时才发送。
    • 类型为"reset"或"button"的按钮不会发送。
    • 多选字段的每个选中项都有一个值。
    • 通过点击提交按钮提交表单时,会发送该提交按钮;否则,不会发送提交按钮。类型为"image"的<input>元素视同提交按钮。
    • <select>元素的值是被选中<option>元素的 value 属性。如果<option>元素没有 value 属性,则该值是它的文本。

    富文本编辑

    两种方式实现:第一个是iframe, 第二个是contenteditable;

    与富文本交互

    与富文本编辑器交互的主要方法是使用 document.execCommand()。

    第一个方法是 queryCommandEnabled(),此方法用于确定对当前选中文本或光标所在位置是否可以执行相关命令。

    另一个方法 queryCommandState()用于确定相关命令是否应用到了当前文本选区。

    最后一个方法是 queryCommandValue(),此方法可以返回执行命令时使用的值

    富文件选择

    在内嵌窗格中使用 getSelection()方法,可以获得富文本编辑器的选区。

    通过表单提交富文本

    因为富文本编辑是在内嵌窗格中或通过为元素指定 contenteditable 属性实现的,而不是在表单控件中实现,所以富文本编辑器技术上与表单没有关系。这意味着要把富文本编辑的结果提交给服务器,必须手工提取 HTML 并自己提交。

    form.addEventListener("submit", (event) => {
        let target = event.target;
        target.elements["comments"].value =
            frames["richedit"].document.body.innerHTML;
    });