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

新しいイベントとカスタム要素 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 での例を使って試してみましょう。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() メソッドは、フォーム コントロール 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)

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

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

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

フォームの状態を復元しています

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

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

this.internals_.setFormValue(value, state);

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

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

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