# 『Vue.js Tutorial From Scratch - e14 - Autocomplete』をやってみました
先日 https://www.yoshida.red/search.html にAlgoliaのQuery Suggestionsを導入したのですが、その時に"そもそもVue.jsでオートコンプリートって?"と一瞬たじろいでしまったので、ちゃんと習得しておきたいと思います。
# オートコンプリートって?
例えばテキストボックスにカーソルをあわせたら、ブワっとリストが出てきて選べる的なヤツ
もちろんタイピングしたら、それに追随してくれるヤツ
何かを選択したら、その内容がテキストボックスに反映されます。
# ってことでやっていきます
Vue CLIを使います。
vue create autocomplete
Manually Selectを選択してからの、今回はRouterにチェックをいれます。
デフォルトのまま進んでいきつつ、dedicated configが必要、と。
で、動画には出てこなかったのですが、yarnかnpmかってところで、その後を見ていくとnpmっぽかったのでNPMにしてみます。
最終的に👇こんな感じになったらOK。
⚓ Running completion hooks...
📄 Generating README.md...
🎉 Successfully created project autocomplete.
👉 Get started with the following commands:
$ cd autocomplete
$ npm run serve
そして、npmでtailwindcssというのをいれていきます。
$ npm install tailwindcss
# VS Codeを使っていきます
srcディレクトリにmain.cssを追加します。
そのCSSファイルに👇の3行を追加します。
main.jsで上記のCSSをインポートします。
そして、(聞いたことないけど…)postcss.config.jsにも設定が必要、と。とりあえず👇コレでイイのかな、、
そんなこんなで npm run serve してやると、localhost:8080で以下のような画面が出てきます。
で、今回はAboutのページの"This is an about page"のところをいじっていきます、と。もちろん汎用的なコンポーネントにしてOKだけどねん。
# About.vue
今回は👇の"This is an about page"のところに変更を加えていきます。
まずはh1をinputに置き換えて👇のように記載します。
<input type="text" v-bind="state">
scriptセクションは以下のようにdataプロパティに関数でstateとして空を返す。
<script>
export default {
data: function () {
return {
state: '',
}
}
}
</script>
画面を見ると(最初真っ白でどこにインプットボックスがあるのか分かりませんでしたが…w)👇のようになります。
で、inputのclass属性に👇のように入れてやると見やすくなる、と。
class="bg-gray-300 px-4 py-2"
👇あぁ、確かに…w
そしてautocomplete="off"にして、ブラウザによるオートコンプリートを避けます。ということで、inputタグの中身は最終的に👇こんな感じ。
<input type="text" class="bg-gray-300 px-4 py-2" autocomplete="off" v-bind="state">
# 配列データ
今度はstatesという配列に'Florida', 'Alabama', 'Alaska', 'Texas'を持たせます。といことで、scriptタグの中は👇のようになります。
<script>
export default {
data: function () {
return {
state: '',
states: [
'Florida', 'Alabama', 'Alaska', 'Texas'
]
}
}
}
</script>
# filterStatesとメソッドを追加
inputタグに👇を追加して、
@input="filterStates"
scriptタグの中にmethodsを追加して、その中にfilterStatesを定義しつつ、dataにおいてもfilteredStatesという空の配列を用意します。
<script>
export default {
data: function () {
return {
state: '',
states: [
'Florida', 'Alabama', 'Alaska', 'Texas'
],
filteredStates: [],
}
},
methods: {
filterStates() {
}
}
}
</script>
filterStatesメソッドの中はinputで入力された文字列が前方一致(startsWith)するものを返してやるという形。ポイントとしては、大文字で入力されても小文字で入力されても大丈夫なようにtoLowerCase()してあげてるところ。
filterStates() {
this.filteredStates = this.states.filter(state => {
return state.toLowerCase().startsWith(this.state.toLowerCase());
});
}
そして、inputのv-bindをv-modelに変更します。
<input type="text" class="bg-gray-300 px-4 py-2" autocomplete="off" v-model="state" @input="filterStates">`
画面でflと入力すると、filteredStatesがArray[1]になっていて、中身は 0: "Florida" になっていることが分かります。
alと入力する場合はAlabamaとAlaskaが両方ヒットします。
以上は基本的な実装になります。
# 配列を画面に表示するように実装
新しくdiv要素を追加していきます。 filteredStatesが存在する場合は、filteredStates(複数形)の中のfilteredState(単数形)をv-forで回して取り出してマスタッシュで表示させる形👇
<div v-if="filteredStates">
<ul>
<li v-for="filteredState in filteredStates">{{ filteredState }}</li>
</ul>
</div>
Youtubeではそのまま画面表示できてるようでしたが、自分の環境では👇のようにユニークなKeyを指定してくださいというエラーが出ていました。
Failed to compile.
./src/views/About.vue
Module Error (from ./node_modules/eslint-loader/index.js):
/Users/eijishinohara/vueis/autocomplete/src/views/About.vue
6:9 error Elements in iteration expect to have 'v-bind:key' directives vue/require-v-for-key
✖ 1 problem (1 error, 0 warnings)
ということで、なんか冗長ですが、:key="filteredState"を付けることにしました。
<li v-for="filteredState in filteredStates" :key="filteredState">{{ filteredState }}</li>
画面表示をすると、最初は全部表示されていますが、
alを入力するとAlabamaとAlaskaだけが表示されるようになりました。
# クリックで選択した値がinputに反映されるようにする
liタグにクリックされた際のというイベントリスナーを追加します。
@click="setState(filteredState)"
そして、setStateメソッドをscriptのmethodsの中に実装します。stateを受け取って、そのstateをセットするだけのシンプルなもの
setState(state) {
this.state = state;
}
これで、fと入力して出てきたFloridaをクリックしたら、Floridaがテキストボックスに入るようになりました 😃
aを入力して出てきたAlaskaを選んでも同様です。
# 選択したらもう選択肢は消えて欲しいよね
と、その前に、今後はulにclass属性を追加して、"w-48 bg-gray-800 text-white"という設定で見た目を良くして、
<ul class="w-48 bg-gray-800 text-white">
liの方にもclassを追加。
class="py-2 border-b cursor-pointer"
画面を開くとイイ感じだけど、出てきたリストが左に寄り過ぎ感。
これをinputのところに持ってくる魔法があるようで、一番外側のdivのclassの設定で👇のようにしてやると、
<div class="about flex flex-col items-center">
👇のように寄ってくれました。
# 開いているのか閉じているのかの状態管理
デフォルトでは data: の中で👇のようにしておいて、
modal: false,
v-ifでfilteredStatesがあるかどうかっていう条件だけだったところにmodalかどうかも追加します。
<div v-if="filteredStates && modal">
この状態だとmodalをtrueにしてないので、入力しても何も出てきません👇
modalをtrueにするトリガーとしてinputの@onfocusを使います。そして、divタグの中に直接modal = trueと記載。
@focus="modal = true"
こうすることで再び👇のように選択できるようになりました。
今度は選択されたらリストを非表示にします。setStateは選択された時のメソッドなので、ここでmodalをfalseにします。
setState(state) {
this.state = state;
this.modal = false;
}
そうすると、例えばalと入力してAlaskaとAlabamaが出てきた場合でもAlaskaを選択してそれをinputの値にセットしたら、リストは消えるようになります。
# 一番最初にinputにカーソルが当たった時
せっかくカーソルを当ててくれたなら候補を出したいですよねぇ。ということで、InitialなStateを作っていきます。そのために mounted を使います。実装は簡単で👇だけです。
mounted() {
this.filterStates();
},
ただし、この場合stateは data: にあるように state:'' という空のままです。なので、この状態でinputにカーソルを当てても何も起こりません(空に該当するものはない為)。
ということで、『もしinputの文字列が何もなかったら、全部出してやれ』という実装をfilterStatesメソッドの中で行います。
if (this.state.length == 0) {
this.filteredStates = this.states;
}
という感じで👇のようにカーソルが当たったら、選択肢が出るようになりました。
# しかし、、ですよ
例えば、Alaskaを選んだとして、もう一回inputにカーソルを当てるとまたリストが全て出てきてしまいます。なぜでしょうか??
この場合はfilterStatesメソッドが呼ばれないのが原因で、なぜならリストを選択した行為は @input では拾われないからです、と。(@inputは何か文字が入力された時ようのリスナー)
ということで、inputタグの中で@inputを使う代わりに watch: を使っていきます。もし、stateが変化したら、filterStatesメソッドをコールしてあげる。
コード的には👇。コレで入力だけでなくてもリストから選んだ時にもfilterStatesメソッドがコールされる。
watch: {
state() {
this.filterStates();
}
}
そうすることで、Floridaを選んだら、リストが消えて、
もう一度inputにカーソルを合わせると、Floridaだけがリスト表示されるようになります。
もちろんinputの内容を消せば、全てのリストが出てくるし、
alとか入れれば、今までのようにAlabamaとAlaskaがリスト表示されます。
# Real Worldで良く目にするような実践的な内容でしたが
👇のようにハードコードされた配列とかいけてないし、axiosとか使ってどっかから取ってくるようにしたり、並べ替えたりしてみてね〜、と。
states: [
'Florida', 'Alabama', 'Alaska', 'Texas'
],
# ということで、以上です。
いや〜、楽しいチュートリアルだったなー 😃 ご参考までに、今回やりくりしたApp.vueの私のコードは👇になります。
<template>
<div class="about flex flex-col items-center">
<input type="text" class="bg-gray-300 px-4 py-2" autocomplete="off" v-model="state" @input="filterStates" @focus="modal = true">
<div v-if="filteredStates && modal">
<ul class="w-48 bg-gray-800 text-white">
<li v-for="filteredState in filteredStates" class="py-2 border-b cursor-pointer" :key="filteredState" @click="setState(filteredState)">{{ filteredState }}</li>
</ul>
</div>
</div>
</template>
<script>
export default {
data: function () {
return {
state: '',
modal: false,
states: [
'Florida', 'Alabama', 'Alaska', 'Texas'
],
filteredStates: [],
}
},
mounted() {
this.filterStates();
},
methods: {
filterStates() {
if (this.state.length == 0) {
this.filteredStates = this.states;
}
this.filteredStates = this.states.filter(state => {
return state.toLowerCase().startsWith(this.state.toLowerCase());
});
},
setState(state) {
this.state = state;
this.modal = false;
}
},
watch: {
state() {
this.filterStates();
}
}
}
</script>