本章内容
理解表单基础
文本框验证与交互
使用其他表单控件
1 表单基础
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 属性。
1.1 提交表单
<!-- 自定义提交按钮 -->
<button type="submit">Submit Form</button>
let form = document.getElementById("myForm");
// 提交表单
form.submit();
1.2 重置表单
let form = document.getElementById("myForm");
// 重置表单
form.reset();
1.3 表单字段
let form = document.getElementById("form1");
// 取得表单中的第一个字段
let field1 = form.elements[0];
// 取得表单中名为"textbox1"的字段
let field2 = form.elements["textbox1"];
// 取得字段的数量
let fieldCount = form.elements.length;
1. 表单字段的公共属性
除fieldset元素以外,所有表单字段都有一组同样的属性。由于input类型可以表示多种表单字段,因此某些属性只适用于特定类型的字段。除此之外的属性可以在任何表单字段上使用。以下列出了这些表单字段的公共属性和方法。
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";
第一次点击之后禁用提交按钮。可以通过监听 submit事件来实现。比如下面这个例子:
// 避免多次提交表单的代码
let form = document.getElementById("myForm");
form.addEventListener("submit", (event) => {
let target = event.target;
// 取得提交按钮
let btn = target.elements["submit-btn"];
// 禁用提交按钮
btn.disabled = true;
});
2. 表单字段的公共方法
每个表单字段都有两个公共方法:focus()和 blur()。
HTML5 为表单字段增加了 autofocus 属性,支持的浏览器会自动为带有该属性的元素设置焦点,而无须使用 JavaScript。比如:
<input type="text" autofocus>
必须先检测元素上是否设置了该属性。如果设置了 autofocus,就不再调用 focus():
window.addEventListener("load", (event) => {
let element = document.forms[0].elements[0];
if (element.autofocus !== true) {
element.focus();
console.log("JS focus");
}
});
blur()用于从元素上移除焦点。
3. 表单字段的公共事件
除了鼠标、键盘、变化和 HTML 事件外,所有字段还支持以下 3 个事件。
blur:在字段失去焦点时触发。
change:在input和textarea元素的 value 发生变化且失去焦点时触发,或者在select元素中选中项发生变化时触发。
focus:在字段获得焦点时触发。
blur 和 focus 事件会因为用户手动改变字段焦点或者调用 blur()或 focus()方法而触发。
change会因控件不同而在不同时机触发。input和textarea元素,change 事件会在字段失去焦点,同时 value 自控件获得焦点后发生变化时触发。对于select元素,change 事件会在用户改变了选中项时触发,不需要控件失去焦点。
注意 blur 和 change 事件,不能依赖这两个事件触发的顺序,必须区分时要多加注意。
2 文本框编程
单行使用input元素,多行使用textarea元素。
2.1 选择文本
两种文本框都支持一个名为 select()的方法,此方法用于全部选中文本框中的文本。
let textbox = document.forms[0].elements["textbox1"];
textbox.select();
让用户能够一次性删除所有默认内容。可以通过以下代码来实现:
textbox.addEventListener("focus", (event) => {
event.target.select();
});
1. select 事件
当选中文本框中的文本时,会触发 select 事件。这个事件确切的触发时机因浏览器而异。
2. 取得选中文本
要取得文本框中选中的文本,可以使用以下代码:
function getSelectedText(textbox){
return textbox.value.substring(textbox.selectionStart,
textbox.selectionEnd); // 分别表示文本选区的起点和终点
}
老版本 IE(IE8 及更早版本),不支持上述方法。老版本有一个包含整个文档中文本选择信息的 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;
}
}
3. 部分选中文本
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"
老版本IE,必须先使用 IE 在文本框上提供的 createTextRange()方法创建一个范围,并使用 moveStart()和 moveEnd()范围方法把这个范围放到正确的位置上。不过,在调用这两个方法前需要先调用 collapse()方法把范围折叠到文本框的开始。接着,moveStart()可以把范围的起点和终点都移动到相同的位置,再给moveEnd()传入要选择的字符总数作为参数。最后一步是使用范围的 select()方法选中文本,如下面的例子所示:
textbox.value = "Hello world!";
var range = textbox.createTextRange();
// 选择第 4~6 个字符
range.collapse(true);
range.moveStart("character", 4);
range.moveEnd("character", 6);
range.select(); // "o w"
2.2 输入过滤
1. 屏蔽字符
下面就是只允许输入数字的代码:
textbox.addEventListener("keypress", (event) => {
if (!/\d/.test(String.fromCharCode(event.charCode))){
event.preventDefault();
}
});
2. 处理剪贴板
以下是与剪贴板相关的 6 个事件。
beforecopy:复制操作发生前触发。
copy:复制操作发生时触发。
beforecut:剪切操作发生前触发。
cut:剪切操作发生时触发。
beforepaste:粘贴操作发生前触发。
paste:粘贴操作发生时触发。
剪贴板上的数据可以通过 window 对象(IE)或 event 对象(Firefox、Safari 和 Chrome)上的clipboardData 对象来获取。clipboardData 对象上有 3 个方法:getData()、setData()和 clearData()。
- getData():方法从剪贴板检索字符串数据,并接收一个参数,该参数是要检索的数据的格式。IE 为此规定了两个选项:“text"和"URL”。Firefox、Safari 和 Chrome 则期待 MIME 类型,不过会将"text"视为等价于"text/plain"。
- setData()方法也类似,其第一个参数用于指定数据类型,第二个参数是要放到剪贴板上的文本。Safari 和 Chrome 不认可"text"类型。只有在 IE8 及更早版本中调用 setData()才有效,其他浏览器会忽略对这个方法的调用。
2.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");
2.4 HTML5 约束验证 API
1. 必填字段
<input type="text" name="username" required>
//可以通过JavaScript 检测对应元素的 required 属性来判断表单字段是否为必填例如:
let isUsernameRequired = document.forms[0].elements["username"].required;
2. 更多输入类型
<input type="email" name="email">
<input type="url" name="homepage">
3. 数值范围
指定 min 属性(最小可能值)、max 属性(最大可能值),以及 step属性(从 min 到 max 的步长值)。例如,如果只允许输入 0 到 100 中 5 的倍数,那么可以这样写:
<input type="number" min="0" max="100" step="5" name="count" id="myNumber">
// input 加5,stepDown()不传默认减1
document.getElementById("myNumber").stepUp(5);
4. 输入模式
<input type="text" pattern="\d+" name="count">
假设有^和$。这意味着输入内容必须从头到尾都严格与模式匹配。
5. 检测有效性
使用 checkValidity()方法可以检测表单中任意给定字段是否有效。必填字段如果没有值就会被视为无效,而字段值不匹配 pattern 属性也会被视为无效。
要检查整个表单是否有效,可以直接在表单上调用 checkValidity()方法。这个方法会在所有字段都有效时返回 true,有一个字段无效就会返回 false:
都有效时返回 true,有一个字段无效就会返回 false:
if(document.forms[0].checkValidity()){
// 表单有效,继续
} else {
// 表单无效
}
而 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。
6. 禁用验证
通过指定 novalidate 属性可以禁止对表单进行任何验证:
<form method="post" action="/signup" novalidate>
<!-- 表单元素 -->
</form>
// js关闭验证
document.forms[0].noValidate = true;
// 提交无需验证
<input type="submit" formnovalidate name="btnNoValidate"
value="Non-validating Submit">
// js提交无需验证
document.forms[0].elements["btnNoValidate"].formNoValidate = true;
3 选择框编程
选择框是使用和元素创建的。为方便交互,HTMLSelectElement 类型在所有表单字段的公共能力之外又提供了以下属性和方法。
add(newOption, relOption):在 relOption 之前向控件中添加新的。
multiple:布尔值,表示是否允许多选,等价于 HTML 的 multiple 属性。
options:控件中所有元素的 HTMLCollection。
remove(index):移除给定位置的选项。
selectedIndex:选中项基于 0 的索引值,如果没有选中项则为–1。对于允许多选的列表,始终是第一个选项的索引。
size:选择框中可见的行数,等价于 HTML 的 size 属性。
选择框的 type 属性可能是"select-one"或"select-multiple",具体取决于 multiple 属性是否存在。当前选中项根据以下规则决定选择框的 value 属性。
如果没有选中项,则选择框的值是空字符串。
如果有一个选中项,且其 value 属性有值,则选择框的值就是选中项 value 属性的值。即使value 属性的值是空字符串也是如此。
如果有一个选中项,且其 value 属性没有指定值,则选择框的值是该项的文本内容。
如果有多个选中项,则选择框的值根据前两条规则取得第一个选中项的值。
每个option元素在 DOM 中都由一个 HTMLOptionElement 对象表示。HTMLOptionElement类型为方便数据存取添加了以下属性。
index:选项在 options 集合中的索引。
label:选项的标签,等价于 HTML 的 label 属性。
selected:布尔值,表示是否选中了当前选项。把这个属性设置为 true 会选中当前选项。
text:选项的文本。
value:选项的值(等价于 HTML 的 value 属性)。
<select name="location" id="selLocation">
<option value="Sunnyvale, CA">Sunnyvale</option>
</select>
let selectbox = document.forms[0].elements["location"];
// 推荐
let text = selectbox.options[0].text; // 选项文本
let value = selectbox.options[0].value; // 选项值
3.1 选项处理
对于只允许选择一项的选择框,获取选项最简单的方式是使用选择框的 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}`);
selectbox.options[0].selected = true; // 选中第一项
与 selectedIndex 不同,设置选项的 selected 属性不会在多选时移除其他选项,从而可以动态选择任意多个选项。如果修改单选框中选项的 selected 属性,则其他选项会被移除。要注意的是,把selected 属性设置为 false 对单选框没有影响。
3.2 添加选项
可以使用 JavaScript 动态创建选项并将它们添加到选择框。可以使用 DOM 方法,如下所示:
let newOption = document.createElement("option");
newOption.appendChild(document.createTextNode("Option text")); // 在 IE8 及更低版本中有问题
newOption.setAttribute("value", "Option value");
selectbox.appendChild(newOption);
// 使用 Option 构造函数创建新选项
let newOption = new Option("Option text", "Option value");
selectbox.add(newOption, undefined); // 最佳方案
3.3 移除选项
selectbox.removeChild(selectbox.options[0]); // 移除第一项
selectbox.remove(0); // 移除第一项
selectbox.options[0] = null; // 移除第一项
3.4 移动和重排选项
直接将某个选项从第一个选择框移动到第二个选择框,只要对相应选项使用 appendChild()方法即可。
let selectbox1 = document.getElementById("selLocations1");
let selectbox2 = document.getElementById("selLocations2");
selectbox2.appendChild(selectbox1.options[0]);
下面的代码演示了将一个选项在选择框中前移一个位置:
let optionToMove = selectbox.options[1];
selectbox.insertBefore(optionToMove, selectbox.options[optionToMove.index-1]);
4 表单序列化
表单序列化(form serialization)已经成为一个常见需求。表单在 JavaScript 中可以使用表单字段的 type 属性连同其 name 属性和 value 属性来进行序列化。
5 富文本编辑
基本的技术就是在空白 HTML 文件中嵌入一个iframe。通过 designMode 属性,可以将这个空白文档变成可以编辑的,实际编辑的则是body元素的 HTML。designMode 属性有两个可能的值:“off”(默认值)和"on"。设置为"on"时,整个文档都会变成可以编辑的(显示插入光标),从而可以像使用文字处理程序一样编辑文本,通过键盘将文本标记为粗体、斜体,等等。
<iframe name="richedit" style="height: 100px; width: 100px"></iframe>
<script>
window.addEventListener("load", () => {
frames["richedit"].document.designMode = "on";
});
</script>
5.1 使用 contenteditable
可以给页面中的任何元素指定 contenteditable 属性,然后该元素会立即被用户编辑。这种方式更受欢迎
<div class="editable" id="richedit" contenteditable></div>
5.2 与富文本交互
与富文本编辑器交互的主要方法是使用 document.execCommand()。这个方法在文档上执行既定的命令,可以实现大多数格式化任务。document.execCommand()可以接收 3 个参数:要执行的命令、表示浏览器是否为命令提供用户界面的布尔值和执行命令必需的值(如果不需要则为 null)。为跨浏览器兼容,第二个参数应该始终为 false,因为 Firefox 会在其为 true 时抛出错误。
// 在内嵌窗格中切换粗体文本样式
frames["richedit"].document.execCommand("bold", false, null);
// 在内嵌窗格中切换斜体文本样式
frames["richedit"].document.execCommand("italic", false, null);
// 在内嵌窗格中创建指向 www.wrox.com 的链接
frames["richedit"].document.execCommand("createlink", false,
"http://www.wrox.com");
// 在内嵌窗格中为内容添加<h1>标签
frames["richedit"].document.execCommand("formatblock", false, "<h1>");
同样的方法也可以用于页面中添加了 contenteditable 属性的元素,只不过要使用当前窗口而不是内嵌窗格中的 document 对象:
// 切换粗体文本样式
document.execCommand("bold", false, null);
queryCommandEnabled(),此方法用于确定对当前选中文本或光标所在位置是否可以执行相关命令。可以执行该命令就返回 true。
let result = frames["richedit"].document.queryCommandEnabled("bold");
queryCommandState()用于确定相关命令是否应用到了当前文本选区。例如,要确定当前选区的文本是否为粗体,可以这样:
let isBold = frames["richedit"].document.queryCommandState("bold");
5.3 富文件选择
在内嵌窗格中使用 getSelection()方法,可以获得富文本编辑器的选区。返回表示当前选中文本的 Selection 对象。每个 Selection 对象都拥
有以下属性。
anchorNode:选区开始的节点。
anchorOffset:在 anchorNode 中,从开头到选区开始跳过的字符数。
focusNode:选区结束的节点。
focusOffset:focusNode 中包含在选区内的字符数。
isCollapsed:布尔值,表示选区起点和终点是否在同一个地方。
rangeCount:选区中包含的 DOM 范围数量。
它的以下方法提供了更多信息,并允许操作选区。
addRange(range):把给定的 DOM 范围添加到选区。
collapse(node, offset):将选区折叠到给定节点中给定的文本偏移处。
collapseToEnd():将选区折叠到终点。
collapseToStart():将选区折叠到起点。
containsNode(node):确定给定节点是否包含在选区中。
deleteFromDocument():从文档中删除选区文本。与执行 execCommand(“delete”, false, null)命令结果相同。
extend(node, offset):通过将 focusNode 和 focusOffset 移动到指定值来扩展选区。
getRangeAt(index):返回选区中指定索引处的 DOM 范围。
removeAllRanges():从选区中移除所有 DOM 范围。这实际上会移除选区,因为选区中至少要包含一个范围。
removeRange(range):从选区中移除指定的 DOM 范围。
selectAllChildren(node):清除选区并选择给定节点的所有子节点。
toString():返回选区中的文本内容。
let selection = frames["richedit"].getSelection();
// 取得选中的文本
let selectedText = selection.toString();
// 取得表示选区的范围
let range = selection.getRangeAt(0);
// 高亮选中的文本
let span = frames["richedit"].document.createElement("span");
span.style.backgroundColor = "yellow";
range.surroundContents(span);
5.4 通过表单提交富文本
因为富文本编辑是在内嵌窗格中或通过为元素指定 contenteditable 属性实现的,而不是在表单控件中实现,这意味着要把富文本编辑的结果提交给服务器,必须手工提取 HTML 并自己提交。
通常的解决方案是在表单中添加一个隐藏字段,使用内嵌窗格或contenteditable 元素的 HTML 更新它的值。在表单提交之前,从内嵌窗格或 contenteditable 元素中提取出 HTML 并插入隐藏字段中。例如,以下代码在使用内嵌窗格实现富文本编辑时,可以用在表单的 onsubmit 事件处理程序中:
form.addEventListener("submit", (event) => {
let target = event.target;
target.elements["comments"].value =
frames["richedit"].document.body.innerHTML;
});