更多表單控制項功能

Arthur Evans

發布日期:2019 年 8 月 8 日

許多開發者會建立自訂表單控件,要么是為了提供瀏覽器中沒有內建的控件,要么是為了自訂外觀和感覺,使其超出內建表單控件的功能範圍。

不過,要複製內建 HTML 表單控制項的功能可能很困難。將 <input> 元素新增至表單時,系統會自動加入下列功能:

  • 輸入內容會自動加入表單的控制項清單。
  • 系統會自動將輸入內容的值連同表單一併提交。
  • 輸入參與 表單驗證。您可以使用 :valid:invalid 偽類來設定輸入框的樣式。
  • 當表單重設、表單重新載入或瀏覽器嘗試自動填入表單項目時,輸入框會收到通知。

自訂表單控制項通常很少具備這些功能。開發者可以透過一些方法來規避 JavaScript 的一些限制,例如在表單中新增隱藏的 <input> 來參與表單提交。但有些功能僅靠 JavaScript 是無法實現的。

兩項 Web 功能使建立自訂表單控制項變得更加容易,並消除了自訂控制項的限制:

  • formdata 事件允許任意 JavaScript 物件參與表單提交,因此您可以新增表單資料而無需使用隱藏的 <input>
  • 表單關聯自訂元素 API 可讓自訂元素更像內建表單控制項。

這兩個特性可以用來創造更好用的新型控制。

基於事件的 API

Browser Support

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

Source

formdata 事件是一個底層 API,允許任何 JavaScript 程式碼參與表單提交。

  • 在要互動的表單中新增一個 formdata 事件監聽器。
  • 當使用者點擊提交按鈕時,表單會觸發一個 formdata 事件,其中包含一個 FormData 對象,該物件保存所有要提交的資料。
  • 每個 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);
});

與表單相關聯的自訂元素

你可以將基於事件的 API 與任何類型的元件一起使用,但它只允許你與提交過程進行互動。

標準化表單控制項參與表單生命週期的許多環節。 表單關聯的 自訂元素 旨在彌合自訂小部件和內建控制項之間的差距。表單相關的自訂元素與標準化表單元素的許多功能相符:

  • 當您將與表單關聯的自訂元素放置在 <form> 中時,它會自動與表單關聯,就像瀏覽器提供的控制項一樣。
  • 可以使用 <label> 元素來標記該元素。
  • 該元素可以設定一個值,該值將隨表單自動提交。
  • 元素可以設定旗標,指出是否具有有效輸入內容。如果其中一個表單控制項的輸入內容無效,就無法提交表單。
  • 這個元素可以為表單生命週期的各個部分提供回呼,例如表單停用或重設為預設狀態時。
  • 此元素支援表單控制項的標準 CSS 偽類,例如 :disabled:invalid

本文檔並未涵蓋所有內容,但確實描述了將自訂元素與表單整合所需的基本知識。

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

將自訂元素轉換為表單關聯的自訂元素需要一些額外的步驟:

  • 在自訂元素類別中新增靜態的 formAssociated 屬性。這會告訴瀏覽器將該元素視為表單控制項。
  • 呼叫元素的 attachInternals() 方法,以存取表單控制項的額外方法和屬性,如 setFormValue()setValidity()
  • 新增表單控制項支援的通用屬性和方法,如 namevaluevalidity

以下是這些元素如何融入基本自訂元素定義:

// Form-associated custom elements must be autonomous custom elements.
// 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>這是該元素的祖先元素。

例如,當該元素被停用時,它可能會停用其影子 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"。要將控制項還原到特定狀態,您還需要知道它處於哪種模式,因此 狀態 可能看起來像 "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 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;
}

特徵檢測

您可以使用特徵偵測來確定 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 事件提供介面,可將表單資料新增至提交程序,不必建立隱藏的 <input> 元素。透過表單關聯的自訂元素 API,您可以為自訂表單控制項提供一組新的功能,這些功能與內建表單控制項類似。