はじめに
初めまして!未経験からエンジニア転職を目指し、学習中のやましょうと申します。
ポートフォリオの作成中に、検索機能を追加する際、ユーザがフォームに文字を入力すると検索候補が表示されるオートコンプリート機能をstimulus-autocompleteというコンポーネントを使って実装しました。このコンポーネントを利用することで、簡単に実装ができました。その経験をもとに、備忘録としての手順を共有します!初めての記事執筆となりますので、ご意見やフィードバックをいただけると幸いです!
対象者
- 検索機能をちょっとパワーアップさせたい方
- プログラミング初心者
完成イメージ
目次
章 | タイトル |
---|---|
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
3章 Scaffoldを実行
rails g scaffoldを実行。
rails g scaffold Cat name:string age:integer
rails db:migrate
サーバーを起動後、http://localhost:3000/cats
で一覧画面にアクセスする。
bin/dev
その後、適当に初期データを追加しておく。
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 | ```
一覧画面を開くと検索フォームが表示されて、猫の名前で検索できるようになりました!
まだ、フォームに文字を入力すると検索候補が表示されるようになっていません。
ここからが本番です!
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コントローラの機能が適用されます。
6章 自動補完部分のビューを作る
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 %>
<!-- ここまで -->
処理の流れ
- 検索フォームがブラウザで表示されると、data-controller="autocomplete"が読み込まれ、Autocompleteコントローラのconnectメソッドが呼び出され、connectメソッド内に定義されている各イベントを監視する。
- 入力フォームの内容が変更されると、connectメソッド内のthis.inputTarget.addEventListener("input", this.onInputChange)が発火し、onInputChangeメソッドを呼び出す。
- onInputChange内で入力内容が一定の長さ(this.minLengthValue)以上である場合、fetchResultsメソッドが呼び出される。
- this.hasUrlValueがtrueである場合(data-autocomplete-url-value属性が設定されている場合)に、サーバーからオートコンプリートの結果を取得する。このメソッド内で、buildURLメソッドを使用してリクエストの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
8章 おわりに
初めて学んだ知識を記事に書き起こしてみたのですが、実装している際に気づかなかったことに気づけたり、Stimulusに関する理解がより深まったと思うので書いてよかったなと思っています。
また、新たに機能を付け加える際には、便利なコンポーネントやライブラリがないか探すようにしようと思えるようにもなりました。この記事が皆さんのポートフォリオ作りのお役に立つことができれば幸いです。
9章 参考文献
https://zenn.dev/nagan/articles/e627918c192265
https://github.com/afcapel/stimulus-autocomplete
https://blog.to-ko-s.com/stimlus-introduce/