概要
今回は、逐次検索機能の実装について
カリキュラムをみながら自分で実装した時の理解度が低かったので、アウトプットの意味も込めて投稿しようと思います。
逐次検索機能とは、例えば「ruby」「python」「ruby on rails」というタグがすでにデータベースに存在する場合、rの文字が入力されると、rの文字と一致する「ruby」「ruby on rails」を候補として瞬時に画面上に表示する機能です。
一般的にインクリメンタルサーチと呼ばれます。
インクリメンタルサーチとは、
文字の入力の都度、自動的に検索が行われる検索機能です。
JSのAjaxを用いて実装します。
実装
jsファイルの記述
ターボリンクスをコメントアウトしてtag.jsを読み込めるようにします。
require("@rails/ujs").start()
// require("turbolinks").start()
require("@rails/activestorage").start()
require("channels")
require("../tag")
tag.jsを作成して編集します。
if (location.pathname.match("posts/new")){
window.addEventListener("load", (e) => {
const inputElement = document.getElementById("post_tag_name");
inputElement.addEventListener("keyup", (e) => {
const input = document.getElementById("post_tag_name").value;
const xhr = new XMLHttpRequest();
xhr.open("GET", `search/?input=${input}`, true);
xhr.responseType = "json";
xhr.send();
xhr.onload = () => {
const tagName = xhr.response.keyword;
const searchResult = document.getElementById('search-result')
searchResult.innerHTML = ''
tagName.forEach(function(tag){
const parentsElement = document.createElement('div')
const childElement = document.createElement('div')
parentsElement.setAttribute('id', 'parents')
childElement.setAttribute('id', tag.id )
childElement.setAttribute('class', 'child' )
parentsElement.appendChild(childElement)
childElement.innerHTML = tag.name
searchResult.appendChild(parentsElement)
const clickElement = document.getElementById(tag.id)
clickElement.addEventListener("click", () => {
document.getElementById("post_tag_name").value = clickElement.textContent;
clickElement.remove();
})
})
}
});
})
};
ここからは、コードの詳細を確認していきます。
const input = document.getElementById("post_tag_name").value;
const xhr = new XMLHttpRequest();
xhr.open("GET", `search/?input=${input}`, true);
xhr.responseType = "json";
xhr.send();
ここでは、post_tag_nameというID名の要素に入力された値を「input」に代入後、Ajaxの記述を行っています。
後の編集で、searchアクションと紐付けるルーティングを設定するので「openメソッド」でsearchアクションへのパスを設定します。この時、「input」に代入されたバリューをqueryパラメータとして設定します。また、レスポンスのデータ型は「json」と指定して、送信を行っています。
また、あとでsearchアクション(タグの検索)をコントローラーで行うための記述を行います。
keywordというキーに対応するバリューとしてセットして、jsonデータとして返す記述をあとでコントローラーにします。
const parentsElement = document.createElement('div')
const childElement = document.createElement('div')
ここでは、インクリメンタルサーチの結果を画面上に表示させるために、div要素を作成しています。
作成したdiv要素の中に、インクリメンタルサーチの結果を加えていきます。
parentsElement.setAttribute('id', 'parents')
childElement.setAttribute('id', tag.id )
childElement.setAttribute('class', 'child' )
先ほど作成したdiv要素にIDとクラス名を与えています。
childElementには、表示させるタグのIDを代入します。また、CSSを割り当てるためのクラス名も与えています。
parentsElement.appendChild(childElement)
childElement.innerHTML = tag.name
searchResult.appendChild(parentsElement)
この部分では、parentsElementの子要素としてchildElementを加えます。次にchildElementに表示させる、タグのHTMLを生成させます。最後に、searchResultの子要素にparentsElementを加えています。
const searchResult = document.getElementById('search-result')
searchResult.innerHTML = ''
二文字目以降に重複して表示されないように、searchResultの中へ空文字の代入をしています。
const clickElement = document.getElementById(tag.id)
clickElement.addEventListener("click", () => {
document.getElementById("post_tag_name").value = clickElement.textContent;
clickElement.remove();
候補として表示させたタグがクリックされると、選択されたタグのテキスト要素を入力フォームのバリューとしてセットします。最後に、選択されたタグは、表示の一覧から削除します。
以上がインクリメンタルサーチの一連の動きになります。
ルーティングを設定
resources :posts, only: [:index, :new, :create] do
collection do
get 'search'
end
end
コントローラーを編集
class PostsController < ApplicationController
def index
@posts = Post.all.order(created_at: :desc)
end
---省略---
def search
return nil if params[:input] == ""
tag = Tag.where(['name LIKE ?', "%#{params[:input]}%"])
render json:{ keyword: tag }
end
---省略---
end
これで実装完了です。動作確認をしてみましょう。
エラーが起きた時
今回の実装で考えうるエラーの解決方法は、getElementByIdをした時のId名に注目してみることです。
今回の実装では、formオブジェクトのpost_tagモデルについてインクリメンタルサーチを実装しました。
また、jsの全文をみた時の一行目にパスの記述があります。
今回は新規投稿をする際のタグ入力で逐次検索をする機能を実装したかったので、posts/newとなっています。
この二点以外はコピペでもいけるのではないでしょうか。
あ、ターボリンクス切り忘れとか、tag.js読み込み忘れは流石にやめましょう。
感想
javascriptはカリキュラムでも触れる機会が少なく、苦手意識があるので頑張ろうと思います。