# 表單,FormData 對象
## 表單概述
表單(`<form>`)用來收集用戶提交的數據,發送到服務器。比如,用戶提交用戶名和密碼,讓服務器驗證,就要通過表單。表單提供多種控件,讓開發者使用,具體的控件種類和用法請參考 HTML 語言的教程。本章主要介紹 JavaScript 與表單的交互。
```html
<form action="/handling-page" method="post">
<div>
<label for="name">用戶名:</label>
<input type="text" id="name" name="user_name" />
</div>
<div>
<label for="passwd">密碼:</label>
<input type="password" id="passwd" name="user_passwd" />
</div>
<div>
<input type="submit" id="submit" name="submit_button" value="提交" />
</div>
</form>
```
上面代碼就是一個簡單的表單,包含三個控件:用戶名輸入框、密碼輸入框和提交按鈕。
用戶點擊“提交”按鈕,每一個控件都會生成一個鍵值對,鍵名是控件的`name`屬性,鍵值是控件的`value`屬性,鍵名和鍵值之間由等號連接。比如,用戶名輸入框的`name`屬性是`user_name`,`value`屬性是用戶輸入的值,假定是“張三”,提交到服務器的時候,就會生成一個鍵值對`user_name=張三`。
所有的鍵值對都會提交到服務器。但是,提交的數據格式跟`<form>`元素的`method`屬性有關。該屬性指定了提交數據的 HTTP 方法。如果是 GET 方法,所有鍵值對會以 URL 的查詢字符串形式,提交到服務器,比如`/handling-page?user_name=張三&user_passwd=123&submit_button=提交`。下面就是 GET 請求的 HTTP 頭信息。
```http
GET /handling-page?user_name=張三&user_passwd=123&submit_button=提交
Host: example.com
```
如果是 POST 方法,所有鍵值對會連接成一行,作為 HTTP 請求的數據體發送到服務器,比如`user_name=張三&user_passwd=123&submit_button=提交`。下面就是 POST 請求的頭信息。
```http
POST /handling-page HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 74
user_name=張三&user_passwd=123&submit_button=提交
```
注意,實際提交的時候,只要鍵值不是 URL 的合法字符(比如漢字“張三”和“提交”),瀏覽器會自動對其進行編碼。
點擊`submit`控件,就可以提交表單。
```html
<form>
<input type="submit" value="提交">
</form>
```
上面表單就包含一個`submit`控件,點擊這個控件,瀏覽器就會把表單數據向服務器提交。
注意,表單里面的`<button>`元素如果沒有用`type`屬性指定類型,那么默認就是`submit`控件。
```html
<form>
<button>提交</button>
</form>
```
上面表單的`<button>`元素,點擊以后也會提交表單。
除了點擊`submit`控件提交表單,還可以用表單元素的`submit()`方法,通過腳本提交表單。
```javascript
formElement.submit();
```
表單元素的`reset()`方法可以重置所有控件的值(重置為默認值)。
```javascript
formElement.reset()
```
## FormData 對象
### 概述
表單數據以鍵值對的形式向服務器發送,這個過程是瀏覽器自動完成的。但是有時候,我們希望通過腳本完成這個過程,構造或編輯表單的鍵值對,然后通過腳本發送給服務器。瀏覽器原生提供了 FormData 對象來完成這項工作。
`FormData()`首先是一個構造函數,用來生成表單的實例。
```javascript
var formdata = new FormData(form);
```
`FormData()`構造函數的參數是一個 DOM 的表單元素,構造函數會自動處理表單的鍵值對。這個參數是可選的,如果省略該參數,就表示一個空的表單。
下面是一個表單。
```html
<form id="myForm" name="myForm">
<div>
<label for="username">用戶名:</label>
<input type="text" id="username" name="username">
</div>
<div>
<label for="useracc">賬號:</label>
<input type="text" id="useracc" name="useracc">
</div>
<div>
<label for="userfile">上傳文件:</label>
<input type="file" id="userfile" name="userfile">
</div>
<input type="submit" value="Submit!">
</form>
```
我們用`FormData()`處理上面這個表單。
```javascript
var myForm = document.getElementById('myForm');
var formData = new FormData(myForm);
// 獲取某個控件的值
formData.get('username') // ""
// 設置某個控件的值
formData.set('username', '張三');
formData.get('username') // "張三"
```
### 實例方法
FormData 提供以下實例方法。
- `FormData.get(key)`:獲取指定鍵名對應的鍵值,參數為鍵名。如果有多個同名的鍵值對,則返回第一個鍵值對的鍵值。
- `FormData.getAll(key)`:返回一個數組,表示指定鍵名對應的所有鍵值。如果有多個同名的鍵值對,數組會包含所有的鍵值。
- `FormData.set(key, value)`:設置指定鍵名的鍵值,參數為鍵名。如果鍵名不存在,會添加這個鍵值對,否則會更新指定鍵名的鍵值。如果第二個參數是文件,還可以使用第三個參數,表示文件名。
- `FormData.delete(key)`:刪除一個鍵值對,參數為鍵名。
- `FormData.append(key, value)`:添加一個鍵值對。如果鍵名重復,則會生成兩個相同鍵名的鍵值對。如果第二個參數是文件,還可以使用第三個參數,表示文件名。
- `FormData.has(key)`:返回一個布爾值,表示是否具有該鍵名的鍵值對。
- `FormData.keys()`:返回一個遍歷器對象,用于`for...of`循環遍歷所有的鍵名。
- `FormData.values()`:返回一個遍歷器對象,用于`for...of`循環遍歷所有的鍵值。
- `FormData.entries()`:返回一個遍歷器對象,用于`for...of`循環遍歷所有的鍵值對。如果直接用`for...of`循環遍歷 FormData 實例,默認就會調用這個方法。
下面是`get()`、`getAll()`、`set()`、`append()`方法的例子。
```javascript
var formData = new FormData();
formData.set('username', '張三');
formData.append('username', '李四');
formData.get('username') // "張三"
formData.getAll('username') // ["張三", "李四"]
formData.append('userpic[]', myFileInput.files[0], 'user1.jpg');
formData.append('userpic[]', myFileInput.files[1], 'user2.jpg');
```
下面是遍歷器的例子。
```javascript
var formData = new FormData();
formData.append('key1', 'value1');
formData.append('key2', 'value2');
for (var key of formData.keys()) {
console.log(key);
}
// "key1"
// "key2"
for (var value of formData.values()) {
console.log(value);
}
// "value1"
// "value2"
for (var pair of formData.entries()) {
console.log(pair[0] + ': ' + pair[1]);
}
// key1: value1
// key2: value2
// 等同于遍歷 formData.entries()
for (var pair of formData) {
console.log(pair[0] + ': ' + pair[1]);
}
// key1: value1
// key2: value2
```
## 表單的內置驗證
### 自動校驗
表單提交的時候,瀏覽器允許開發者指定一些條件,它會自動驗證各個表單控件的值是否符合條件。
```html
<!-- 必填 -->
<input required>
<!-- 必須符合正則表達式 -->
<input pattern="banana|cherry">
<!-- 字符串長度必須為6個字符 -->
<input minlength="6" maxlength="6">
<!-- 數值必須在1到10之間 -->
<input type="number" min="1" max="10">
<!-- 必須填入 Email 地址 -->
<input type="email">
<!-- 必須填入 URL -->
<input type="URL">
```
如果一個控件通過驗證,它就會匹配`:valid`的 CSS 偽類,瀏覽器會繼續進行表單提交的流程。如果沒有通過驗證,該控件就會匹配`:invalid`的 CSS 偽類,瀏覽器會終止表單提交,并顯示一個錯誤信息。
```css
input:invalid {
border-color: red;
}
input,
input:valid {
border-color: #ccc;
}
```
### checkValidity()
除了提交表單的時候,瀏覽器自動校驗表單,還可以手動觸發表單的校驗。表單元素和表單控件都有`checkValidity()`方法,用于手動觸發校驗。
```javascript
// 觸發整個表單的校驗
form.checkValidity()
// 觸發單個表單控件的校驗
formControl.checkValidity()
```
`checkValidity()`方法返回一個布爾值,`true`表示通過校驗,`false`表示沒有通過校驗。因此,提交表單可以封裝為下面的函數。
```javascript
function submitForm(action) {
var form = document.getElementById('form');
form.action = action;
if (form.checkValidity()) {
form.submit();
}
}
```
### willValidate 屬性
控件元素的`willValidate`屬性是一個布爾值,表示該控件是否會在提交時進行校驗。
```javascript
// HTML 代碼如下
// <form novalidate>
// <input id="name" name="name" required />
// </form>
var input = document.querySelector('#name');
input.willValidate // true
```
### validationMessage 屬性
控件元素的`validationMessage`屬性返回一個字符串,表示控件不滿足校驗條件時,瀏覽器顯示的提示文本。以下兩種情況,該屬性返回空字符串。
- 該控件不會在提交時自動校驗
- 該控件滿足校驗條件
```javascript
// HTML 代碼如下
// <form><input type="text" required></form>
document.querySelector('form input').validationMessage
// "請填寫此字段。"
```
下面是另一個例子。
```javascript
var myInput = document.getElementById('myinput');
if (!myInput.checkValidity()) {
document.getElementById('prompt').innerHTML = myInput.validationMessage;
}
```
### setCustomValidity()
控件元素的`setCustomValidity()`方法用來定制校驗失敗時的報錯信息。它接受一個字符串作為參數,該字符串就是定制的報錯信息。如果參數為空字符串,則上次設置的報錯信息被清除。
這個方法可以替換瀏覽器內置的表單驗證報錯信息,參數就是要顯示的報錯信息。
```html
<form action="somefile.php">
<input
type="text"
name="username"
placeholder="Username"
pattern="[a-z]{1,15}"
id="username"
>
<input type="submit">
</form>
```
上面的表單輸入框,要求只能輸入小寫字母,且不得超過15個字符。如果輸入不符合要求(比如輸入“ABC”),提交表單的時候,Chrome 瀏覽器會彈出報錯信息“Please match the requested format.”,禁止表單提交。下面使用`setCustomValidity()`方法替換掉報錯信息。
```javascript
var input = document.getElementById('username');
input.oninvalid = function (event) {
event.target.setCustomValidity(
'用戶名必須是小寫字母,不能為空,最長不超過15個字符'
);
}
```
上面代碼中,`setCustomValidity()`方法是在`invalid`事件的監聽函數里面調用。該方法也可以直接調用,這時如果參數不為空字符串,瀏覽器就會認為該控件沒有通過校驗,就會立刻顯示該方法設置的報錯信息。
```javascript
/* HTML 代碼如下
<form>
<p><input type="file" id="fs"></p>
<p><input type="submit"></p>
</form>
*/
document.getElementById('fs').onchange = checkFileSize;
function checkFileSize() {
var fs = document.getElementById('fs');
var files = fs.files;
if (files.length > 0) {
if (files[0].size > 75 * 1024) {
fs.setCustomValidity('文件不能大于 75KB');
return;
}
}
fs.setCustomValidity('');
}
```
上面代碼一旦發現文件大于 75KB,就會設置校驗失敗,同時給出自定義的報錯信息。然后,點擊提交按鈕時,就會顯示報錯信息。這種校驗失敗是不會自動消除的,所以如果所有文件都符合條件,要將報錯信息設為空字符串,手動消除校驗失敗的狀態。
### validity 屬性
控件元素的屬性`validity`屬性返回一個`ValidityState`對象,包含當前校驗狀態的信息。
該對象有以下屬性,全部為只讀屬性。
- `ValidityState.badInput`:布爾值,表示瀏覽器是否不能將用戶的輸入轉換成正確的類型,比如用戶在數值框里面輸入字符串。
- `ValidityState.customError`:布爾值,表示是否已經調用`setCustomValidity()`方法,將校驗信息設置為一個非空字符串。
- `ValidityState.patternMismatch`:布爾值,表示用戶輸入的值是否不滿足模式的要求。
- `ValidityState.rangeOverflow`:布爾值,表示用戶輸入的值是否大于最大范圍。
- `ValidityState.rangeUnderflow`:布爾值,表示用戶輸入的值是否小于最小范圍。
- `ValidityState.stepMismatch`:布爾值,表示用戶輸入的值不符合步長的設置(即不能被步長值整除)。
- `ValidityState.tooLong`:布爾值,表示用戶輸入的字數超出了最長字數。
- `ValidityState.tooShort`:布爾值,表示用戶輸入的字符少于最短字數。
- `ValidityState.typeMismatch`:布爾值,表示用戶填入的值不符合類型要求(主要是類型為 Email 或 URL 的情況)。
- `ValidityState.valid`:布爾值,表示用戶是否滿足所有校驗條件。
- `ValidityState.valueMissing`:布爾值,表示用戶沒有填入必填的值。
下面是一個例子。
```javascript
var input = document.getElementById('myinput');
if (input.validity.valid) {
console.log('通過校驗');
} else {
console.log('校驗失敗');
}
```
下面是另外一個例子。
```javascript
var txt = '';
if (document.getElementById('myInput').validity.rangeOverflow) {
txt = '數值超過上限';
}
document.getElementById('prompt').innerHTML = txt;
```
如果想禁止瀏覽器彈出表單驗證的報錯信息,可以監聽`invalid`事件。
```javascript
var input = document.getElementById('username');
var form = document.getElementById('form');
var elem = document.createElement('div');
elem.id = 'notify';
elem.style.display = 'none';
form.appendChild(elem);
input.addEventListener('invalid', function (event) {
event.preventDefault();
if (!event.target.validity.valid) {
elem.textContent = '用戶名必須是小寫字母';
elem.className = 'error';
elem.style.display = 'block';
input.className = 'invalid animated shake';
}
});
input.addEventListener('input', function(event){
if ( 'block' === elem.style.display ) {
input.className = '';
elem.style.display = 'none';
}
});
```
上面代碼中,一旦發生`invalid`事件(表單驗證失敗),`event.preventDefault()`用來禁止瀏覽器彈出默認的驗證失敗提示,然后設置定制的報錯提示框。
### 表單的 novalidate 屬性
表單元素的 HTML 屬性`novalidate`,可以關閉瀏覽器的自動校驗。
```html
<form novalidate>
</form>
```
這個屬性也可以在腳本里設置。
```javascript
form.noValidate = true;
```
如果表單元素沒有設置`novalidate`屬性,那么提交按鈕(`<button>`或`<input>`元素)的`formnovalidate`屬性也有同樣的作用。
```html
<form>
<input type="submit" value="submit" formnovalidate>
</form>
```
## enctype 屬性
表單能夠用四種編碼,向服務器發送數據。編碼格式由表單的`enctype`屬性決定。
假定表單有兩個字段,分別是`foo`和`baz`,其中`foo`字段的值等于`bar`,`baz`字段的值是一個分為兩行的字符串。
```
The first line.
The second line.
```
下面四種格式,都可以將這個表單發送到服務器。
**(1)GET 方法**
如果表單使用`GET`方法發送數據,`enctype`屬性無效。
```html
<form
action="register.php"
method="get"
onsubmit="AJAXSubmit(this); return false;"
>
</form>
```
數據將以 URL 的查詢字符串發出。
```http
?foo=bar&baz=The%20first%20line.%0AThe%20second%20line.
```
**(2)application/x-www-form-urlencoded**
如果表單用`POST`方法發送數據,并省略`enctype`屬性,那么數據以`application/x-www-form-urlencoded`格式發送(因為這是默認值)。
```html
<form
action="register.php"
method="post"
onsubmit="AJAXSubmit(this); return false;"
>
</form>
```
發送的 HTTP 請求如下。
```http
Content-Type: application/x-www-form-urlencoded
foo=bar&baz=The+first+line.%0D%0AThe+second+line.%0D%0A
```
上面代碼中,數據體里面的`%0D%0A`代表換行符(`\r\n`)。
**(3)text/plain**
如果表單使用`POST`方法發送數據,`enctype`屬性為`text/plain`,那么數據將以純文本格式發送。
```html
<form
action="register.php"
method="post"
enctype="text/plain"
onsubmit="AJAXSubmit(this); return false;"
>
</form>
```
發送的 HTTP 請求如下。
```http
Content-Type: text/plain
foo=bar
baz=The first line.
The second line.
```
**(4)multipart/form-data**
如果表單使用`POST`方法,`enctype`屬性為`multipart/form-data`,那么數據將以混合的格式發送。
```html
<form
action="register.php"
method="post"
enctype="multipart/form-data"
onsubmit="AJAXSubmit(this); return false;"
>
</form>
```
發送的 HTTP 請求如下。
```http
Content-Type: multipart/form-data; boundary=---------------------------314911788813839
-----------------------------314911788813839
Content-Disposition: form-data; name="foo"
bar
-----------------------------314911788813839
Content-Disposition: form-data; name="baz"
The first line.
The second line.
-----------------------------314911788813839--
```
這種格式也是文件上傳的格式。
## 文件上傳
用戶上傳文件,也是通過表單。具體來說,就是通過文件輸入框選擇本地文件,提交表單的時候,瀏覽器就會把這個文件發送到服務器。
```html
<input type="file" id="file" name="myFile">
```
此外,還需要將表單`<form>`元素的`method`屬性設為`POST`,`enctype`屬性設為`multipart/form-data`。其中,`enctype`屬性決定了 HTTP 頭信息的`Content-Type`字段的值,默認情況下這個字段的值是`application/x-www-form-urlencoded`,但是文件上傳的時候要改成`multipart/form-data`。
```html
<form method="post" enctype="multipart/form-data">
<div>
<label for="file">選擇一個文件</label>
<input type="file" id="file" name="myFile" multiple>
</div>
<div>
<input type="submit" id="submit" name="submit_button" value="上傳" />
</div>
</form>
```
上面的 HTML 代碼中,file 控件的`multiple`屬性,指定可以一次選擇多個文件;如果沒有這個屬性,則一次只能選擇一個文件。
```javascript
var fileSelect = document.getElementById('file');
var files = fileSelect.files;
```
然后,新建一個 FormData 實例對象,模擬發送到服務器的表單數據,把選中的文件添加到這個對象上面。
```javascript
var formData = new FormData();
for (var i = 0; i < files.length; i++) {
var file = files[i];
// 只上傳圖片文件
if (!file.type.match('image.*')) {
continue;
}
formData.append('photos[]', file, file.name);
}
```
最后,使用 Ajax 向服務器上傳文件。
```javascript
var xhr = new XMLHttpRequest();
xhr.open('POST', 'handler.php', true);
xhr.onload = function () {
if (xhr.status !== 200) {
console.log('An error occurred!');
}
};
xhr.send(formData);
```
除了發送 FormData 實例,也可以直接 AJAX 發送文件。
```javascript
var file = document.getElementById('test-input').files[0];
var xhr = new XMLHttpRequest();
xhr.open('POST', 'myserver/uploads');
xhr.setRequestHeader('Content-Type', file.type);
xhr.send(file);
```
## 參考鏈接
- [HTML5 Form Validation With the “pattern” Attribute](https://webdesign.tutsplus.com/tutorials/html5-form-validation-with-the-pattern-attribute--cms-25145), Thoriq Firdaus
- 前言
- 入門篇
- 導論
- 歷史
- 基本語法
- 數據類型
- 概述
- null,undefined 和布爾值
- 數值
- 字符串
- 對象
- 函數
- 數組
- 運算符
- 算術運算符
- 比較運算符
- 布爾運算符
- 二進制位運算符
- 其他運算符,運算順序
- 語法專題
- 數據類型的轉換
- 錯誤處理機制
- 編程風格
- console 對象與控制臺
- 標準庫
- Object 對象
- 屬性描述對象
- Array 對象
- 包裝對象
- Boolean 對象
- Number 對象
- String 對象
- Math 對象
- Date 對象
- RegExp 對象
- JSON 對象
- 面向對象編程
- 實例對象與 new 命令
- this 關鍵字
- 對象的繼承
- Object 對象的相關方法
- 嚴格模式
- 異步操作
- 概述
- 定時器
- Promise 對象
- DOM
- 概述
- Node 接口
- NodeList 接口,HTMLCollection 接口
- ParentNode 接口,ChildNode 接口
- Document 節點
- Element 節點
- 屬性的操作
- Text 節點和 DocumentFragment 節點
- CSS 操作
- Mutation Observer API
- 事件
- EventTarget 接口
- 事件模型
- Event 對象
- 鼠標事件
- 鍵盤事件
- 進度事件
- 表單事件
- 觸摸事件
- 拖拉事件
- 其他常見事件
- GlobalEventHandlers 接口
- 瀏覽器模型
- 瀏覽器模型概述
- window 對象
- Navigator 對象,Screen 對象
- Cookie
- XMLHttpRequest 對象
- 同源限制
- CORS 通信
- Storage 接口
- History 對象
- Location 對象,URL 對象,URLSearchParams 對象
- ArrayBuffer 對象,Blob 對象
- File 對象,FileList 對象,FileReader 對象
- 表單,FormData 對象
- IndexedDB API
- Web Worker
- 附錄:網頁元素接口
- a
- img
- form
- input
- button
- option
- video,audio