更多表單控制項功能

有了新事件和自訂元素 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 與任何類型的元件搭配使用,但只能用於與提交程序互動。

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

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

這可說是眾多功能!本文不會介紹所有這些元素,但會說明將自訂元素與表單整合所需的基本知識。

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

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

  • 在自訂元素類別中新增靜態 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 屬性,或是 <fieldset>disabled 狀態變更,而該元素是此元素的祖系。disabled 參數代表元素的新停用狀態。舉例來說,元素可能會在停用時停用其陰影 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