algolia のフロント側を Vue.js + TypeScript で試す。無限スクロールしたい...

# はじめに

Algolia で検索しながら無限スクロールがしたかったので、algolia に Vue.js + TypeScript で入門します。最終的に以下の gif のようなものができます。

![algolia.gif](https://qiita-image-store.s3.amazonaws.com/0/45822/f222dc9f-9b93-ebdb-02b5-85dbdb0b49aa.gif)

それとなくできたのですが、細かい部分が意図した動きにならず困っています。が、細かい部分なので今は無視しています。
公式サンプルはこちら [infinite scroll](https://community.algolia.com/vue-instantsearch/examples/infinite-scroll.html) なのですが、これも意図した動きをしてくれません。

何が困っているかというと、一度スクロールをすると data の page が `this.page++;` によって大きな数になるのですが、それ以降の検索でうまいことリセットされず、意図した検索結果が返ってこないのです。（最初の20件とかしか返さなくなる..）

僕の場合はこの困りごとを、以下のような方法でお茶を濁しています。これも完璧ではないのですが。

```js
e.addEventListener("input", () => {
  this.page = 1 // まずはリセットして
  this.resetPage() // さらに足りなければ +1 するようの独自メソッド
})  
```

とはいえある程度うごくものなので、まとめておきます。

# Step1. Vue CLI でアプリをつくります

まずはおもむろに、以下コマンドです。TypeScrip を選びましたので、以降 TS です。

```console
$ vue create study-algolia
$ yarn serve
```

これで、めでたく無事エラーがでます。前は出なかったのになー。
https://github.com/webpack/webpack/issues/8768#issuecomment-462090153

仕方ないので、言われるままに以下

![image.png](https://qiita-image-store.s3.amazonaws.com/0/45822/023535e0-d541-8c82-1b78-6f4359cf7737.png)

# Step2. ふつうに algolia をつかってみる

vue でフロントを書くときに便利なツールとして [vue-instantsearch](https://community.algolia.com/vue-instantsearch/) が用意されていますので、これを使います。

```console
$ yarn add vue-instantsearch
```

main.ts に以下を追記します。

```diff:main.ts
+ import InstantSearch from 'vue-instantsearch';
+ Vue.use(InstantSearch);
```

これで、 `yarn serve` すると以下のように怒られます。

```console:error
Could not find a declaration file for module 'vue-instantsearch'.
```

`Vue-instantsearch.d.ts` を書きます。これで、解消します。

```typescript:src/vue-instantsearch.t.ds
declare module 'vue-instantsearch'
```

App.vue を以下のようにすることでひとまず動きます。
cf. [Create your first search experience](https://community.algolia.com/vue-instantsearch/getting-started/getting-started.html#create-your-first-search-experience)

```vue:App.vue
<template>
  <ais-index
    app-id="latency"
    api-key="3d9875e51fbd20c7754e65422f7ce5e1"
    index-name="bestbuy"
  >
    <ais-search-box></ais-search-box>
    <ais-results>
      <template slot-scope="{ result }">
        <h2>
          <ais-highlight :result="result" attribute-name="name"></ais-highlight>
        </h2>
      </template>
    </ais-results>
  </ais-index>
</template>
```

こんな感じ。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/45822/8e92e26a-2989-77ea-5442-52ac199ec8e4.png)

ここまでで、ふつうに algolia を使うところまで出来ました。

# Step3. 自分が用意したデータでインデックスをつくってみる

動作確認をしたいので、index データを自分でつくってみます。go で書きます。

```go:main.go
import (
	"encoding/json"
	"github.com/algolia/algoliasearch-client-go/algoliasearch"
	"net/http"
)

func main() {
	client := algoliasearch.NewClient(
		"{Your Application ID}",
		"{Admin API Key}",
	)
	index := client.InitIndex("test_notes")
	str := `[{"id": 1, "content": "Hello world"}, {"id": 2, "content": "I like Vue.js"}]`
	var posts []algoliasearch.Object
	d := json.NewDecoder(strings.NewReader(str))
	d.Decode(&posts)
	index.AddObjects(posts)
}
```

```console
$ go run main.go
```

これで、インデックスがつくられてダッシュボードからも確認が可能になります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/45822/3007b4d4-de29-0f6d-580a-27298fef59c9.png)

これを Vue.js 側から呼び出すには以下の部分を修正します。

```vue:App.vue
// 抜粋
<div id="app" class="container-fluid">
    <ais-index
      id="main"
      app-id="自分の" // <- ここ
      api-key="自分の" // <- ここ
      index-name="test_notes" // <- ここ
// 抜粋
    <ais-highlight :result="result" attribute-name="content"></ais-highlight> // <- ここ
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/45822/eea4fd8a-f8b5-d0c1-b2a3-1c1afd3aadce.png)

良い感じに呼び出せています。

大量にデータを入れておきたいので、json ファイルから index を作れるように go のコードを変更しておきます。

```go:main.go
func main() {
	client := algoliasearch.NewClient(
		"{Your Application ID}",
		"{Admin API Key}",
	)
	index := client.InitIndex("test_notes")
	jsonFile, _ := os.Open("notes.json")

	var posts []algoliasearch.Object
	byteValue, _ := ioutil.ReadAll(jsonFile)
	json.Unmarshal([]byte(byteValue), &posts)
	index.AddObjects(posts)
}
```

`notes.json` に json データを大量に書いて置いてください。無限スクロールが楽しめるように。

# Step4. 無限スクロールに対応する

cf. [Infinite scroll](https://community.algolia.com/vue-instantsearch/examples/infinite-scroll.html)

```console
$ yarn add vue-observe-visibility
```

```diff:main.ts
+ import VueObserveVisibility from 'vue-observe-visibility'
+ Vue.use(VueObserveVisibility)
```

```diff:App.vue
     index-name="test_notes"
+    :query-parameters="{'page': page}"
   >
     <ais-search-box></ais-search-box>
-    <ais-results>
+    <ais-results :stack="true">
       <template slot-scope="{ result }">
         <h2>
           <ais-highlight :result="result" attribute-name="content"></ais-highlight>
         </h2>
       </template>
     </ais-results>
+    <div v-observe-visibility="loadMore">Loading more...</div>
   </ais-index>
 </template>
```

以上で、無限スクロールに対応できました。

# Step5. 検索のたびに page = 1 にリセットしてみる

調整をした App.vue の全体を記載しておきます。 `mounted` と `resetPage` を追加しています。

これで冒頭の gif が得られます。

```typescript:main.ts
import Vue from 'vue'
import App from './App.vue'
import InstantSearch from 'vue-instantsearch';
import VueObserveVisibility from 'vue-observe-visibility'

Vue.use(InstantSearch);
Vue.use(VueObserveVisibility)

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')
```

```vue:App.vue
<template>
  <div id="app" class="container-fluid">
    <ais-index
      id="main"
      app-id="自分の"
      api-key="自分の"
      index-name="test_notes"
      :query-parameters="{'page': page}"
    >
      <div class="row">
        <ais-search-box>
          <div class="input-group">
            <ais-input
              id="input"
              placeholder="Search product by name or reference..."
              :class-names="{
                'ais-input': 'form-control'
                }"
            ></ais-input>
          </div>
        </ais-search-box>
      </div>
      <div class="row">
        <ais-results :stack="true">
          <template scope="{ result }">
            <div class="content">
              <ais-highlight :result="result" attribute-name="content"></ais-highlight>
            </div>
          </template>
        </ais-results>
        <ais-no-results></ais-no-results>
        <div id="loadMore" v-observe-visibility="loadMore">Loading more...</div>
      </div>
    </ais-index>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";

@Component
export default class App extends Vue {
  page = 1;
  loadMore(isVisible: any) {
    if (isVisible) {
      this.page++;
    }
  }

  mounted() {
    const i = document.querySelector("#input");
    i!.addEventListener("input", () => {
      this.page = 1;
      this.resetPage();
    });
  }

  // page = 1 にして 20 件を表示するが、
  // それでも画面に div id="loadMore" が表示されている場合は page++ するため。
  // ここのロジックが中途半端...
  resetPage() {
    const e = document.querySelector("#loadMore");
    const topPosition = e!.getBoundingClientRect().top;
    const windowHeight = window.innerHeight;
    if (topPosition <= windowHeight) {
      this.page++;
    }
  }
}
</script>

<style>
#app {
  text-align: center;
}
#input {
  margin: 20px;
}
.content {
  height: 40px;
}
</style>
```

以上です。
algolia に入門してみましたが、とても使いやすい気がします。細かなところは、もっと深い部分をちゃんと読まないと。🍋