javascriptでライブラリ無しでオートコンプリート

javascriptを使ってイチからオートコンプリートを実装しました。

オートコンプリートは何もなければライブラリを使って実装するのが早いですが、 イチから自分で実装する機会があったのでメモを残します。

要件

  • PC(Chrome, IE, Safari), Android(Chrome), iPhone(safari)に対応する。
  • 入力したキーワードを含む都道府県を選択肢として表示する。
  • 選択肢をクリックしたら選択したこととする。
  • PCでは選択肢からカーソルキー&エンターキーでも選択できるようにする。

この4つ目が肝です。

対応方法

こちらに動くものを用意してあります。
デモ

HTMLは以下のようになっているとします。
テキストボックスに文字を入力するとulタグ内に選択肢が表示されるようにします。

<input type="text" id="keyword" placeholder="都道府県名を入れてください" autocomplete="off"/>
<ul id="outputBox"></ul>

javascriptは以下の通りです。

// 都道府県一覧
const prefectureList = ['北海道','青森県','岩手県','宮城県','秋田県','山形県','福島県',
              '茨城県','栃木県','群馬県','埼玉県','千葉県','東京都','神奈川県',
              '新潟県','富山県','石川県','福井県','山梨県','長野県','岐阜県',
              '静岡県','愛知県','三重県','滋賀県','京都府','大阪府','兵庫県',
              '奈良県','和歌山県','鳥取県','島根県','岡山県','広島県','山口県',
              '徳島県','香川県','愛媛県','高知県','福岡県','佐賀県','長崎県',
              '熊本県','大分県','宮崎県','鹿児島県','沖縄県'];

// in/out要素を変数に入れておく
const inputElm = document.getElementById('keyword');
const outboxElm = document.getElementById('outputBox');

/**
 * テキストボックスに文字入力時
 */
inputElm.addEventListener('compositionend', function(e) {
    // タブキーは次の要素に移動させるのでスルー
    if (e.key == 'Tab') {
        return;
    }

    outboxElm.innerHTML = '';
    let _this = this;
    setTimeout(function(){
        // 入力された文字を含む都道府県を抽出する
        prefectureList.forEach(function(prefecture){
            if (prefecture.indexOf(_this.value) != -1) {
                let liElm = document.createElement('li');
                liElm.setAttribute('tabindex', '0');

                // 表示する選択肢にクリックイベントを設定する
                liElm.onclick = function() {
                    // 選択肢をクリックしたらテキストボックスに反映する
                    inputElm.value = this.innerHTML;
                    outboxElm.innerHTML = '';
                }

                // キーワードを含む選択肢をulに追加する
                let txtNode = document.createTextNode(prefecture);
                liElm.appendChild(txtNode);
                outboxElm.appendChild(liElm);
            }
        });
    }, 10);
});

/**
 * テキストボックスにフォーカスが当たっている時のキー操作
 */
inputElm.addEventListener('keydown', function(e) {
    // 下キーを入力されたら選択肢の一番上にフォーカスを当てる
    if (e.key == 'ArrowDown' || e.key == 'Down') {
        let firstElm = outboxElm.getElementsByTagName('li')[0];
        if (firstElm) {
            firstElm.focus();
        }
        e.preventDefault();
    }
});

/**
 * 選択肢にフォーカスが当たっている時のキー操作 or Enter
 */
outboxElm.addEventListener('keydown', function(e) {
    var currentElm = document.activeElement;

    // 下キーを入力されたら次の選択肢にフォーカスを当てる
    // ブラウザによりArrowDownだったりDownだったりする
    if (e.key == 'ArrowDown' || e.key == 'Down') {
        if (currentElm.nextElementSibling) {
            currentElm.nextElementSibling.focus();
        }
        e.preventDefault();
    }
    // 上キーを入力されたら前の選択肢にフォーカスを当てる
    // ブラウザによりArrowUpだったりUpだったりする
    if (e.key == 'ArrowUp' || e.key == 'Up') {
        if (currentElm.previousElementSibling) {
            currentElm.previousElementSibling.focus();
        }
        e.preventDefault();
    }
    // Enterを入力されたら現在の選択肢をクリックした時と同じ挙動をする
    if (e.key == 'Enter') {
        currentElm.click();
    }
});

これで基本的な挙動は実装できました。

なお、これだけではキーワードをBackspaceやDeleteで削除した際に選択肢を変更する処理は入っていません。
必要であればkeydownイベント等を利用して文字削除時の処理を入れてください。