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

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

Arthur Evans

多くのデベロッパーは、ブラウザに組み込まれていないコントロールを提供したり、組み込みのフォーム コントロールで実現できないデザインをカスタマイズしたりするために、カスタムのフォーム コントロールを構築しています。

ただし、組み込みの HTML フォーム コントロールの機能を再現するのは難しい場合があります。フォームに <input> 要素を追加したときに自動的に取得される機能には、次のようなものがあります。

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

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

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

  • formdata イベントを使用すると、任意の JavaScript オブジェクトがフォームの送信に参加できるため、非表示の <input> を使用せずにフォームデータを追加できます。
  • フォームに関連付けられたカスタム要素 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 の例を使って試してみましょう。Chrome 77 以降で実行すると API の動作を確認できます。

ブラウザの互換性

対応ブラウザ

  • Chrome: 5. <ph type="x-smartling-placeholder">
  • Edge: 12。 <ph type="x-smartling-placeholder">
  • Firefox: 4. <ph type="x-smartling-placeholder">
  • Safari: 5. <ph type="x-smartling-placeholder">

ソース

フォームに関連付けられたカスタム要素

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

void formResetCallback()

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

void formStateRestoreCallback(state, mode)

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

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

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

フォームの状態の復元

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

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

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;
}

実際の例

次の例では、フォームに関連付けられたカスタム要素の多くの機能をまとめています。 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 より