【JS 口袋書】第 11 章:HTML 表單及 localStorage 的使用

前端小智發表於2019-11-01

作者:valentinogagliardi

譯者:前端小智

來源:github


為了保證的可讀性,本文采用意譯而非直譯。

重新介紹 HTML 表單

網頁不僅僅是用來顯示資料的。有了 HTML 表單,我們們可以收集和操作使用者資料。在本章中,通過構建一個簡單的 HTML 表單來學習表單的相關的知識。

在這個過程中,會了解更多關於 DOM 事件的資訊,從在 第8章 我們知道了一個 <form> 元素是一個 HTML 元素,它可能包含其他的子元素,比如:

  • <input> 用於捕獲資料
  • <textarea> 用於捕獲文字
  • <button> 用於提交表單

在本章中,我們們構建一個包含 <input><textarea><button> 的表彰。理想情況下,每個 input 都應該具有 type 的屬性,該屬性指示輸入型別: 例如 textemailnumberdate 等。除了 type 屬性之外,可能還希望向每個表單元素新增 id 屬性。

inputtextarea 也可以有一個 name 屬性。如果你們想在不使用 JS 的情況下傳送表單,name 屬性非常重要。稍後會詳細介紹。

另外,將每個表單元素與 <label> 關聯也是一種常見的方式。在下面的示例中,會看到每個 labelfor 屬性繫結對應 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> 上有很多可用的屬性,我發現 minlengthmaxlength 是最有用的兩個。在實戰中,它們可以阻止懶惰的垃圾郵件傳送者傳送帶有 “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”,而像 HTMLElementEventTarget 這樣的實體是函式:

console.log(typeof EventTarget); // "function"
複製程式碼

因此,如果任何 HTML 元素都連線到 EventTarget,這意味著 <form>EventTarget 的“例項”,如下:

const aForm = document.createElement("form");
console.log(aForm instanceof EventTarget); // true
複製程式碼

formEventTarget 的一種專門化型別。每個EventTarget 都可以接收和響應 DOM 事件(如第8章所示)。

DOM 事件有很多型別,比如 clickblurchange 等等。現在,我們們感興趣的是 HTML 表單特有的 submit 事件。當使用者單擊 inputtype 為 “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的 情況下傳送表單的方式,因為它是基於 DjangoRailsfriends 等 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);
複製程式碼

這種儲存資料的方式並不是最好的判斷。 如果欄位更改怎麼辦? 現在我們們有了 nametaskdescription,但將來可能會新增更多輸入,所以需要動態提取這些欄位。 當然,還要解決物件銷燬問題,來看看 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 方法將其轉換為僅包含 namedescriptiontask (過濾按鈕型別 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 集合看起來類似於陣列,但是它們缺少諸如 mapfilter 之類的用於迭代其元素的方法。 仍然可以使用方括號表示法訪問每個元素,我們可以通過 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 和瀏覽器。然而,在現代瀏覽器中有一個內建的工具,它就像一個非常簡單的資料庫,非常適合我們的需要:localStoragelocalStorage 的行為類似於 JS 物件,它有一堆方法:

  • setItem 用於儲存資料

  • getItem 用於讀取資料

  • clear 用於刪除所有值

  • removeItem 用於清除對應的 key 的值

稍後我們將看到 setItemgetItem,首先我們們先得有一個 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";
  }
}
複製程式碼

現在,有了這個結構,我們就可以連線 FormLocalStorage

  • Form 中的 saveData 應該呼叫 Storage 實現

  • LocalStorage.saveLocalStorage.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

原文:github.com/valentinoga…

交流

乾貨系列文章彙總如下,覺得不錯點個Star,歡迎 加群 互相學習。

github.com/qq449245884…

因為篇幅的限制,今天的分享只到這裡。如果大家想了解更多的內容的話,可以去掃一掃每篇文章最下面的二維碼,然後關注我們們的微信公眾號,瞭解更多的資訊和有價值的內容。

clipboard.png

每次整理文章,一般都到2點才睡覺,一週4次左右,挺苦的,還望支援,給點鼓勵

【JS 口袋書】第 11 章:HTML 表單及 localStorage 的使用

相關文章