更多表單控制項功能

新增事件和自訂元素 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 範例試試看。如要查看 API 的實際運作情形,請務必透過 Chrome 77 以上版本執行。

瀏覽器相容性

瀏覽器支援

  • 5
  • 12
  • 4
  • 5

資料來源

與表單相關聯的自訂元素

以事件為基礎的 API 可與任何類型的元件搭配使用,但只能讓您與提交程序互動。

除了提交內容之外,標準化表單控制項會參與表單生命週期的許多部分。與表單相關的自訂元素有助於消除自訂小工具和內建控制項之間的差距。表單相關自訂元素與標準化表單元素的多項功能相符:

  • 當您在 <form> 內置入與表單相關聯的自訂元素時,該元素會自動與表單建立關聯,例如瀏覽器提供的控制項。
  • 您可以使用 <label> 元素為元素加上標籤。
  • 元素可以設定會自動隨表單提交的值。
  • 元素可以設定標記,確認是否具備有效的輸入內容。如果其中一個表單控制項的輸入內容無效,就無法提交表單。
  • 元素可以為表單生命週期的各個部分提供回呼,例如表單停用或重設為預設狀態。
  • 該元素支援表單控制項 (例如 :disabled:invalid) 的標準 CSS 虛擬類別。

新功能非常豐富!本文不會介紹所有選項,但會說明整合自訂元素與表單所需的基本知識。

定義表單關聯的自訂元素

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

  • 在自訂元素類別中新增靜態 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 參數代表元素的新停用狀態。例如, 元素在停用時,可能會停用其 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 色輪。提交的 value 是標準格式 (例如 "#7fff00") 中的所選顏色。不過,如要將控制項還原到特定狀態,您也需要知道控制項處於哪個模式,因此state看起來可能會像 "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;
}

實際範例

以下範例彙整表單相關自訂元素的多項功能。如要查看 API 的實際運作情形,請務必透過 Chrome 77 以上版本執行。

功能偵測

您可以使用功能偵測功能,判斷是否可使用 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 上。