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

做者:valentinogagliardijavascript

譯者:前端小智html

來源:github前端


阿(a)裏(li)雲(yun)今年比去年便宜,10.24~11.11購買是1年86元,3年229元,能夠點擊這裏進行參與:java


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

從新介紹 HTML 表單

網頁不只僅是用來顯示數據的。有了 HTML 表單,我們能夠收集和操做用戶數據。在本章中,經過構建一個簡單的 HTML 表單來學習表單的相關的知識。github

在這個過程當中,會了解更多關於 DOM 事件的信息,從在 第8章 咱們知道了一個 <form> 元素是一個 HTML 元素,它可能包含其餘的子元素,好比:web

  • <input> 用於捕獲數據
  • <textarea> 用於捕獲文本
  • <button> 用於提交表單

在本章中,我們構建一個包含 <input><textarea><button> 的表彰。理想狀況下,每一個 input 都應該具備 type 的屬性,該屬性指示輸入類型: 例如 textemailnumberdate 等。除了 type 屬性以外,可能還但願向每一個表單元素添加 id 屬性。redis

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'/>
複製代碼

只要表單存在上面 列出的任何一種按鈕,那麼在相應表單控件擁有焦點的狀況下,按回車鍵就能夠提交表單。(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的 狀況下發送表單的方式,由於它是基於 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…

交流

阿(a)裏(li)雲(yun)最近在作活動,低至2折,有興趣能夠看看:promotion.aliyun.com/ntms/yunpar…

乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。

github.com/qq449245884…

由於篇幅的限制,今天的分享只到這裏。若是你們想了解更多的內容的話,能夠去掃一掃每篇文章最下面的二維碼,而後關注我們的微信公衆號,瞭解更多的資訊和有價值的內容。

clipboard.png

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

相關文章
相關標籤/搜索