做者:valentinogagliardijavascript
譯者:前端小智html
來源:github前端
阿(a)裏(li)雲(yun)今年比去年便宜,10.24~11.11購買是1年86元,3年229元,能夠點擊這裏進行參與:java
爲了保證的可讀性,本文采用意譯而非直譯。git
網頁不只僅是用來顯示數據的。有了 HTML 表單,我們能夠收集和操做用戶數據。在本章中,經過構建一個簡單的 HTML 表單來學習表單的相關的知識。github
在這個過程當中,會了解更多關於 DOM 事件的信息,從在 第8章
咱們知道了一個 <form>
元素是一個 HTML 元素,它可能包含其餘的子元素,好比:web
<input>
用於捕獲數據<textarea>
用於捕獲文本<button>
用於提交表單在本章中,我們構建一個包含 <input>
、<textarea>
和 <button>
的表彰。理想狀況下,每一個 input
都應該具備 type
的屬性,該屬性指示輸入類型: 例如 text
、email
、number
、date
等。除了 type
屬性以外,可能還但願向每一個表單元素添加 id
屬性。redis
input
和 textarea
也能夠有一個 name
屬性。若是大家想在不使用 JS 的狀況下發送表單,name 屬性很是重要。稍後會詳細介紹。數據庫
另外,將每一個表單元素與 <label>
關聯也是一種常見的方式。在下面的示例中,會看到每一個 label
與 for
屬性綁定對應 input
元素的 id
,做用是點擊 label
元素就能讓 input
聚焦。數組
若是沒有填寫全部須要的信息,用戶將沒法提交表單。這是一個避免空數據的簡單驗證,從而防止用戶跳太重要字段。有了這些知識,如今就能夠建立 HTML 表單了。建立一個名爲 form.html 的新文件並構建 HTML:
<html lang="en">
<head>
<meta charset="UTF-8">
<title>HTML forms and JavaScript</title>
</head>
<body>
<h1>What's next?</h1>
<form>
<label for="name">Name</label>
<input type="text" id="name" name="name" required>
<label for="description">Short description</label>
<input type="text" id="description" name="description" required>
<label for="task">Task</label>
<textarea id="task" name="tak" required></textarea>
<button type="submit">Submit</button>
</form>
</body>
<script src="form.js"></script>
</html>
複製代碼
如上所述,表單中的 input
具備正確的屬性,從如今開始,能夠經過填充一些數據來測試表單。 編寫 HTML 表單時,要特別注意 type
屬性,由於它決定了用戶可以輸入什麼樣的數據。
HTML5 還引入了表單驗證:例如,類型爲 email
的輸入只接受帶有「at」
符 號@
的電子郵件地址。不幸的是,這是對電子郵件地址應用的唯一檢查:沒有人會阻止用戶輸入相似 a@a
這樣的電子郵件。它有 @
,但仍然是無效的(用於電子郵件輸入的 pattern
屬性能夠幫助解決這個問題。
在 <input>
上有不少可用的屬性,我發現 minlength
和 maxlength
是最有用的兩個。在實戰中,它們能夠阻止懶惰的垃圾郵件發送者發送帶有 「aa」
或 「testtest」
的表單。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>HTML forms and JavaScript</title>
</head>
<body>
<h1>What's next?</h1>
<form>
<label for="name">Name</label>
<input type="text" id="name" name="name" required minlength="5">
<label for="description">Short description</label>
<input type="text" id="description" name="description" required minlength="5">
<label for="task">Task</label>
<textarea id="task" name="tak" required minlength="10"></textarea>
<button type="submit">Submit</button>
</form>
</body>
<script src="form.js"></script>
</html>
複製代碼
有了這個表單,我們就能夠更進一步了,接着,來看下錶單是如何工做的。
HTML 表單是 HTMLFormElement 類型的一個元素。與幾乎全部的 HTML 元素同樣,它鏈接到 HTMLElement
,後者又鏈接到 EventTarget
。當咱們訪問 DOM 元素時,它們被表示爲 JS 對象。在瀏覽器中試試這個:
const aForm = document.createElement("form");
console.log(typeof aForm);
複製代碼
輸出是 「object」
,而像 HTMLElement
或 EventTarget
這樣的實體是函數:
console.log(typeof EventTarget); // "function"
複製代碼
所以,若是任何 HTML 元素都鏈接到 EventTarget
,這意味着 <form>
是 EventTarget
的「實例」,以下:
const aForm = document.createElement("form");
console.log(aForm instanceof EventTarget); // true
複製代碼
form
是 EventTarget
的一種專門化類型。每一個EventTarget
均可以接收和響應 DOM 事件(如第8章所示)。
DOM 事件有不少類型,好比 click
、blur
、change
等等。如今,我們感興趣的是 HTML 表單特有的 submit
事件。當用戶單擊 input
或 type
爲 「submit」 的按鈕(元素必須出如今表單中)時,就會分派 submit
事件,以下所示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>HTML forms and JavaScript</title>
</head>
<body>
<h1>What's next?</h1>
<form>
<label for="name">Name</label>
<input type="text" id="name" name="name" required minlength="5">
<label for="description">Short description</label>
<input type="text" id="description" name="description" required minlength="5">
<label for="task">Task</label>
<textarea id="task" name="task" required minlength="10"></textarea>
<button type="submit">Submit</button>
</form>
</body>
<script src="form.js"></script>
</html>
複製代碼
請注意,<button type="submit">Submit</button>
就在表單內部。 一些開發人員使用input
方式:
<!-- 通用提交按鈕 -->
<input type="submit">
<!-- 自定義提交按鈕 -->
<button>提交表單</button>
<!-- 圖像按鈕 -->
<input type='image' src='av.gif'/>
複製代碼
只要表單存在上面 列出的任何一種按鈕,那麼在相應表單控件擁有焦點的狀況下,按回車鍵就能夠提交表單。(textarea 是一個例外,在文本中回車會換行。)若是表單裏沒有提交按鈕,按回車鍵不會提交表單。
我們的目標是獲取表單上的全部用戶輸入,因此,須要監聽 submit
事件。
const formSelector = document.querySelector("form");
new Form(formSelector);
複製代碼
DOM 還提供 document.forms
,這是頁面內全部表單的集合。 我們如今只須要:
const formSelector = document.forms[0];
new Form(formSelector);
複製代碼
如今的想法是:給定一個表單選擇器,咱們能夠註冊一個事件監聽器來響應表單的發送。爲了註冊監聽器,咱們可使用構造函數,並讓它調用一個名爲 init
的方法。在與 form.html 相同的文件夾中建立一個名爲 form.js 的新文件,並從一個簡單的類開始:
"use strict";
class Form {
constructor(formSelector) {
this.formSelector = formSelector;
this.init();
}
init() {
this.formSelector.addEventListener("submit", this.handleSubmit);
}
}
複製代碼
我們的事件監聽器是 this.handleSubmit
,與每一個事件監聽器同樣,它能夠訪問名爲 event
的參數。 從第8章中應該知道,事件是實際分派的事件,其中包含有關動做自己的許多有用信息。 我們來實現 this.handleSubmit
:
"use strict";
class Form {
constructor(formSelector) {
this.formSelector = formSelector;
this.init();
}
init() {
this.formSelector.addEventListener("submit", this.handleSubmit);
}
handleSubmit(event) {
console.log(event);
}
}
複製代碼
而後,實例化類 From
:
const formSelector = document.forms[0];
new Form(formSelector);
複製代碼
此時,在瀏覽器中打開 form.html。輸入內容並點擊「提交」。會發生什麼呢? 輸出以下:
http://localhost:63342/little-javascript/code/ch10/form.html?name=Valentino&description=Trip+to+Spoleto&tak=We%27re+going+to+visit+the+city%21
複製代碼
這是怎麼回事? 大多數 DOM 事件都有所謂的「默認行爲」。submit
事件尤爲嘗試將表單數據發送到虛構的服務器。這就是在沒有 JS的 狀況下發送表單的方式,由於它是基於 Django、Rails和friends 等 web 框架的應用程序的一部分。
每一個輸入值都映射到相應的 name
屬性。在本例中不須要 name
,由於這裏我們想用 JS 來控制表單,因此須要禁用默認行爲。能夠經過調用 preventDefault
來禁用:
"use strict";
class Form {
constructor(formSelector) {
this.formSelector = formSelector;
this.init();
}
init() {
this.formSelector.addEventListener("submit", this.handleSubmit);
}
handleSubmit(event) {
event.preventDefault();
console.log(event);
}
}
const formSelector = document.forms[0];
new Form(formSelector);
複製代碼
保存文件,而後再次刷新 form.html。 嘗試填寫表格,而後單擊提交。 會看到 event
對象打印到控制檯:
Event {...}
bubbles: true
cancelBubble: false
cancelable: true
composed: false
currentTarget: null
defaultPrevented: true
eventPhase: 0
isTrusted: true
path: (5) [form, body, html, document, Window]
returnValue: false
srcElement: form
target: form
timeStamp: 8320.840000000317
type: "submit"
複製代碼
在 event
對象的許多屬性中,還有 event.target
,這代表我們的 HTML 表單與全部輸入一塊兒保存在那裏,來看看是否確實如此。
爲了獲取表單的值,經過檢查 event.target
,您將發現有一個名爲 elements
的屬性。 該屬性是表單中全部元素的集合。這個 elements
集合是一個有序列表,其中包含着表單的全部字段,例如 <input>
、<textarea>
、<button>
和 <fieldset>
。若是嘗試使用 console.log(event.target.elements)
進行打印,則會看到:
0: input#name
1: input#description
2: textarea#task
3: button
length: 4
description: input#description
name: input#name
tak: textarea#task
task: textarea#task
複製代碼
每一個表單字段在 elements
集合中的順序,與它們出如今標記中的順序相同,能夠按照位置和 name
特性來訪問它們。如今,我們有兩種方法獲取輸入的值:
經過相似數組的表示法: event.target.elements[0].value
經過 id: event.target.elements.some_id.value
實際上,若是如今但願在每一個表單元素上添加適當的id
屬性,則能夠訪問與event.target.elements.some_id
相同的元素,其中 id
是你分配給該屬性的字符串。 因爲 event.target.elements
首先是一個對象,因此還可使用 ES6 對象解構:
const { name, description, task } = event.target.elements;
複製代碼
這種作法不是 100%
推薦的,例如在 TypeScript 你會獲得一個錯誤,但只要寫 「vanilla JS」
就能夠了。如今有了這些值,我們就能夠完成 handleSubmit
了,在此過程當中,還建立了另外一個名爲 saveData
的方法。如今它只是將值打印到控制檯:
"use strict";
class Form {
constructor(formSelector) {
this.formSelector = formSelector;
this.init();
}
init() {
this.formSelector.addEventListener("submit", this.handleSubmit);
}
handleSubmit(event) {
event.preventDefault();
const { name, description, task } = event.target.elements;
this.saveData({
name: name.value,
description: description.value,
task: task.value
});
}
saveData(payload) {
console.log(payload);
}
}
const formSelector = document.forms[0];
new Form(formSelector);
複製代碼
這種保存數據的方式並非最好的判斷。 若是字段更改怎麼辦? 如今我們有了 name
,task
和 description
,但未來可能會添加更多輸入,因此須要動態提取這些字段。 固然,還要解決對象銷燬問題,來看看 event.target.elements
0: input#name
1: input#description
2: textarea#task
3: button
length: 4
description: input#description
name: input#name
tak: textarea#task
task: textarea#task
複製代碼
它看起來像一個數組。我們使用 map
方法將其轉換爲僅包含 name
,description
和task
(過濾按鈕類型 submit):
handleSubmit(event) {
event.preventDefault();
const inputList = event.target.elements.map(function(formInput) {
if (formInput.type !== "submit") {
return formInput.value;
}
});
/*
TODO this.saveData( maybe inputList ?)
*/
}
複製代碼
在瀏覽器中嘗試一下並查看控制檯:
Uncaught TypeError: event.target.elements.map is not a function
at HTMLFormElement.handleSubmit (form.js:15)
複製代碼
「 .map不是函數」
。 那麼 event.target.elements
究竟是什麼? 看起來像一個數組,但倒是另外一種野獸:它是 HTMLFormControlsCollection
。 在 第8章中,我們對這些內容有所瞭解,並看到一些 DOM 方法返回了 HTMLCollection
。
// Returns an HTMLCollection
document.chidren;
複製代碼
HTML 集合看起來相似於數組,可是它們缺乏諸如 map
或 filter
之類的用於迭代其元素的方法。 仍然可使用方括號表示法訪問每一個元素,咱們能夠經過 Array.from 將相似數組轉成真正的數組:
handleSubmit(event) {
event.preventDefault();
const arrOfElements = Array.from(event.target.elements);
const inputList = arrOfElements.map(function(formInput) {
if (formInput.type !== "submit") {
return formInput.value;
}
});
console.log(inputList);
/*
TODO this.saveData( maybe inputList ?)
*/
}
複製代碼
經過 Array.from
方法將 event.target.elements
構造一個數組。Array.from
接受一個映射函數做爲第二個參數,進一步優化:
handleSubmit(event) {
event.preventDefault();
const inputList = Array.from(event.target.elements, function(formInput) {
if (formInput.type !== "submit") {
return formInput.value;
}
});
console.log(inputList);
/*
TODO this.saveData( maybe inputList ?)
*/
}
複製代碼
刷新 form.html,填寫表單,而後按「提交」。 在控制檯中看到如下數組:
["Valentino", "Trip to Spoleto", "We're going to visit the city!", undefined]
複製代碼
最後,我想生成一個對象數組,其中每一個對象還具備相關表單輸入的name屬性:
handleSubmit(event) {
event.preventDefault();
const inputList = Array.from(event.target.elements, function(formInput) {
if (formInput.type !== "submit") {
return {
name: formInput.name,
value: formInput.value
};
}
});
console.log(inputList);
/*
TODO this.saveData( maybe inputList ?)
*/
}
複製代碼
再次刷新 form.html,填寫表單,將看到:
[
{
"name": "name",
"value": "Valentino"
},
{
"name": "description",
"value": "Trip to Spoleto"
},
{
"name": "task",
"value": "We're going to visit the city!"
},
undefined
]
複製代碼
good job,有一個 undefined
的空值,它來自 button
元素。 map
的默認行爲是在「空」值的狀況下返回 undefined
。 因爲咱們檢查了 if (formInput.type !== "submit")
,所以 button
元素未從 map
返回,而是被 undefined
取代。 咱們能夠稍後將其刪除,如今來看看 localStorage
。
我們有時候須要爲用戶保留一些數據,這樣作有不少緣由。 例如考慮一個筆記應用程序,用戶能夠在 HTML表單中插入新內容,而後再回來查看這些筆記。 下次她打開頁面時,將在其中找到全部內容。
在瀏覽器中保存數據有哪些選項? 持久化數據的一種重要方法是使用數據庫,但這裏咱們只有一些 HTML、JS 和瀏覽器。然而,在現代瀏覽器中有一個內置的工具,它就像一個很是簡單的數據庫,很是適合咱們的須要:localStorage
。localStorage
的行爲相似於 JS 對象,它有一堆方法:
setItem
用於保存數據
getItem
用於讀取數據
clear
用於刪除全部值
removeItem 用於清除對應的 key
的值
稍後咱們將看到 setItem
和 getItem
,首先我們先得有一個 form.html 文件,內容以下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>HTML forms and JavaScript</title>
</head>
<body>
<h1>What's next?</h1>
<form>
<label for="name">Name</label>
<input type="text" id="name" name="name" required minlength="5">
<label for="description">Short description</label>
<input type="text" id="description" name="description" required minlength="5">
<label for="task">Task</label>
<textarea id="task" name="task" required minlength="10"></textarea>
<button type="submit">Submit</button>
</form>
</body>
<script src="form.js"></script>
</html>
複製代碼
還有用於攔截提交事件的相關 JS 代碼:
"use strict";
class Form {
constructor(formSelector) {
this.formSelector = formSelector;
this.init();
}
init() {
this.formSelector.addEventListener("submit", this.handleSubmit);
}
handleSubmit(event) {
event.preventDefault();
const inputList = Array.from(event.target.elements, function(formInput) {
if (formInput.type !== "submit") {
return {
name: formInput.name,
value: formInput.value
};
}
});
console.log(inputList);
/*
TODO this.saveData( maybe inputList ?)
*/
}
saveData(payload) {
console.log(payload);
}
}
const formSelector = document.forms[0];
new Form(formSelector);
複製代碼
此時,我們須要實現 this.saveData
來將每一個筆記保存到 localStorage
。 這樣作時,須要保持儘量的通用。 換句話說,我不想用直接保存到 localStorage
的邏輯來填充this.saveData
。
相反,我們爲 Form
類提供一個外部依賴項(另外一個類),該類的做用是實現實際代碼。 未來咱們將這些筆記信息保存到 localStorage
仍是數據庫中都沒有關係。 對於每種用例,咱們應該可以爲 Form
提供不一樣的「存儲」,並隨着需求的變化而從一種轉換爲另外一種。 爲此,咱們首先調整構造函數以接受新的「存儲」參數:
class Form {
constructor(formSelector, storage) {
this.formSelector = formSelector;
this.storage = storage;
this.init();
}
init() {
this.formSelector.addEventListener("submit", this.handleSubmit);
}
handleSubmit(event) {
event.preventDefault();
const inputList = Array.from(event.target.elements, function(formInput) {
if (formInput.type !== "submit") {
return {
name: formInput.name,
value: formInput.value
};
}
});
}
saveData(payload) {
console.log(payload);
}
}
複製代碼
如今,隨着類的複雜度增長,須要驗證構造函數的參數。做爲一個用於處理 HTML 表單的類,我們至少須要檢查 formSelector
是不是 form 類型的 HTML 元素:
constructor(formSelector, storage) {
// Validating the arguments
if (!(formSelector instanceof HTMLFormElement))
throw Error(`Expected a form element got ${formSelector}`);
//
this.formSelector = formSelector;
this.storage = storage;
this.init();
}
複製代碼
若是 formSelector
不是一個表單類型的,就會報錯。另外還要驗證 storage
,由於咱們必須將用戶輸入存儲到某個地方。
constructor(formSelector, storage) {
// Validating the arguments
if (!(formSelector instanceof HTMLFormElement))
throw Error(`Expected a form element got ${formSelector}`);
// Validating the arguments
if (!storage) throw Error(`Expected a storage, got ${storage}`);
//
this.formSelector = formSelector;
this.storage = storage;
this.init();
}
複製代碼
存儲實現將是另外一個類。在咱們的例子中,能夠是相似於通用LocalStorage
的東西,在 form.js 中建立類 LocalStorage
:
class LocalStorage {
save() {
return "saveStuff";
}
get() {
return "getStuff";
}
}
複製代碼
如今,有了這個結構,咱們就能夠鏈接 Form
和 LocalStorage
:
Form
中的 saveData
應該調用 Storage
實現
LocalStorage.save
和 LocalStorage.get
能夠是靜態的
仍然在 form.js 中,以下更改類方法:
"use strict";
/*
Form implementation
*/
class Form {
constructor(formSelector, storage) {
// Validating the arguments
if (!(formSelector instanceof HTMLFormElement))
throw Error(`Expected a form element got ${formSelector}`);
// Validating the arguments
if (!(storage instanceof Storage))
throw Error(`Expected a storage, got ${storage}`);
//
this.formSelector = formSelector;
this.storage = storage;
this.init();
}
init() {
this.formSelector.addEventListener("submit", this.handleSubmit);
}
handleSubmit(event) {
event.preventDefault();
const inputList = Array.from(event.target.elements, function(formInput) {
if (formInput.type !== "submit") {
return {
name: formInput.name,
value: formInput.value
};
}
});
this.saveData('inputList', inputList);
}
saveData(key,payload) {
this.storage.save(key, payload);
}
}
/*
Storage implementation
*/
class LocalStorage {
static save(key, val) {
if (typeof val === 'object') {
val = JSON.stringify(val)
}
localStorage.setItem(key, val, redis.print)
}
static get(key) {
const val = localStorage.getItem(key)
if (val === null) return null
return JSON.parse(val)
}
}
const formSelector = document.forms[0];
const storage = LocalStorage;
new Form(formSelector, storage);
複製代碼
代碼部署後可能存在的BUG無法實時知道,過後爲了解決這些BUG,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具 Fundebug。
阿(a)裏(li)雲(yun)最近在作活動,低至2折,有興趣能夠看看:promotion.aliyun.com/ntms/yunpar…
乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。
由於篇幅的限制,今天的分享只到這裏。若是你們想了解更多的內容的話,能夠去掃一掃每篇文章最下面的二維碼,而後關注我們的微信公衆號,瞭解更多的資訊和有價值的內容。
每次整理文章,通常都到2點才睡覺,一週4次左右,挺苦的,還望支持,給點鼓勵