更多表單控制項功能

有了新事件和自訂元素 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.
  • 邊緣:12。
  • Firefox:4.
  • Safari:5.

資料來源

與表單相關聯的自訂元素

事件式 API 可與任何類型的元件搭配使用,但該 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> 為此元素的祖系變更。disableddisabled 參數代表元素的新停用狀態。舉例來說,元素可能會在停用時停用其陰影 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 網站上。