更多表單控制項功能

透過新活動和自訂元素 API,參與表單變得更簡單了。

Arthur Evans

許多開發人員會建立自訂表單控制項,藉此提供瀏覽器未內建的控制項,或是自訂表單控制項比內建的表單控制項無法自訂的外觀和風格。

不過,複製內建 HTML 表單控制項的功能並不容易。將 <input> 元素新增至表單時,可以參考下列幾項功能:

  • 輸入內容會自動新增至表單的控制項清單。
  • 輸入內容的值會自動隨表單提交。
  • 輸入內容會進行表單驗證。您可以使用 :valid:invalid 虛擬類別為輸入項目設定樣式。
  • 系統會在表單重設、表單重新載入,或瀏覽器嘗試自動填入表單項目時,通知使用者輸入內容。

自訂表單控制項通常只提供少數上述功能。開發人員可以解決部分 JavaScript 限制,例如在表單提交後在表單中加入隱藏的 <input>。不過,其他功能無法單獨在 JavaScript 中複製。

兩項全新網頁版功能可讓您更輕鬆地建立自訂表單控制項,並移除現有自訂控制項的限制:

  • formdata 事件可讓任意 JavaScript 物件參與表單提交,因此您不必使用隱藏的 <input> 就能新增表單資料。
  • 與表單相關聯的自訂元素 API 可讓自訂元素看起來更像內建表單控制項,

這兩項功能有助於建立新的控制項,使其更臻完善。

以事件為基礎的 API

formdata 事件是一個低階 API,可讓任何 JavaScript 程式碼參與表單提交。機制的運作方式如下:

  1. 您要將 formdata 事件監聽器新增至要與之互動的表單。
  2. 當使用者按一下提交按鈕時,表單會觸發 formdata 事件,其中包含 FormData 物件,用於保存目前提交的所有資料。
  3. 每個 formdata 事件監聽器都有機會在提交表單前加入或修改資料。

以下是在 formdata 事件監聽器中傳送單一值的範例:

const form = document.querySelector('form');
// FormData event is sent on <form> submission, before transmission.
// The event has a formData property
form.addEventListener('formdata', ({formData}) => {
  // https://developer.mozilla.org/docs/Web/API/FormData
  formData.append('my-input', myInputValue);
});

請試試我們的 Glitch 範例。請務必在 Chrome 77 以上版本中執行這個指令碼,以便查看這個 API 的實際運作情形。

瀏覽器相容性

瀏覽器支援

  • Chrome:5.
  • Edge:12.
  • Firefox:4.
  • Safari:5.

資料來源

與表單相關聯的自訂元素

事件式 API 可與任何類型的元件搭配使用,但該 API 只能用來與提交程序互動。

除了提交內容外,標準化表單控制項也會參與表單生命週期的許多部分。表單相關自訂元素旨在彌平自訂小工具與內建控制項之間的隔閡。與表單相關聯的自訂元素與許多標準化表單元素的多項功能相符:

  • 將表單相關自訂元素放入 <form> 後,系統就會自動將該元素與表單建立關聯,例如瀏覽器提供的控制項。
  • 您可以使用 <label> 元素來為元素加上標籤。
  • 元素可以設定一個值,該值會自動與表單提交。
  • 元素可以設定旗標,指出其是否具備有效的輸入值。如果其中一個表單控制項的輸入內容無效,就無法提交表單。
  • 元素可針對表單生命週期的不同部分提供回呼,例如表單停用或重設為預設狀態。
  • 元素支援表單控制項的標準 CSS 虛擬類別,例如 :disabled:invalid

有許多功能!本文章僅說明部分主題,僅會說明整合自訂元素與表單時須具備的基本概念。

如需自訂元素的簡介,請參閱「Web 基礎知識」中的「自訂元素 v1:可重複使用的網頁元件」。

定義與表單相關聯的自訂元素

如要將自訂元素轉換為與表單相關聯的自訂元素,必須完成幾個額外步驟:

  • 將靜態的 formAssociated 屬性新增至自訂元素類別。這會指示瀏覽器將元素視為表單控制項。
  • 在元素上呼叫 attachInternals() 方法,即可存取表單控制項 (例如 setFormValue()setValidity()) 的額外方法和屬性。
  • 新增表單控制項支援的常見屬性和方法,例如 namevaluevalidity

這些項目如何融入基本自訂元素定義:

// Form-associated custom elements must be autonomous custom elements--
// meaning they must extend HTMLElement, not one of its subclasses.
class MyCounter extends HTMLElement {

  // Identify the element as a form-associated custom element
  static formAssociated = true;

  constructor() {
    super();
    // Get access to the internal form control APIs
    this.internals_ = this.attachInternals();
    // internal value for this control
    this.value_ = 0;
  }

  // Form controls usually expose a "value" property
  get value() { return this.value_; }
  set value(v) { this.value_ = v; }

  // The following properties and methods aren't strictly required,
  // but browser-level form controls provide them. Providing them helps
  // ensure consistency with browser-provided controls.
  get form() { return this.internals_.form; }
  get name() { return this.getAttribute('name'); }
  get type() { return this.localName; }
  get validity() {return this.internals_.validity; }
  get validationMessage() {return this.internals_.validationMessage; }
  get willValidate() {return this.internals_.willValidate; }

  checkValidity() { return this.internals_.checkValidity(); }
  reportValidity() {return this.internals_.reportValidity(); }

  
}
customElements.define('my-counter', MyCounter);

註冊完成後,無論您要使用瀏覽器提供的表單控制項,都可以使用這個元素:

<form>
  <label>Number of bunnies: <my-counter></my-counter></label>
  <button type="submit">Submit</button>
</form>

設定值

attachInternals() 方法會傳回 ElementInternals 物件,提供表單控制項 API 的存取權。其中最基本的 setFormValue() 方法可設定控制項目前的值。

setFormValue() 方法可採用以下三種類型的值之一:

  • 字串值。
  • File 物件。
  • FormData 物件。您可以使用 FormData 物件傳送多個值。舉例來說,信用卡輸入控制項可能會傳遞卡號、到期日和驗證碼。

如要設定簡易值:

this.internals_.setFormValue(this.value_);

如要設定多個值,可以採用如下方式:

// Use the control's name as the base name for submitted data
const n = this.getAttribute('name');
const entries = new FormData();
entries.append(n + '-first-name', this.firstName_);
entries.append(n + '-last-name', this.lastName_);
this.internals_.setFormValue(entries);
敬上

輸入驗證

您的控制項也可以呼叫 setValidity(),參與表單驗證 方法。

// Assume this is called whenever the internal value is updated
onUpdateValue() {
  if (!this.matches(':disabled') && this.hasAttribute('required') &&
      this.value_ < 0) {
    this.internals_.setValidity({customError: true}, 'Value cannot be negative.');
  }
  else {
    this.internals_.setValidity({});
  }
  this.internals.setFormValue(this.value_);
}

您可以使用 :valid:invalid 設定與表單關聯的自訂元素樣式 虛擬類別,就像內建表單控制項一樣。

生命週期回呼

與表單相關聯的自訂元素 API 包含一組額外的生命週期回呼,要連結至表單生命週期。回呼是選用項目:只有在元素需要在生命週期的該點執行特定作業時,才會實作回呼。

void formAssociatedCallback(form)

當瀏覽器將元素與表單元素建立關聯,或取消與表單元素間的關聯時,會呼叫此方法。

void formDisabledCallback(disabled)

在元素的 disabled 狀態變更後呼叫 (原因是新增或移除此元素的 disabled 屬性)。或因為 disabled 狀態變更在是此元素的祖系 <fieldset> 上。disabled 參數代表元素的新停用狀態。舉例來說,當元素停用時,該元素可停用其 shadow DOM 中的元素。

void formResetCallback()

表單重設後呼叫。元素應自行重設為某種預設狀態。如果是 <input> 元素,通常必須設定 value 屬性,使其與標記中設定的 value 屬性相符;如果是核取方塊,請將 checked 屬性設為與 checked 屬性相符。

void formStateRestoreCallback(state, mode)

曾在下列任一情況下呼叫:

  • 當瀏覽器還原元素狀態時 (例如導覽後或瀏覽器重新啟動時)。在這個範例中,mode 引數為 "restore"
  • 當瀏覽器的輸入輔助功能 (例如表單自動填入) 設定某個值時。在這個範例中,mode 引數為 "autocomplete"

第一個引數的類型取決於呼叫 setFormValue() 方法的方式。詳情請參閱「還原表單狀態」。

正在還原表單狀態

在某些情況下 (例如回到某個頁面或重新啟動瀏覽器時),瀏覽器可能會嘗試將表單還原成使用者離開時離開的狀態。

如果是與表單相關聯的自訂元素,則還原的狀態取自您傳遞至 setFormValue() 方法的值。您可以使用單一值參數 (如前述範例所示) 呼叫方法,也可以使用兩個參數來呼叫方法:

this.internals_.setFormValue(value, state);

value 代表控制項的提交資料表值。選用的 state 參數是控制項狀態的「內部」表示法,其中可能包括未傳送至伺服器的資料。state 參數採用的類型與 value 參數相同,可以是字串、FileFormData 物件。

如果無法只根據值還原控制項的狀態,state 參數就非常實用。舉例來說,假設您建立了多種模式的顏色挑選器:調色盤或 RGB 色輪。提交的表格是標準格式中選取的顏色,例如 "#7fff00"。但是,如要將控制項還原到特定狀態,您也需要知道狀態處於哪個模式,因此狀態看起來可能會是 "palette/#7fff00"

this.internals_.setFormValue(this.value_,
    this.mode_ + '/' + this.value_);

程式碼必須根據儲存的狀態值還原狀態。

formStateRestoreCallback(state, mode) {
  if (mode == 'restore') {
    // expects a state parameter in the form 'controlMode/value'
    [controlMode, value] = state.split('/');
    this.mode_ = controlMode;
    this.value_ = value;
  }
  // Chrome currently doesn't handle autofill for form-associated
  // custom elements. In the autofill case, you might need to handle
  // a raw value.
}

若是較簡單的控制項 (例如數字輸入),值可能就足以將控制項還原至先前的狀態。如果您在呼叫 setFormValue() 時省略 state,該值會傳遞至 formStateRestoreCallback()

formStateRestoreCallback(state, mode) {
  // Simple case, restore the saved value
  this.value_ = state;
}

實際範例

以下範例將許多表單關聯自訂元素的功能整合起來。 請務必在 Chrome 77 以上版本中執行這個指令碼,以便查看這個 API 的實際運作情形。

特徵偵測

您可以利用功能偵測功能,判斷是否有 formdata 事件和表單相關的自訂元素可用。目前任一功能都沒有釋出 polyfill。在這兩種情況下,您都可以改回新增隱藏的表單元素,將控制項值套用到表單。與表單關聯的自訂元素中,許多較先進的功能,可能難以或無法執行 polyfill。

if ('FormDataEvent' in window) {
  // formdata event is supported
}

if ('ElementInternals' in window &&
    'setFormValue' in window.ElementInternals.prototype) {
  // Form-associated custom elements are supported
}

結論

formdata 事件和表單相關聯的自訂元素提供用於建立自訂表單控制項的新工具。

formdata 事件不會提供任何新功能,但提供介面,讓您不必建立隱藏的 <input> 元素,即可將表單資料加入提交程序。

與表單相關聯的自訂元素 API 提供一組新功能,讓自訂表單控制項的運作方式與內建表單控制項相同。

主頁橫幅由 Oudom Pravat 提供 Unsplash 網站上。