ユーザー検索のインクリメンタルサーチ
グループ新規作成・編集画面において、ユーザーをインクリメンタルサーチできるように実装する。
インクリメンタルサーチは、以下の画像のように文字が入力されるごとに検索をかけ、結果を表示できる仕組みのこと。
インクリメンタルサーチ実装のステップ
・0. 作業用のブランチを作成する
・1. ルーティングなどAPI側の準備をする
・2. テキストフィールドを作成する
・3. テキストフィールドに入力されるたびにイベントが発火するようにする
・4. イベント時に非同期通信できるようにする
・5. 非同期通信の結果を得て、HTMLを作成する
・6. 5で作成したHTMLをビュー上に追加する
・7. エラー時の処理を行う
0.作業用のブランチを作成する
メッセージの非同期化の時と同様に、作業を始める前は必ず作業用のブランチを作成。
また、現在選択しているブランチが、インクリメンタルサーチのために作成したブランチになっているかどうか
も必ず確認する。
1.ルーティングなどAPI側の準備をする
-
routes.rbでusers#indexのルーティングを設定する。
今回はusersのindexを使う。 -
users_controller.rbにindexアクションを追加する。
-
JSON形式のレスポンスを返せるように、追加したindexアクションに対して、以下のようにコードを追記する。
1 2 3 4 5 6 |
def index
respond_to do |format|
format.html
format.json
end
end
|
これによって、サーバーはJSON形式で値を返し、jbuilderファイルが読み込まれるようになる。
4.app/views/usersディレクトリにindex.json.jbuilderファイルを作成する。
5. 作成したindex.json.jbuilderを以下の例に従って記述する。
1 2 3 4 |
1 2 3 4 5 6 7 8 9 10 11 12 |
Rails.application.routes.draw do
devise_for :users
root 'groups#index'
# :indexを追記
resources :users, only: [:index, :edit, :update]
resources :groups, only: [:new, :create, :edit, :update] do
resources :messages, only: [:index, :create]
namespace :api do
resources :messages, only: :index, defaults: { format: 'json' }
end
end
end
|
1 2 3 4 5 6 7 8 9 |
class UsersController < ApplicationController
def index
respond_to do |format|
format.html
format.json
end
end
~以下省略~
|
解説
users_controllerでは respond_to
メソッドを使用し、レスポンスをhtmlとjsonで条件分岐させる。
jsonがレスポンスとして返るので、jbuilderを生成しjsonを形成できるようにする。
2.テキストフィールドを作成する
インクリメンタルサーチでは、collection_checkboxes
は使わないので、こちらは消して、以下のように_form.html.haml
を書き換える。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
= form_for group do |f|
- if group.errors.any?
.chat-group-form__errors
%h2= "#{group.errors.full_messages.count}件のエラーが発生しました。"
%ul
- group.errors.full_messages.each do |message|
%li= message
.chat-group-form__field
.chat-group-form__field--left
= f.label :name, class: 'chat-group-form__label'
.chat-group-form__field--right
= f.text_field :name, class: 'chat__group_name chat-group-form__input', placeholder: 'グループ名を入力してください'
.chat-group-form__field
.chat-group-form__field--left
%label.chat-group-form__label{:for => "chat_group_チャットメンバーを追加"} チャットメンバーを追加
.chat-group-form__field--right
.chat-group-form__search.clearfix
%input#user-search-field.chat-group-form__input{:placeholder => "追加したいユーザー名を入力してください", :type => "text"}/
#user-search-result
.chat-group-form__field.clearfix
.chat-group-form__field--left
%label.chat-group-form__label{:for => "chat_group_チャットメンバー"} チャットメンバー
.chat-group-form__field--right
#chat-group-users.js-add-user
.chat-group-form__field.clearfix
.chat-group-form__field--left
.chat-group-form__field--right
= f.submit class: 'chat-group-form__action-btn'
|
3.テキストフィールドに入力されるたびにイベントが発火するようにする
調べること
- テキストフィールドに文字が入力されるたびにイベントを発火させるメソッド
- フォームの値を取得する時に使用するメソッド
実装すること
1.app/assets/javascriptsディレクトリにusers.jsファイルを作成。
2.テキストフィールドに文字が入力されるたびにイベントが呼び出されるように記述。
3.テキストフィールドに入力された文字を取得。
1 2 3 4 5 6 |
$(function(){
$(*****).on(*****, function(){
var input = ***** //フォームの値を取得して変数に代入する
})
});
|
確認すること
テキストフィールドに入力される度にイベントが呼ばれているかを、console.log
などを使用して確認する。
問題2:文字が入力されるたびにメソッドが実行され、入力された文字がコンソールに出力されるように実装する。
1 2 3 4 5 6 |
$(function() {
$("#user-search-field").on("keyup", function() {
let input = $("#user-search-field").val();
console.log(input);
});
});
|
解説
検索入力のタグである #user-search-field
を指定しjQueryオブジェクトを取得する。「テキストフィールドに文字が入力されるたびにイベントを発火させるメソッド」はkeyup
とし、入力するためにキーが押された後、上がるタイミングで処理が実行されるようにする。
処理には「フォームの値を取得する時に使用するメソッド」である valメソッド
を使用して値を取得し、コンソールに表示されるようなものにする。
4.イベント時に非同期通信できるようにする
調べること
- 曖昧(あいまい)な文字列の検索の方法
実装すること
キーが入力される度に非同期通信でユーザーを検索できるようにする。
1 2 3 4 5 6 |
ここで『リクエストの送信先が正しく設定できているか』と『paramsとしてコントローラーでデータが受け取れているか』をbinding.pry
で確認。
jsから渡されたデータを使って、コントローラで該当するユーザーを検索する。
1 2 3 4 5 |
def index
@users = User.where(*****)
~以下省略~
end
|
binding.pry
で@usersに正しく値が代入されているか確認する。
問題3:イベント時に非同期通信で入力したデータを送信する。サーバーでは入力した文字を含むユーザーを検索し、検索結果をjson形式で返すように実装する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
1 2 3 4 5 6 7 8 |
解答
ユーザー名を入力した値に対して、ajaxを使用してサーバーに送信できるようにする。通信が成功した場合、失敗した場合はどちらもconsole.logを仮置きしている。
jsのAjax関数の設定により、サーバーに入力した値が送られるので、適切なコントローラーで、値を含むデータをDBから取得するロジックを組む。
コードの処理としては、params[:keyword]に値が入っていればそのまま処理は続けられ、空だった場合はそこで処理が終わる。
検索処理の内容は、whereメソッドを使用し、入力された値を含むかつ、ログインしているユーザーのidは除外するという条件で取得している。
別解
見本コード
1 2 3 4 5 6 7 |
def index
@users = User.search(params[:keyword], current_user.id)
respond_to do |format|
format.html
format.json
end
end
|
1 2 3 4 5 6 7 8 |
~省略~
def self.search(input, id)
return nil if input == ""
User.where(['name LIKE ?', "%#{input}%"] ).where.not(id: id).limit(10)
end
~省略~
|
解答
controllerの記述を一部modelに記述することで可読性をあげている。
検索の処理自体はmodelに記述しておき、コントローラーで呼び出して使用する形にしている。
処理の流れとしてはcontrollerだけで実行していた時と変わらない。
5.非同期通信の結果を得て、HTMLを作成する
調べること
実装すること
非同期通信の結果をdone関数で受取り、ビューに追加するためのHTMLを作成する必要がある。
- done関数の処理
- 条件分岐(引数に値がはいっていなかった場合とそうでない時で分岐 )
- イベントが発火するたびに、すでに検索結果欄で出力されている投稿情報を
emptyメソッド
を使用して削除する(これを実装しないと、一つ前の検索結果が残り続けてしまう) - done関数で受け取った配列型の引数の値1つ1つに対して、処理を行えるようにする
1 2 3 4 5 |
.done(function(引数){
//emptyメソッドで一度検索結果を空にする
//usersが空かどうかで条件分岐
//配列オブジェクト1つ1つに対する処理
})
|
- 引数に値が入っていた場合に、ビューに追加するためのHTMLを作成
ユーザー名、data-user-nameとdata-user-idにそれぞれDBから取得した情報が表示されるように記述する。
1 2 3 4 5 6 7 8 |
function 関数名(引数){
var html = `
<div class="chat-group-user clearfix">
<p class="chat-group-user__name">ユーザー名</p>
<div class="user-search-add chat-group-user__btn chat-group-user__btn--add" data-user-id="ユーザーのid" data-user-name="ユーザー名">追加</div>
</div>
`
}
|
- 引数に値が入っていなかった場合に、ビューに追加するためのHTMLを作成
1 2 3 4 5 6 |
function 関数名(引数){
var html = `
<div class="chat-group-user clearfix">
<p class="chat-group-user__name">ユーザーが見つかりません</p>
</div>`
}
|
確認すること
Ajaxの通信の処理が正しく行われ、値がdoneで引数として受け取れているかどうかを、console.log
で確かめる。
5~7の見本コードについては、「7. エラー時の処理を行う」にてまとめている。
6.作成したHTMLをビュー上に描画する
調べること
- 定義した関数を呼び出す方法
- appendの使い方
実装すること
『非同期通信の結果を得て、HTMLを作成する』でHTMLを組み立てることができたら、append()を用いてビューに追加する。
- 引数に値が入っていた場合
1 2 3 4 5 6 7 8 9 |
function 関数名(引数){
var html = `
<div class="chat-group-user clearfix">
<p class="chat-group-user__name">ユーザー名</p>
<div class="user-search-add chat-group-user__btn chat-group-user__btn--add" data-user-id="ユーザーのid" data-user-name="ユーザー名">追加</div>
</div>
`
$(*****).append(html)
}
|
- 引数に値が入っていなかった場合
1 2 3 4 5 6 7 8 |
function 関数名(引数){
var html = `
<div class="chat-group-user clearfix">
<p class="chat-group-user__name">ユーザーが見つかりません</p>
</div>
`
$(*****).append(html)
}
|
関数を定義したら、それぞれ所定の箇所で呼び出す。
確認すること
console.log(html)
で表示されるhtmlが意図するものになっているか、確認。
特に以下の画像のように定義した変数に値が入っているかどうかも確認。
5~7の見本コードについては、「7. エラー時の処理を行う」にてまとめている。
7.エラー時の処理を行う
通信に失敗した場合の処理を実装する。
アラートで「ユーザー検索に失敗しました」と表示ができれば十分。
1 2 3 |
.fail(function() {
alert("ユーザー検索に失敗しました");
})
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
$(function() {
function addUser(user) {
let html = `
<div class="chat-group-user clearfix">
<p class="chat-group-user__name">${user.name}</p>
<div class="user-search-add chat-group-user__btn chat-group-user__btn--add" data-user-id="${user.id}" data-user-name="${user.name}">追加</div>
</div>
`;
$("#user-search-result").append(html);
}
function addNoUser() {
let html = `
<div class="chat-group-user clearfix">
<p class="chat-group-user__name">ユーザーが見つかりません</p>
</div>
`;
$("#user-search-result").append(html);
}
$("#user-search-field").on("keyup", function() {
let input = $("#user-search-field").val();
$.ajax({
type: "GET",
url: "/users",
data: { keyword: input },
dataType: "json"
})
.done(function(users) {
$("#user-search-result").empty();
if (users.length !== 0) {
users.forEach(function(user) {
addUser(user);
});
} else if (input.length == 0) {
return false;
} else {
addNoUser();
}
})
.fail(function() {
alert("通信エラーです。ユーザーが表示できません。");
});
});
});
|
解答
検索結果に応じて、検索結果が該当する場合と、しない場合で条件分岐をする。
検索結果は配列に格納されるので、lengthメソッドを使用して条件分岐が可能。
返り値がない場合はreturn falseと記述し、返り値がない事を伝えることで可読性が向上することが期待できる。
検索結果が該当ありの場合は、forEach文を使用しユーザーごとHTMLを描画していく。
addUserという関数を定義し、HTMLを用意する。${}
を使用する事でテンプレートリテラル内で式展開が可能。これを使用してユーザーの名前などを表示できるよう実装していく。
最後に、appendメソッド
を使用してHTMLを描画するように記述する。
インクリメンタルサーチの後について
インクリメンタルサーチを実装できても終わりではない。
該当するユーザーの中から追加したいユーザーを選択し、DBに保存されるようにする必要がある。
つまり、ユーザー検索結果として一覧される段階までは実装できたので、次は『追加ボタンを押される→チャットメンバー欄に追加される→更新するボタンを押すとサーバー側に送られる」という流れで実装していくことになる。
インクリメンタルサーチ後の実装のステップ
- 追加ボタンが押された時にイベントが発火するようにする
- 追加ボタンをクリックされたユーザーの名前を、チャットメンバーの部分に追加し、検索結果からは消す
- 削除を押すと、チャットメンバーから削除される
- ログイン中のユーザーを追加済みの状態にする
- 編集画面では既存のユーザーが追加済みの状態であることを確認する
見本コードについては、「5. 編集画面では既存のユーザーが追加済みの状態であることを確認する」にてまとめている。
1.追加ボタンが押された時にイベントが発火するようにする
調べること
- $()の意味
実装すること
- 追加ボタンが押された時にclickイベントが発火するようにする。 後から追加された要素に対してイベントを設定するには以下のように記述する。
1 2 3 4 5 |
$(調査範囲).on('イベント', *****, function(){
//console.log()でイベント発火の有無を確認しましょう
});
|
調査範囲には ***** の親要素で、ページ読み込み時から存在するものを選ぶ。
確認すること
console.logを用いて以下のように、console.logが読み込まれているか必ず確認する。
もし正しい記述ができているにも関わらずconsole.logが読み込まれない場合は、クラス名もしくはid名に誤りがないかや『#』もしくは『.』が抜けていないかを確認する。
2.追加ボタンをクリックされたユーザーの名前を、チャットメンバーの部分に追加し、検索結果からは消す
調べること
- カスタムデータ属性を取得する方法
- thisの使い方
- 取得した要素(DoM)の親要素を指定する方法
実装すること
-
「追加」ボタンがクリックされたユーザーが、検索結果一覧から消える
どのユーザーのhtmlかを特定するためにdata-user-id
とdata-user-name
を取得する
イベントが発生した要素を取得し、その親要素を削除する。
今イベントが発生している追加ボタンのaタグを起点に、その親要素のchat-group-userを削除する。 -
「追加」ボタンがクリックされたユーザーが、チャットメンバーの方に追加される。
以下のコードをもとに、追加する(appendする)要素を作成する。
1 2 3 4 5 6 7 8 9 10 |
function 関数名(引数1(userのname), 引数2(userのid)){
var html = `
<div class='chat-group-user'>
<input name='group[user_ids][]' type='hidden' value='ユーザーのid'> //この記述によりuserがDBに保存される
<p class='chat-group-user__name'>ユーザー名</p>
<div class='user-search-remove chat-group-user__btn chat-group-user__btn--remove js-remove-btn'>削除</div>
</div>
`
$(*****).append(html)
}
|
※上記のままだと、再度検索を行うと追加済みユーザーが検索されてしまう。これを避けるようにする実装は、ChatSpaceの全ての実装が終わった後、余裕があれば・・・。
確認すること
更新するボタンを押した段階で、group[user_ids][]にクリックしたユーザーのidが入っているかどうかbinding.pry
で確認。
3.削除を押すと、チャットメンバーから削除される
追加ボタンの時と同様に、削除を押したらビューからユーザー名を表示しているDOMを取り除く実装をする。
4.ログイン中のユーザーを追加済みの状態にする
以下のように_form.html.hamlを編集。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
= form_for group do |f|
- if group.errors.any?
.chat-group-form__errors
%h2= "#{group.errors.full_messages.count}件のエラーが発生しました。"
%ul
- group.errors.full_messages.each do |message|
%li= message
.chat-group-form__field
.chat-group-form__field--left
= f.label :name, class: 'chat-group-form__label'
.chat-group-form__field--right
= f.text_field :name, class: 'chat__group_name chat-group-form__input', placeholder: 'グループ名を入力してください'
.chat-group-form__field.clearfix
.chat-group-form__field--left
%label.chat-group-form__label{:for => "chat_group_チャットメンバーを追加"} チャットメンバーを追加
.chat-group-form__field--right
.chat-group-form__search.clearfix
%input#user-search-field.chat-group-form__input{:placeholder => "追加したいユーザー名を入力してください", :type => "text"}/
#user-search-result
.chat-group-form__field.clearfix
.chat-group-form__field--left
%label.chat-group-form__label{:for => "chat_group_チャットメンバー"} チャットメンバー
.chat-group-form__field--right
#chat-group-users.js-add-user
.chat-group-user.clearfix.js-chat-member#chat-group-user-8
%input{name: "group[user_ids][]", type: "hidden", value: current_user.id}
%p.chat-group-user__name= current_user.name
.chat-group-form__field.clearfix
.chat-group-form__field--left
.chat-group-form__field--right
= f.submit class: 'chat-group-form__action-btn'
|
確認すること
更新するボタンを押した段階で、group[user_ids][]にログインしているユーザーのid(current_user.id)が入っているかどうかbinding.pry
で確認。
もし、current_userのidがgroup作成時にuser_ids[]に含まれていない場合、side_barに作成した新しいgroupが表示されないので、必ず確認。
上記の理由は、サイドバーのhamlの記述(each文の箇所)を見直していただければわかる。
5.編集画面では既存のユーザーが追加済みの状態であることを確認する
編集画面にアクセスした時に、既にグループに追加されているユーザーは、最初から追加済みの状態であるようにする。
良い例
既存ユーザー(testさん、testくん)は編集画面においても、すでにチャットメンバーとして存在している。
悪い例
既存ユーザー(testくん)が編集画面に遷移すると、チャットメンバーからは外されてしまっている。このまま「更新する」ボタンをクリックすると、testくんはチャットメンバーから外されたまま更新されてしまう。
ヒント
- DBにはすでにuserは保存されている。
- each文を使う。
- 上記の方法だとcurrent_userも削除できる状態になってしまうためcurrent_userは別で表示させる
問題5:追加ボタンをクリックされたユーザーの名前をチャットメンバーの部分に追加し、検索結果からは消すようなロジックを実装する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
= form_for group do |f|
- if group.errors.any?
.chat-group-form__errors
%h2= "#{group.errors.full_messages.count}件のエラーが発生しました。"
%ul
- group.errors.full_messages.each do |message|
%li= message
.chat-group-form__field
.chat-group-form__field--left
= f.label :name, class: 'chat-group-form__label'
.chat-group-form__field--right
= f.text_field :name, class: 'chat__group_name chat-group-form__input', placeholder: 'グループ名を入力してください'
.chat-group-form__field.clearfix
.chat-group-form__field--left
%label.chat-group-form__label{:for => "chat_group_チャットメンバーを追加"} チャットメンバーを追加
.chat-group-form__field--right
.chat-group-form__search.clearfix
%input#user-search-field.chat-group-form__input{:placeholder => "追加したいユーザー名を入力してください", :type => "text"}/
#user-search-result
.chat-group-form__field.clearfix
.chat-group-form__field--left
%label.chat-group-form__label{:for => "chat_group_チャットメンバー"} チャットメンバー
.chat-group-form__field--right
#chat-group-users.js-add-user
.chat-group-user.clearfix.js-chat-member
%input{name: "group[user_ids][]", type: "hidden", value: current_user.id}
%p.chat-group-user__name= current_user.name
- group.users.each do |user|
- if current_user.name != user.name
.chat-group-user.clearfix.js-chat-member
%input{name: "group[user_ids][]", type: "hidden", value: user.id}
%p.chat-group-user__name
= user.name
%a.user-search-remove.chat-group-user__btn.chat-group-user__btn--remove.js-remove-btn
削除
.chat-group-form__field.clearfix
.chat-group-form__field--left
.chat-group-form__field--right
= f.submit class: 'chat-group-form__action-btn'
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
$(function() {
function addUser(user) {
let html = `
<div class="chat-group-user clearfix">
<p class="chat-group-user__name">${user.name}</p>
<div class="user-search-add chat-group-user__btn chat-group-user__btn--add" data-user-id="${user.id}" data-user-name="${user.name}">追加</div>
</div>
`;
$("#user-search-result").append(html);
}
function addNoUser() {
let html = `
<div class="chat-group-user clearfix">
<p class="chat-group-user__name">ユーザーが見つかりません</p>
</div>
`;
$("#user-search-result").append(html);
}
function addDeleteUser(name, id) {
let html = `
<div class="chat-group-user clearfix" id="${id}">
<p class="chat-group-user__name">${name}</p>
<div class="user-search-remove chat-group-user__btn chat-group-user__btn--remove js-remove-btn" data-user-id="${id}" data-user-name="${name}">削除</div>
</div>`;
$(".js-add-user").append(html);
}
function addMember(userId) {
let html = `<input value="${userId}" name="group[user_ids][]" type="hidden" id="group_user_ids_${userId}" />`;
$(`#${userId}`).append(html);
}
$("#user-search-field").on("keyup", function() {
let input = $("#user-search-field").val();
$.ajax({
type: "GET",
url: "/users",
data: { keyword: input },
dataType: "json"
})
.done(function(users) {
$("#user-search-result").empty();
if (users.length !== 0) {
users.forEach(function(user) {
addUser(user);
});
} else if (input.length == 0) {
return false;
} else {
addNoUser();
}
})
.fail(function() {
alert("通信エラーです。ユーザーが表示できません。");
});
});
$(document).on("click", ".chat-group-user__btn--add", function() {
console.log
const userName = $(this).attr("data-user-name");
const userId = $(this).attr("data-user-id");
$(this)
.parent()
.remove();
addDeleteUser(userName, userId);
addMember(userId);
});
$(document).on("click", ".chat-group-user__btn--remove", function() {
$(this)
.parent()
.remove();
});
});
|
解説
追加ボタンが押された際に処理が実行されるようにする。
$(document).on
することで常に最新のHTMLの情報を取得することができる。
$(document).onを用いることで、Ajax通信で作成されたHTMLの情報を取得することができる。
今回だとappendさせて作成したHTMLから情報を取得する際、documentを用いることで値の取得を可能にしている。
追加ボタンの対象であるユーザー情報を変数へ代入し、HTMLを描画する。その際、検索結果欄からメンバー欄へ移動するように見せるために検索結果欄からremoveメソッド
を使用してHTMLは削除する。
ユーザーの追加や削除の情報は addMember
というメソッドを作成してコントロールしている。ここではメンバーを追加する際に情報を user_ids
へ追加できるような仕組みを作り、削除ボタンを押すと同時にその情報も削除されるように実装している。