stimulus-autocompleteを使ってオートコンプリート付き検索機能を作ってみた！

# はじめに
初めまして！未経験からエンジニア転職を目指し、学習中のやましょうと申します。
ポートフォリオの作成中に、検索機能を追加する際、ユーザがフォームに文字を入力すると検索候補が表示されるオートコンプリート機能をstimulus-autocompleteというコンポーネントを使って実装しました。このコンポーネントを利用することで、簡単に実装ができました。その経験をもとに、備忘録としての手順を共有します！初めての記事執筆となりますので、ご意見やフィードバックをいただけると幸いです！

# 対象者
- 検索機能をちょっとパワーアップさせたい方
- プログラミング初心者



# 完成イメージ
[![Image from Gyazo](https://i.gyazo.com/17788f617d1d2b8eabbed6df736e5c7d.gif)](https://gyazo.com/17788f617d1d2b8eabbed6df736e5c7d)

# 目次
| 章 |    タイトル    |
|---:|-------------|
| 1  |開発環境・使用技術| 
| 2  |セットアップ| 
| 3  |Scaffoldを実行| 
| 4  |検索の実装| 
| 5  |stimulus-autocompleteをインストール|
 6  |オートコンプリート部分のビューを作成|
| 7 |検索フォームとオートコンプリート部分のビューを紐づける|
| 8|おわりに|
| 9 |参考文献|


# 1章　開発環境・使用技術
最近自分がよく使っているからという理由でtailwind cssを採用してます。

- Rails 7.0.8
- Ruby 3.2.2
- tailwind css
- ransack 4.0.0


# 2章　セットアップ
- 今回はstimulus-autocompleteをimportmapを使ってインストールするので、JSのバンドラーをデフォルトのimportmapにしました。
cat-stimulus-autocompleteというアプリ名で作成。
```
rails new cat-stimulus-autocomplete　--css=tailwind --skip-action-mailer --skip-action-mailbox --skip-aciton-text --skip-action-storage --skip-aciotn-cable --skip-test
```
DB作成
```
bin/rails db:create
```
サーバーを起動する
```
bin/dev
```
起動できていることがわかった！
[![Image from Gyazo](https://i.gyazo.com/de94550d1ed95644907b0b5cc8443490.png)](https://gyazo.com/de94550d1ed95644907b0b5cc8443490)


# 3章　Scaffoldを実行
rails g scaffoldを実行。

```
rails g scaffold Cat name:string age:integer
```

```
rails db:migrate
```
サーバーを起動後、```http://localhost:3000/cats```で一覧画面にアクセスする。
```
bin/dev
```


[![Image from Gyazo](https://i.gyazo.com/0a8df3d25c1c5dd9e46b8313266c3462.png)](https://gyazo.com/0a8df3d25c1c5dd9e46b8313266c3462)

その後、適当に初期データを追加しておく。

[![Image from Gyazo](https://i.gyazo.com/133ae327bb1aa5820636692041d2c8cd.png)](https://gyazo.com/133ae327bb1aa5820636692041d2c8cd)
# 4章　検索の実装
今回は、ransackという簡単に検索機能を作れる仕組みを提供してくれるgemを使います。
Gemfileにransackを追加後、bundle install。
```
gem 'ransack'
```
```
bundle install
```


一覧画面に検索フォームを設置します。
```index.html.erb```に検索フォームを追加する。検索対象のカラムはnameにしています。
```
<div class="w-full">
  <% if notice.present? %>
    <p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block" id="notice"><%= notice %></p>
  <% end %>

  <div class="flex justify-between items-center">
    <h1 class="font-bold text-4xl">Cats</h1>
    <%= link_to "New cat", new_cat_path, class: "rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium" %>
  </div>
  
  <!-- 検索フォームを追加 -->
  <%= search_form_for @cat, url: cats_path do |f| %>
    <div class="relative flex rounded-md">
		<%= f.search_field :name_cont, class: "w-[300px] py-3 px-4 pl-11 block border-gray-200 shadow-sm rounded-md text-sm focus:z-10 focus:border-blue-500 focus:ring-blue-500 dark:bg-slate-900 dark:border-gray-700 dark:text-gray-400", placeholder: "名前を入力してください" %>
		<%= f.hidden_field :name %>
		<ul class="list-group bg-white absolute w-full md:text-sm max-w-max" data-autocomplete-target="results"></ul>
        <div class="absolute inset-y-0 left-0 flex items-center pointer-events-none pl-4">
          <svg class="h-4 w-4 text-gray-400" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
            <path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/>
          </svg>
        </div>
		<%= f.submit "検索", class: "ml-4 py-3 px-4 inline-flex flex-shrink-0 justify-center items-center gap-2 rounded-md border border-transparent font-semibold bg-blue-500 text-white hover:bg-blue-600 focus:z-10 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all text-sm" %>
    </div>
  <% end %>
  <!-- ここまで -->

  <div id="cats" class="min-w-full">
    <%= render @cats %>
  </div>
</div>
```



```cats_controller.rb```のindexアクションを修正。

```
class CatsController < ApplicationController
  before_action :set_cat, only: %i[ show edit update destroy ]

  # GET /cats or /cats.json
  def index
    @cat = Cat.ransack(params[:q])
    @cats = @cat.result
  end

　　　　以下省略
end

```

```models/cat.rb```を修正。
```
class Cat < ApplicationRecord
  def self.ransackable_attributes(auth_object = nil)
    ["age", "created_at", "id", "name", "updated_at"]
  end
end
```
- ransack '4.0.0'から、検索対象のテーブル(catsテーブル)のカラムをモデル(Cat.rb)に明示しないと検索してくれないので記述しています。この記述を忘れてしまうと、以下のようなエラーが出るので気をつけましょう！
```
ActionView::Template::Error (Ransack needs Cat attributes explicitly allowlisted as
19:07:25 web.1  | searchable. Define a `ransackable_attributes` class method in your `Cat`
19:07:25 web.1  | model, watching out for items you DON'T want searchable (for
19:07:25 web.1  | example, `encrypted_password`, `password_reset_token`, `owner` or
19:07:25 web.1  | other sensitive information). You can use the following as a base:
19:07:25 web.1  | 
19:07:25 web.1  | ```ruby
19:07:25 web.1  | class Cat < ApplicationRecord
19:07:25 web.1  | 
19:07:25 web.1  |   # ...
19:07:25 web.1  | 
19:07:25 web.1  |   def self.ransackable_attributes(auth_object = nil)
19:07:25 web.1  |     ["age", "created_at", "id", "name", "updated_at"]
19:07:25 web.1  |   end
19:07:25 web.1  | 
19:07:25 web.1  |   # ...
19:07:25 web.1  | 
19:07:25 web.1  | end
19:07:25 web.1  | ```

```

一覧画面を開くと検索フォームが表示されて、猫の名前で検索できるようになりました！
まだ、フォームに文字を入力すると検索候補が表示されるようになっていません。
ここからが本番です！
[![Image from Gyazo](https://i.gyazo.com/fc9ecc2363502192a0df82520a54bd1b.gif)](https://gyazo.com/fc9ecc2363502192a0df82520a54bd1b)


# 5章　stimulus-autocompleteをインストール
ここでフォームに文字を入力すると検索候補が表示される挙動を実現するために、
stimulus-autocompleteを使います。
- stimulus-autocompleteとはStimulusで書かれたオートコンプリート機能の仕組みを提供してくれるコンポートネント。
https://github.com/afcapel/stimulus-autocomplete

- Stimulusについて知りたい方は、以下の記事が分かりやすく説明されています！
https://blog.to-ko-s.com/stimlus-introduce/


※Rails7系では```rails new```の段階でstimulus-railsが自動でインストールされているので新たにStimulusをインストールする必要はありません。


今回はimportmapでstimulus-autocompleteをインストールしています。
```
./bin/importmap pin stimulus-autocomplete
```



実行後の```config/importmap.rb```
```
# Pin npm packages by running ./bin/importmap

pin "application", preload: true
pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
pin "@hotwired/stimulus", to: "https://ga.jspm.io/npm:@hotwired/stimulus@3.2.2/dist/stimulus.js" #追加された
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
pin_all_from "app/javascript/controllers", under: "controllers"
pin "stimulus-autocomplete", to: "https://ga.jspm.io/npm:stimulus-autocomplete@3.1.0/src/autocomplete.js" #追加された
```



- ```app/javascript/controllers/application.js```にstimulus-autocompleteの設定を追加する。

```
import { Application } from "@hotwired/stimulus"
import { Autocomplete } from 'stimulus-autocomplete'  #コンポーネントを読み込むための記述

const application = Application.start()
application.register('autocomplete', Autocomplete)  #コンポーネントにあるAutocompleteコントローラを使えるようにするための記述

// Configure Stimulus development experience
application.debug = false
window.Stimulus   = application

export { application }
```

- ```application.register('autocomplete', Autocomplete)```の記述により、HTML内でdata-controller="autocomplete"の属性をもつ要素に対して、[Autocompleteコントローラ](https://github.com/afcapel/stimulus-autocomplete/blob/main/src/autocomplete.js)の機能が適用されます。



# 6章　自動補完部分のビューを作る
[![Image from Gyazo](https://i.gyazo.com/a89bcf973e7e77a7ba89613ca1a6dd48.jpg)](https://gyazo.com/a89bcf973e7e77a7ba89613ca1a6dd48)

views/cats/search.html.erbを新たに作成する
```
<% @cats.each do |cat| %>
  <li class="w-80 flex items-center gap-x-3.5 py-2 px-3 rounded-md text-sm text-gray-800 hover:bg-blue-200 focus:ring-2 focus:ring-blue-500 dark:text-gray-400 dark:hover:bg-blue-200 dark:hover:text-gray-300" role="option" data-autocomplete-value="<%= cat.id %>" data-autocomplete-label="<%= cat.name %>">
    <%= cat.name %>
  </li>
<% end %>
```

# 7章　検索フォームとオートコンプリート部分のビューを紐付ける
#### 検索フォームを修正

- 入力フォームに文字を入力した時に、自動補完させたいので```index.html.erb```の入力フォームを```<div data-controller="autocomplete" data-autocomplete-url-value="/cats/search" role="combobox"></div>```で囲む。

- Autocompleteコントローラが入力フォームを取得するために、以下を追加する。

 ```<%= f.search_field :name_cont ... %>```に```data: { autocomplete_target: 'input' }```を追加する。
```<%= f.hidden_field :name ... %>```に```data: { autocomplete_target: 'hidden' }```を追加する。

```
  <!-- 検索フォームを追加 -->
  <%= search_form_for @cat, url: cats_path do |f| %>
    <div class="relative flex rounded-md">
      <div data-controller="autocomplete" data-autocomplete-url-value="/cats/search" role="combobox">
		<%= f.search_field :name_cont, data: { autocomplete_target: 'input' }, class: "w-[300px] py-3 px-4 pl-11 block border-gray-200 shadow-sm rounded-md text-sm focus:z-10 focus:border-blue-500 focus:ring-blue-500 dark:bg-slate-900 dark:border-gray-700 dark:text-gray-400", placeholder: "名前を入力してください" %>
		<%= f.hidden_field :name, data: { autocomplete_target: 'hidden' } %>
		<ul class="list-group bg-white absolute w-full md:text-sm max-w-max" data-autocomplete-target="results"></ul>
        <div class="absolute inset-y-0 left-0 flex items-center pointer-events-none pl-4">
          <svg class="h-4 w-4 text-gray-400" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
            <path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/>
          </svg>
        </div>
	  </div>
	  <%= f.submit "検索", class: "ml-4 py-3 px-4 inline-flex flex-shrink-0 justify-center items-center gap-2 rounded-md border border-transparent font-semibold bg-blue-500 text-white hover:bg-blue-600 focus:z-10 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all text-sm" %>
    </div>
  <% end %>
  <!-- ここまで -->
```
処理の流れ
1.  検索フォームがブラウザで表示されると、data-controller="autocomplete"が読み込まれ、Autocompleteコントローラの[connectメソッド](https://github.com/afcapel/stimulus-autocomplete/blob/main/src/autocomplete.js#L19)が呼び出され、connectメソッド内に定義されている各イベントを監視する。
2. 入力フォームの内容が変更されると、connectメソッド内の[this.inputTarget.addEventListener("input", this.onInputChange)](https://github.com/afcapel/stimulus-autocomplete/blob/main/src/autocomplete.js#L31C5-L31C67)が発火し、[onInputChangeメソッド](https://github.com/afcapel/stimulus-autocomplete/blob/main/src/autocomplete.js#L172)を呼び出す。
3. onInputChange内で入力内容が一定の長さ（this.minLengthValue）以上である場合、[fetchResultsメソッド](https://github.com/afcapel/stimulus-autocomplete/blob/main/src/autocomplete.js#L194)が呼び出される。
4. this.hasUrlValueがtrueである場合（data-autocomplete-url-value属性が設定されている場合）に、サーバーからオートコンプリートの結果を取得する。このメソッド内で、[buildURLメソッド](https://github.com/afcapel/stimulus-autocomplete/blob/main/src/autocomplete.js#L211)を使用してリクエストのURLを構築し、そのURLに対して非同期のリクエストを行う。


#### searchアクションとルーティングを設定する
- Autocompletコントローラのメソッドから、非同期のリクエストを受け取ってオートコンプリート部分のビューを返すためにcatsコントローラにsearchアクションを追加。また、ルーティングを設定する。

これにより検索フォームに文字が入力されると、/cats/searchにリクエストが行われ、オートコンプリートの結果が取得されるようになります。

```
class CatsController < ApplicationController

  省略

  # 追加
  def search
    @cats = Cat.where("name like ?", "%#{params[:q]}%")
    respond_to do |format|
      format.js
    end
  end

  private

  # Use callbacks to share common setup or constraints between actions.
  def set_cat
    @cat = Cat.find(params[:id])
  end
  # Only allow a list of trusted parameters through.
  def cat_params
    params.require(:cat).permit(:name, :age, :cat_image, :cat_image_cache)
  end
end
```

```
resources :cats do
  get :search, on: :collection
end
```
そうすると、完成！！！
[![Image from Gyazo](https://i.gyazo.com/969fa6e58e170665bd18fd854ca23c51.gif)](https://gyazo.com/969fa6e58e170665bd18fd854ca23c51)


# 8章　おわりに
初めて学んだ知識を記事に書き起こしてみたのですが、実装している際に気づかなかったことに気づけたり、Stimulusに関する理解がより深まったと思うので書いてよかったなと思っています。
また、新たに機能を付け加える際には、便利なコンポーネントやライブラリがないか探すようにしようと思えるようにもなりました。この記事が皆さんのポートフォリオ作りのお役に立つことができれば幸いです。




# 9章　参考文献
https://zenn.dev/nagan/articles/e627918c192265
https://github.com/afcapel/stimulus-autocomplete
https://blog.to-ko-s.com/stimlus-introduce/