序章

オートコンプリートは、一般的な最新の機能です。 ユーザーが入力フィールドを操作すると、入力内容に関連する推奨値のリストが提供されます。

この記事では、v-modelを使用してオートコンプリートコンポーネントを作成する方法、キー修飾子を使用してイベントを処理する方法、および非同期リクエストに備えてコンポーネントを準備する方法を学習します。

前提条件

このチュートリアルを完了するには、次のものが必要です。

このチュートリアルは、ノードv15.3.0、npm v6.14.9、およびvuev2.6.11で検証されました。

ステップ1—プロジェクトの設定

このチュートリアルでは、@vue/cliで生成されたデフォルトのVueプロジェクトからビルドします。

  1. npx @vue/cli create vue-autocomplete-component-example --default

これにより、デフォルト構成で新しいVueプロジェクトが構成されます:Vue 2babeleslint

新しく作成されたプロジェクトディレクトリに移動します。

  1. cd vue-autocomplete-component-example

これで、コードエディタを使用して新しいオートコンプリートコンポーネントを作成できます。 これは、テンプレート、スクリプト、およびスタイルを備えた単一ファイルのVueコンポーネントになります。

オートコンプリートコンポーネントを作成するには、テンプレートに入力とリストの2つが必要です。

src / components / SearchAutocomplete.vue
<template>
  <div class="autocomplete">
    <input
      type="text"
    />
    <ul
      class="autocomplete-results"
    >
      <li
        class="autocomplete-result"
      >
        (result)
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'SearchAutocomplete'
};
</script>

このコードは、テキストを入力するための入力とオートコンプリートの結果を表示するための順序付けられていないリストを含むdivを作成します。

次に、同じファイル内のコンポーネントにいくつかのスタイルを指定します。

src / components / SearchAutocomplete.vue
<style>
  .autocomplete {
    position: relative;
  }

  .autocomplete-results {
    padding: 0;
    margin: 0;
    border: 1px solid #eeeeee;
    height: 120px;
    min-height: 1em;
    max-height: 6em;    
    overflow: auto;
  }

  .autocomplete-result {
    list-style: none;
    text-align: left;
    padding: 4px 2px;
    cursor: pointer;
  }

  .autocomplete-result:hover {
    background-color: #4AAE9B;
    color: white;
  }
</style>

ステップ2—検索結果のフィルタリング

ユーザーが入力するときに、結果のリストを表示する必要があります。 コードは、入力の変更をリッスンして、それらの結果をいつ表示するかを知る必要があります。

そのためには、v-modelを使用します。 v-modelは、ユーザー入力イベントのデータを更新するフォーム入力とテキストエリアの双方向データバインディングを備えたディレクティブです。

結果をフィルタリングするには、ユーザーがいつ入力を終了したかを知る必要があるため、@inputのイベントリスナーも追加します。

src / components / SearchAutocomplete.vue
<template>
  <div class="autocomplete">
    <input
      v-model="search"
      @input="onChange"
      type="text"
    />
    <ul
      class="autocomplete-results"
    >
      <li
        class="autocomplete-result"
      >
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'SearchAutocomplete',
  data() {
    return {
      search: '',
    };
  },
  methods: {
    onChange() {
      // ...
    }
  }
};
</script>

何を検索し、いつ検索するかがわかったので、表示するデータが必要になります。

このチュートリアルでは、値の配列を使用しますが、フィルター関数を更新して、より複雑なデータ構造を処理することもできます。

App.vueを開き、autocompleteコンポーネントをインポートして参照するように変更します。

src / App.vue
<template>
  <div id="app">
    <SearchAutocomplete
      :items="[
        'Apple',
        'Banana',
        'Orange',
        'Mango',
        'Pear',
        'Peach',
        'Grape',
        'Tangerine',
        'Pineapple'
      ]"
    />
  </div>
</template>

<script>
import SearchAutocomplete from './components/SearchAutocomplete.vue'

export default {
  name: 'App',
  components: {
    SearchAutocomplete
  }
}
</script>

次に、コードエディタでSearchAutocomplete.vueに再度アクセスし、コードを追加して検索結果をフィルタリングして表示します。

src / components / SearchAutocomplete.vue
<template>
  <div class="autocomplete">
    <input
      v-model="search"
      @input="onChange"
      type="text"
    />
    <ul
      v-show="isOpen"
      class="autocomplete-results"
    >
      <li
        v-for="(result, i) in results"
        :key="i"
        class="autocomplete-result"
      >
        {{ result }}
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'SearchAutocomplete',
  props: {
    items: {
      type: Array,
      required: false,
      default: () => [],
    },
  },
  data() {
    return {
      search: '',
      results: [],
      isOpen: false,
    };
  },
  methods: {
    filterResults() {
      this.results = this.items.filter(item => item.toLowerCase().indexOf(this.search.toLowerCase()) > -1);
    },
    onChange() {
      this.filterResults();
      this.isOpen = true;
    }
  },
}
</script>

filterResults()で、toLowerCase()が入力されたテキストと配列の各要素の両方にどのように適用されるかに注目してください。 これにより、ユーザーは大文字または小文字の単語を使用しても、関連する結果を得ることができます。

また、ユーザーが何かを入力した後にのみ結果のリストを表示するようにする必要があります。 v-show を使用して条件付きで表示することで、これを実現できます。

注: v-ifの代わりにv-showを使用する理由は、このリストの表示が頻繁に切り替えられるためですが、v-showの初期レンダリングコストはが高いほど、v-ifのトグルコストは高くなります。

ステップ3—クリックイベントを使用した検索結果の更新

次に、結果のリストが使用可能であることを確認する必要があります。 ユーザーが結果の1つをクリックして、その値を選択した値として自動的に表示できるようにする必要があります。

これは、クリックイベントをリッスンし、値を検索語として設定することで実現できます。

src / components / SearchAutocomplete.vue
<template>
  <div class="autocomplete">
    <input
      v-model="search"
      @input="onChange"
      type="text"
    />
    <ul
      v-show="isOpen"
      class="autocomplete-results"
    >
      <li
        v-for="(result, i) in results"
        :key="i"
        @click="setResult(result)"
        class="autocomplete-result"
      >
        {{ result }}
      </li>
    </ul>
  </div>
</template>

また、ユーザーエクスペリエンスを向上させるために、結果のリストを閉じる必要があります。

src / components / SearchAutocomplete.vue
<script>
export default {
  name: 'SearchAutocomplete',
  // ...
  methods: {
    setResult(result) {
      this.search = result;
      this.isOpen = false;
    },
    // ...
  },
}
</script>

ユーザーが結果リストをクリックして結果リストを閉じるには、このコンポーネントの外部でクリックイベントをリッスンする必要があります。 コンポーネントがマウントされたら、ユーザーがどこかをクリックしたときに、それがコンポーネントの外にあるかどうかを確認する必要があることを達成しましょう。 Node.contains を使用して、イベントターゲットがコンポーネントに属しているかどうかを確認します。

src / components / SearchAutocomplete.vue
<script>
export default {
  name: 'SearchAutocomplete',
  // ...
  mounted() {
    document.addEventListener('click', this.handleClickOutside);
  },
  destroyed() {
    document.removeEventListener('click', this.handleClickOutside);
  }
  methods: {
    // ...
    handleClickOutside(event) {
      if (!this.$el.contains(event.target)) {
        this.isOpen = false;
      }
    }
  },
}
</script>

この時点で、アプリケーションをコンパイルできます。

  1. npm run serve

次に、Webブラウザで開きます。 オートコンプリートコンポーネントを操作して、配列からアイテムが提案されるようにすることができます。

ステップ4—矢印キーナビゲーションのサポート

UPDOWNENTERキーの両方のイベントリスナーを追加しましょう。 キー修飾子のおかげで、Vueは最も一般的に使用されるキーのエイリアスを提供するため、どのキーコードが各キーに属しているかを確認する必要はありません。

どの結果が選択されているかを追跡するために、arrowCounterを追加して、検索結果の配列に現在のインデックスの値を保持します。 arrowCounterの初期値を-1に設定して、ユーザーがアクティブに選択する前にオプションが選択されないようにします。 ユーザーがDOWNキーを押したら、arrowCounterの値を1増やします。 ユーザーがUPキーを押したら、arrowCounterの値を1減らします。

ユーザーがENTERキーを押すと、結果の配列からそのインデックスを取得する必要があります。

リストが終了したらカウントを続けたり、結果が表示される前にカウントを開始したりしないように注意する必要があります。

src / components / SearchAutocomplete.vue
<template>
  <div class="autocomplete">
    <input
      v-model="search"
      @input="onChange"
      @keydown.down="onArrowDown"
      @keydown.up="onArrowUp"
      @keydown.enter="onEnter"
      type="text"
    />
    <ul
      v-show="isOpen"
      class="autocomplete-results"
    >
      <li
        v-for="(result, i) in results"
        :key="i"
        @click="setResult(result)"
        class="autocomplete-result"
        :class="{ 'is-active': i === arrowCounter }"
      >
        {{ result }}
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'SearchAutocomplete',
  // ...
  data() {
    return {
      search: '',
      results: [],
      isOpen: false,
      arrowCounter: -1
    };
  },
  methods: {
    // ...
    handleClickOutside(event) {
      if (!this.$el.contains(event.target)) {
        this.arrowCounter = -1;
        this.isOpen = false;
      }
    },
    onArrowDown() {
      if (this.arrowCounter < this.results.length) {
        this.arrowCounter = this.arrowCounter + 1;
      }
    },
    onArrowUp() {
      if (this.arrowCounter > 0) {
        this.arrowCounter = this.arrowCounter - 1;
      }
    },
    onEnter() {
      this.search = this.results[this.arrowCounter];
      this.arrowCounter = -1;
      this.isOpen = false;
    }
  },
}
</script>

選択したオプションにアクティブなCSSクラスを追加して、視覚的な補助を追加しましょう。

src / components / SearchAutocomplete.vue
<style>
  // ...

  .autocomplete-result.is-active,
  .autocomplete-result:hover {
    background-color: #4AAE9B;
    color: white;
  }
</style>

ステップ5—非同期読み込みの処理

さらに、サーバーからの応答を待って結果をロードする必要があることをコンポーネントに通知することにより、非同期サポートを提供できます。

注:コンポーネント内でリクエストを行うことができますが、ほとんどのアプリはすでに特定のlibを使用してリクエストを行っているため、ここに依存関係を追加する必要はありません。

コンポーネントにいくつかの変更を加える必要があります。

  1. 結果を待つ必要があるかどうかを通知するためのポインター
  2. 入力の値が変更されたら、親コンポーネントにイベントを発行します
  3. データがいつ受信されたかを知るウォッチャー
  4. ユーザーに通知するためのローディングインジケーター
src / components / SearchAutocomplete.vue
<template>
  <div class="autocomplete">
    <input
      v-model="search"
      @input="onChange"
      @keydown.down="onArrowDown"
      @keydown.up="onArrowUp"
      @keydown.enter="onEnter"
      type="text"
    />
    <ul
      v-show="isOpen"
      class="autocomplete-results"
    >
      <li
        v-if="isLoading"
        class="loading"
      >
        Loading results...
      </li>
      <li
        v-else
        v-for="(result, i) in results"
        :key="i"
        @click="setResult(result)"
        class="autocomplete-result"
        :class="{ 'is-active': i === arrowCounter }"
      >
        {{ result }}
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'SearchAutocomplete',
  props: {
    // ...
    isAsync: {
      type: Boolean
      required: false,
      default: false,
    },
  },
  // ...
  watch: {
    items: function (value, oldValue) {
      if (this.isAsync) {
        this.results = value;
        this.isOpen = true;
        this.isLoading = false;
      },
    }
  },
  // ...
  methods: {
    // ...
    onChange() {
      this.$emit('input', this.search);

      if (this.isAsync) {
        this.isLoading = true;
      } else {
        this.filterResults();
        this.isOpen = true;
      }
    },
    // ...
  },
}
</script>

このコード変更により、isAsyncプロップが導入されます。 これがコンポーネントに存在する場合、サーバー要求からの要求によって提供されるitemsを比較することによって結果をフィルタリングします。

ステップ6—プロジェクトのまとめ

すべての変更を適用すると、SearchAutocomplete.vueは次のようになります。

src / components / SearchAutocomplete.vue
<template>
  <div class="autocomplete">
    <input
      type="text"
      @input="onChange"
      v-model="search"
      @keydown.down="onArrowDown"
      @keydown.up="onArrowUp"
      @keydown.enter="onEnter"
    />
    <ul
      id="autocomplete-results"
      v-show="isOpen"
      class="autocomplete-results"
    >
      <li
        class="loading"
        v-if="isLoading"
      >
        Loading results...
      </li>
      <li
        v-else
        v-for="(result, i) in results"
        :key="i"
        @click="setResult(result)"
        class="autocomplete-result"
        :class="{ 'is-active': i === arrowCounter }"
      >
        {{ result }}
      </li>
    </ul>
  </div>
</template>

<script>
  export default {
    name: 'SearchAutocomplete',
    props: {
      items: {
        type: Array,
        required: false,
        default: () => [],
      },
      isAsync: {
        type: Boolean,
        required: false,
        default: false,
      },
    },
    data() {
      return {
        isOpen: false,
        results: [],
        search: '',
        isLoading: false,
        arrowCounter: -1,
      };
    },
    watch: {
      items: function (value, oldValue) {
        if (value.length !== oldValue.length) {
          this.results = value;
          this.isLoading = false;
        }
      },
    },
    mounted() {
      document.addEventListener('click', this.handleClickOutside)
    },
    destroyed() {
      document.removeEventListener('click', this.handleClickOutside)
    },
    methods: {
      setResult(result) {
        this.search = result;
        this.isOpen = false;
      },
      filterResults() {
        this.results = this.items.filter((item) => {
          return item.toLowerCase().indexOf(this.search.toLowerCase()) > -1;
        });
      },
      onChange() {
        this.$emit('input', this.search);

        if (this.isAsync) {
          this.isLoading = true;
        } else {
          this.filterResults();
          this.isOpen = true;
        }
      },
      handleClickOutside(event) {
        if (!this.$el.contains(event.target)) {
          this.isOpen = false;
          this.arrowCounter = -1;
        }
      },
      onArrowDown() {
        if (this.arrowCounter < this.results.length) {
          this.arrowCounter = this.arrowCounter + 1;
        }
      },
      onArrowUp() {
        if (this.arrowCounter > 0) {
          this.arrowCounter = this.arrowCounter - 1;
        }
      },
      onEnter() {
        this.search = this.results[this.arrowCounter];
        this.isOpen = false;
        this.arrowCounter = -1;
      },
    },
  };
</script>

<style>
  .autocomplete {
    position: relative;
  }

  .autocomplete-results {
    padding: 0;
    margin: 0;
    border: 1px solid #eeeeee;
    height: 120px;
    overflow: auto;
  }

  .autocomplete-result {
    list-style: none;
    text-align: left;
    padding: 4px 2px;
    cursor: pointer;
  }

  .autocomplete-result.is-active,
  .autocomplete-result:hover {
    background-color: #4AAE9B;
    color: white;
  }
</style>

ライブデモはCodePenから入手できます。

結論

この記事では、Vue.jsを使用してオートコンプリートコンポーネントを作成しました。 これは、v-modelfilter@input@click@keydown、および.$emitを利用することによって実現されました。

Vue.jsの詳細については、Vue.jsトピックページで演習とプログラミングプロジェクトを確認してください。