🐵

yubinbango.jsで自動入力が行われたら別の処理を実行する(Object.defineProperty()を使ってセッターを上書き)

2024/09/11に公開

課題の確認

yubinbango.js は郵便番号を入力すると対応する住所のinputのテキストボックスに自動入力を行ってくれる、大変便利なライブラリです。

最近このライブラリを利用したのですが、要件としては、自動入力のみならず「自動入力が行われたら、それをトリガーに別の処理を行う」というものでした。

要件を満たす処理を行う上での課題としては、以下のようなものでした。

  • addEventListenerのinputイベントでは自動挿入を検知してくれない
  • MutationObserverでも自動挿入を検知してくれない

調べるとこの課題に関する記事が見つかりました。
Qiita - HTMLInputElement.value を監視する

今回の課題に対する対応策としては、この記事にあるように、Object.definePropertyを使ってセッターをカスタマイズする、ということなのですが、もう少しゆっくりリファレンス等を確認しながら背景知識にふれ、最終的な実装に至ろうという記事です。

最初に、自動挿入の対象となる<input>のvalue属性について確認しておきたいと思います。

<input> のvalue属性

MDN - <input>: 入力欄(フォーム入力)要素 - 属性

入力コントロールの値です。HTML の中で指定されると、これは初期値となり、その後で JavaScript を使用してそれぞれの HTMLInputElement オブジェクトの value プロパティにアクセスすることで、いつでも変更したり受け取ったりすることができます。

今回の文脈で重要なところを抜き出すと、

  • HTMLで指定されるvalue「属性」は初期値であること
  • その後、Javascriptで取得・変更する際は、HTMLInputElementオブジェクトのvalueプロパティを使用する

いったん、<input>valueについておさえたことにして、次に、yubinbango.jsの自動挿入が具体的にはコードのどの部分で行われているか確認してみます。

自動挿入に対応するyubinganbo.jsのコードを確認する

yubinbango.js の以下の部分が対応します。
(確認のためには、コードを挿入するので、動作を自分で確かめたい方はローカルにDLしてやってみてください)

t.prototype.D = function (t, n, e) {
  var r = { 'p-region-id': e.k, 'p-region': e.region, 'p-locality': e.l, 'p-street-address': e.m, 'p-extended-address': e.o },
    o = n.querySelectorAll('.' + t);
  [].map.call(o, function (n) {
    return (n.value += r[t] ? r[t] : '');
  });
};

ピンポイントでいえば、n.value += の部分でinputのvalueに挿入をしています。
今回の課題はこの自動挿入処理をトリガーに別の処理をする、ということになります。

次へ進む前に、このnについて確認しておきます。
(上記によれば、HTMLInputElementオブジェクトのはずですが、一応確認します)

試しに、return処理の前に、console.log(typeof n)するとobjectと表示されますがそれだけだとよく分かりません。

typeofのリファレンスの下部により詳細なタイプを取得するためのコードがあるので、そこのtype関数を使ってconsole.log(type(n));すると、上記の<input>の「属性」のところに出てきたHTMLInputElementと表示されます。(正確にいえばこのオブジェクトを生成したコンストラクタ関数の名前なのですがここでは深入りしません)

MDN - typeof - Custom method that gets a more specific type

ここまで、学んできた事をもとに、最初に提示した「自動入力が行われたら、それをトリガーに別の処理を行う」という課題を、コードに落とせるような形で言い換えると、以下のようになります。

「自動挿入によって発生するHTMLInputElemtnオブジェクトのvalueプロパティの更新をトリガーとして、別の処理を行う」

このように課題を定義しなおすと、まずMutationObserverは利用できないことが分かります。
MutationObserverに関する詳細な説明は割愛しますが、MutationObserverは「属性」の変更は検知できますが、「プロパティ」の変更を検知するものではないからです。
MDN - MutationObserver

では、どのようにこの課題にアプローチするかというと、Object.defineProperty()というメソッドを使うことになります。

Object.definePropertyのゲッターとセッター

Object.definePropertyのリファレンスには、いろいろと書いてありますが、今回の課題は、値が更新されたタイミングでの処理をカスタマイズしたいという課題なので、以下の部分が対象となります。

MDN - Object.defineProperty() - 独自のゲッターとセッター

この項目のコードを見るだけは分かりづらいかもしれませんが、
要するに、オブジェクトに対して、プロパティの値を取得した(getした)時、値を変更した(setした)時、どのような処理をするのかを自分で定義することが出来るということです。

セッターの動作を確認するための簡単なコードを書いてみます。

以下はボタンをクリックすると、valueプロパティに「こんにちは」を設定し、その設定をトリガーに「こんにちは が挿入されました」とalert表示されるだけのものです。
(テキストボックスに何も表示されませんが、これについては後で説明します)

では、最後に今回の課題を解決するコードを書いてみます。

課題を解決するコード

まず、うまく行きそうでうまく行かないコードを書いてみます。

動かしてみると分かると思いますが、正しい郵便番号を入れると、set部分に定義された処理が行われ、console.log('newValue: ', newValue);によってコンソールに、都道府県名、市町村区、町域が、表示されます。

しかし、画面のinputのテキストボックスには何も反映されません。
どうやら、独自でsetterを定義したことで、デフォルトで提供されているHTMLInputElementvalueプロパティの挙動(テキストボックスへの反映)が上書きされて失われてしまったようです。

では、元々HTMLInputElementが持っていた機能を維持しつつ、追加の処理をしたい場合は、どうしたら良いでしょうか。
MDN - Object.getOwnPropertyDescriptor()とメソッドを使います。

Object.getOwnPropertyDescriptor() 静的メソッドは、与えられたオブジェクトの特定のプロパティ (すなわち、あるオブジェクトの直接の表現であり、オブジェクトのプロトタイプチェーン内のものではない) の構成を記述したオブジェクトを返します。

これを読んだだけではよく分からないと思いますが、今回で言えば、HTMLInputElementオブジェクトのvalueプロパティが持つ、ゲッターやセッターの機能を取得できるということです。
(このあたりのことを詳しく説明し出すと本筋からズレてどんどん長くなりそうなので、別の機会にします)

以下のようにします。
const originalInputDescriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');

あとは、この元々の機能を利用しつつ、そこに、自分が実行したい処理(ここではconsole.logしているだけですが)をset部分に書くだけです。
(yubinbango.jsは、適切な郵便番号になるまではkeyupのたびに空文字列を挿入してくるので空文字列の時は処理されないようにしています)

以上となります。

##その他参考記事参考
stackoverflow - Detect input value change with MutationObserver

Discussion