より多機能なフォーム コントロール

新しいイベントとカスタム要素 API により、フォームへの参加がはるかに簡単になりました。

Arthur Evans

多くのデベロッパーは、ブラウザに組み込まれていないコントロールを提供したり、組み込みのフォーム コントロールでは不可能なルック アンド フィールをカスタマイズしたりするために、カスタム フォーム コントロールを作成します。

ただし、組み込みの HTML フォーム コントロールの機能を複製するのは難しい場合があります。フォームに追加すると <input> 要素に自動的に付与される機能の一部を以下に示します。

  • 入力は、フォームのコントロールのリストに自動的に追加されます。
  • 入力の値はフォームと一緒に自動的に送信されます。
  • 入力はフォームの検証で処理されます。入力のスタイル設定には、:valid:invalid の疑似クラスを使用します。
  • 入力は、フォームがリセットされたとき、フォームが再読み込みされたとき、またはブラウザがフォームの入力内容を自動入力しようとしたときに通知されます。

カスタム フォーム コントロールには、通常、これらの機能の一部しかありません。デベロッパーは、JavaScript の制限の一部を回避できます。たとえば、フォームに非表示の <input> を追加してフォーム送信に参加させるなどです。ただし、JavaScript だけでは再現できない機能もあります。

2 つの新しいウェブ機能により、カスタム フォーム コントロールを簡単に作成し、現在のカスタム コントロールの制限を解除できます。

  • formdata イベントを使用すると、任意の JavaScript オブジェクトがフォームの送信に参加できるため、非表示の <input> を使用せずにフォームデータを追加できます。
  • Form-associated custom elements API を使用すると、カスタム要素を組み込みフォーム コントロールのように動作させることができます。

これらの 2 つの機能を使用すると、より効果的な新しい種類のコントロールを作成できます。

イベントベースの API

formdata イベントは、任意の JavaScript コードがフォーム送信に参加できるようにする低レベルの API です。このメカニズムは次のように機能します。

  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 以降で実行してください。

ブラウザの互換性

対応ブラウザ

  • 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() メソッドは、フォーム コントロール API へのアクセスを提供する ElementInternals オブジェクトを返します。最も基本的なメソッドは setFormValue() メソッドで、コントロールの現在の値を設定します。

setFormValue() メソッドは、次の 3 種類の値のいずれかを受け取ることができます。

  • 文字列値
  • 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 属性が追加または削除されたか、この要素の祖先である <fieldset>disabled 状態が変更されたために、要素の disabled 状態が変更された後に呼び出されます。disabled パラメータは、要素の新しい無効状態を表します。たとえば、要素が無効になっているときに、その Shadow DOM 内の要素を無効にできます。

void formResetCallback()

フォームがリセットされた後に呼び出されます。要素は、なんらかのデフォルト状態にリセットされる必要があります。<input> 要素の場合、通常は、マークアップで設定された value 属性と一致するように value プロパティを設定します(チェックボックスの場合は、checked 属性と一致するように checked プロパティを設定します)。

void formStateRestoreCallback(state, mode)

次のいずれかの状況で呼び出されます。

  • ブラウザが要素の状態を復元するとき(ナビゲーション後やブラウザの再起動時など)。この場合、mode 引数は "restore" です。
  • フォームの自動入力などのブラウザの入力支援機能によって値が設定された場合。この場合、mode 引数は "autocomplete" です。

最初の引数の型は、setFormValue() メソッドの呼び出し方法によって異なります。詳しくは、フォームの状態を復元するをご覧ください。

フォームの状態の復元

ページに戻ったときやブラウザを再起動したときなど、状況によっては、ブラウザがフォームをユーザーが離れたときの状態に復元しようとすることがあります。

フォームに関連付けられたカスタム要素の場合、復元された状態は、setFormValue() メソッドに渡す値から取得されます。前述の例のように、単一の値パラメータでメソッドを呼び出すことも、2 つのパラメータで呼び出すこともできます。

this.internals_.setFormValue(value, state);

value は、コントロールの送信可能な値を表します。省略可能な state パラメータは、コントロールの状態の内部表現です。これには、サーバーに送信されないデータが含まれる場合があります。state パラメータは、value パラメータと同じ型を受け取ります。文字列、File オブジェクト、FormData オブジェクトのいずれかです。

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 イベントとフォームに関連付けられたカスタム要素が使用可能かどうかを判断できます。現在のところ、どちらの機能にもポリフィルはリリースされていません。どちらの場合も、非表示のフォーム要素を追加して、コントロールの値をフォームに反映できます。フォームに関連付けられたカスタム要素の高度な機能の多くは、ポリフィル化が困難または不可能な可能性があります。

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 より