実装デモ
機能説明
予約に対して、メンバーを複数アサインする機能。
Reservationと、Memberが多対多でアソシエーションされている。
メンバーのアサインを複数、かつ、入力補完(検索)しながらインタラクティブに選択肢を表示させる。
リポジトリはこちら
環境
$ ruby -v
=> ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-linux]
$ rails -v
=> Rails 7.0.3.1
TailwindCSSをつかって実装していきます。
アプリケーションの作成
$ rails new myapp --css tailwind
$ cd myapp
$ rails g scaffold Reservation title:string date:date
$ rails db:migrate
rootを設定
routes.rb
root "reservations#index" #追記
サーバー起動
$ bin/dev
Memberモデルと中間モデルの作成
$ rails g model Member name:string
$ rails g model Attendee member:references reservation:references
$ rails db:migrate
アソシエーションの設定
reservation.rb
class Reservation < ApplicationRecord
has_many :attendees, dependent: :destroy
has_many :attended_members, through: :attendees, source: :member
end
seedの設定
メンバーの名前を入れる
今回はこちらのサイトから生成された名前を使用
長いので折りたたみ(クリックで表示)
seed.rb
Member.create!([
{name: "鈴木 綾子"},
{name: "高橋 めぐみ"},
{name: "田中 朋子"},
{name: "鈴木 和美"},
{name: "佐々木 真由美"},
{name: "小林 真弓"},
{name: "中村 千恵"},
{name: "藤井 舞"},
{name: "伊藤 千明"},
{name: "松本 亜美"},
{name: "鈴木 則子"},
{name: "野口 瞳"},
{name: "清水 弘子"},
{name: "西田 由美"},
{name: "増田 紀子"},
{name: "中山 晶子"},
{name: "関 恭子"},
{name: "藤澤 恵"},
{name: "斎藤 葵"},
{name: "後藤 文子"},
{name: "古田 仁美"},
{name: "荒木 あかね"},
{name: "渡辺 千代"},
{name: "武田 絵梨"},
{name: "星 博子"},
{name: "松尾 麻実"},
{name: "鷲尾 智子"},
{name: "弓削 久美子"},
{name: "野口 真貴子"},
{name: "坂口 洋美 "},
{name: "矢野 直樹"},
{name: "佐藤 創"},
{name: "伊藤 俊樹"},
{name: "小林 勇介"},
{name: "木下 祐介"},
{name: "野田 祐介"},
{name: "永井 真一"},
{name: "長谷川 真一郎"},
{name: "福島 渉"},
{name: "堤 和彦"},
{name: "古川 弘樹"},
{name: "大西 昌弘"},
{name: "杉浦 裕"},
{name: "佐藤 洋二郎"},
{name: "鈴木 昭徳"},
{name: "野中 薫"},
{name: "武内 浩二"},
{name: "山本 暁"},
{name: "村田 剛志"},
{name: "佐藤 三千夫"},
{name: "渡邉 竜"},
{name: "高橋 寛司"},
{name: "中村 紘一郎"},
{name: "石原 剛史"},
{name: "宮崎 崇史"},
{name: "野口 優"},
{name: "谷川 淳"},
{name: "白石 雄大"},
{name: "堀 正己"},
{name: "山崎 広一 "},
])
$ rails db:seed
新規登録時にメンバーを選択させるセレクトボックスを表示させる
/views/reservations/_form.html.erb
<div class="my-5">
<select id="select" >
<% Member.all.each do |m| %>
<option value=<%= m.id%>><%= m.name %></option>
<% end %>
</select>
</div>
JSを読み込ませる
※以下はRails7でのやり方です。Rials6以前をお使いの場合は、JSの読み込み方法が異なるので、気を付けてください。
JSファイルを新規作成
app/javascript/attendee.js
console.log("test")
下記を最後に追記
config/importmap.rb
pin "attendee"
下記を最後に追加
views/reservations/_form.html.erb
<%= javascript_import_module_tag "attendee" %>
再度、サーバーを立て直す
$ bin/dev
以下のように検証ページのconsoleに"test"が出てくればOK
選択肢をカスタマイズ
ここが大きなコードになります。詳しくはコードのコメントを確認してください。
attendee.js
// 大まかな流れ
// セレクトボックスの値から、idと名前をもってきて、クリックできるdivをつくる
// クリックすると、idをinputに追加する
// 同時に、表示用のdivにも名前を追加する
// 検索用のボックスを作成して、そこの入力を検知
// 入力された値により、セレクトボックスの値を検索してクリックできるdivを再生成
// 編集時には、セレクトされている状態にする
window.addEventListener('load', function() {
const selectableOptions = document.getElementById("selectable-options")
function addSelectableOptions(keyword){//選択肢を追加する関数
while(selectableOptions.firstChild){
selectableOptions.removeChild(selectableOptions.firstChild);
}
let opsions = document.getElementById("select").children
let attendees_field = document.getElementById("attendees")
for(let i=0; i < opsions.length; i++){
let append_selectable_option
append_selectable_option = document.createElement("div")
append_selectable_option.innerHTML = opsions[i].innerHTML //名前を要素に追加
append_selectable_option.setAttribute('data-id', opsions[i].value)//IDをdata属性に追加
append_selectable_option.onclick = function() {//クリックしたときに動く関数
let selected_member_id = append_selectable_option.getAttribute('data-id')
let selected_member_name = append_selectable_option.innerHTML
if (append_selectable_option.classList.contains("is_selected")){//is_selectedが含まれていたら
let selected_options_arry = attendees_field.value.split(",")// 選択されているIDを配列に入れて
let removed_arry = selected_options_arry.filter(e =>{
return Number(e) != Number(selected_member_id)
})//配列の中から自身のIDを削除
attendees_field.value = removed_arry.join(",")// inputに追加
let selected_names_arry = selected_member_names.innerHTML.split(",")// 選択されている名前を配列に入れて
let removed_names_arry = selected_names_arry.filter(e =>{
return e != selected_member_name
})//配列の中から自身の名前を削除
selected_member_names.innerHTML = removed_names_arry.join(",")// inputに追加
}else{
attendees_field.value += selected_member_id + ","
selected_member_names.innerHTML += selected_member_name + ","
}
append_selectable_option.classList.toggle('is_selected')
};
append_selectable_option.classList.add(
"cursor-pointer",
"hover:bg-gray-200",
"border-b-2",
"border-gray-200",
"p-2",
)
if (attendees_field.value.split(",").includes(String(append_selectable_option.getAttribute('data-id')))){//既に選択されている場合は、is_selectedを追加
append_selectable_option.classList.add("is_selected")
}
if (opsions[i].innerHTML.indexOf(keyword) != -1 ){//ユーザーが入力した値が含まれている要素のみを追加する
selectableOptions.appendChild(append_selectable_option)
}
}
}
serch_box.addEventListener("input",function(){//ユーザーの入力ごとに実行される関数
if (this.value != ""){
addSelectableOptions(this.value)
}
});
function setDefaultValueInEdit () {//編集時に値をセットする
let attendees_field = document.getElementById("attendees")
let opsions = document.getElementById("select").children
let selected_options_arry = attendees_field.value.split(",")// 選択されているIDを配列に入れて
for(let i=0; i < opsions.length; i++){
if (selected_options_arry.includes(String(opsions[i].value))){
selected_member_names.innerHTML += opsions[i].innerHTML + ","
}
}
}
setDefaultValueInEdit()
})
ビューページも編集します。
_form.html.erb
<div class="my-5">
<div id="selected_member_names"></div>
<input type="text" id="serch_box" placeholder="名前で検索" class="block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full">
<div id="selectable-options"></div>
<select id="select" class="hidden">
<% Member.all.each do |m| %>
<option value=<%= m.id%>><%= m.name %></option>
<% end %>
</select>
<%= text_field_tag :attendees, @reservation.attendees.pluck(:member_id).join(",") + ",", class:"hidden" %>
</div>
tailwindもカスタマイズします。
assets/stylesheets/application.tailwind.css
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.is_selected {
@apply bg-blue-200;
}
}
turboがあるとうまくJSが動作しないのでオフにします
app/javascript/application.js
// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
import "@hotwired/turbo-rails"
import "controllers"
Turbo.session.drive = false;
以下のように表示されれば問題ありません。
データベースへ保存
入力された値は、パラメーターのattendeesにはいっているのでその値でデータベースへ保存してあげます。
def create
@reservation = Reservation.new(reservation_params)
respond_to do |format|
if @reservation.save
format.html { redirect_to reservation_url(@reservation), notice: "Reservation was successfully created." }
format.json { render :show, status: :created, location: @reservation }
params[:attendees].split(",").each do |attendee|
Attendee.create(member_id: attendee, reservation_id: @reservation.id)
end
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @reservation.errors, status: :unprocessable_entity }
end
end
end
# 省略
def update
respond_to do |format|
if @reservation.update(reservation_params)
format.html { redirect_to reservation_url(@reservation), notice: "Reservation was successfully updated." }
format.json { render :show, status: :ok, location: @reservation }
@reservation.attendees.destroy_all
params[:attendees].split(",").each do |attendee|
Attendee.create(member_id: attendee, reservation_id: @reservation.id)
end
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @reservation.errors, status: :unprocessable_entity }
end
end
end
ビューページで参加者を表示
views/reservations/_reservation.html.erb
<p class="my-5">
<strong class="block font-medium mb-1">members:</strong>
<% reservation.attended_members.each do |m| %>
<%= m.name %>,
<% end %>
</p>
以上で完成です。