作者:valentinogagliardi
譯者:前端小智
來源:github
為了保證的可讀性,本文采用意譯而非直譯。
重新介紹 HTML 表單
網頁不僅僅是用來顯示資料的。有了 HTML 表單,我們們可以收集和操作使用者資料。在本章中,通過構建一個簡單的 HTML 表單來學習表單的相關的知識。
在這個過程中,會了解更多關於 DOM 事件的資訊,從在 第8章
我們知道了一個 <form>
元素是一個 HTML 元素,它可能包含其他的子元素,比如:
<input>
用於捕獲資料<textarea>
用於捕獲文字<button>
用於提交表單
在本章中,我們們構建一個包含 <input>
、<textarea>
和 <button>
的表彰。理想情況下,每個 input
都應該具有 type
的屬性,該屬性指示輸入型別: 例如 text
、email
、number
、date
等。除了 type
屬性之外,可能還希望向每個表單元素新增 id
屬性。
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'/>
複製程式碼
只要表單存在上面 列出的任何一種按鈕,那麼在相應表單控制元件擁有焦點的情況下,按Enter鍵就可以提交表單。(textarea 是一個例外,在文字中回車會換行。)如果表單裡沒有提交按鈕,按Enter鍵不會提交表單。
我們們的目標是獲取表單上的所有使用者輸入,所以,需要監聽 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 表單與所有輸入一起儲存在那裡,來看看是否確實如此。
從 from 提取資料
為了獲取表單的值,通過檢查 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
。
瞭解 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。
交流
乾貨系列文章彙總如下,覺得不錯點個Star,歡迎 加群 互相學習。
因為篇幅的限制,今天的分享只到這裡。如果大家想了解更多的內容的話,可以去掃一掃每篇文章最下面的二維碼,然後關注我們們的微信公眾號,瞭解更多的資訊和有價值的內容。
每次整理文章,一般都到2點才睡覺,一週4次左右,挺苦的,還望支援,給點鼓勵