Railsで、データベースから取得した値を使ってフォームの自動入力補完機能を実装する方法についてまとめたメモ書きです。
イメージはこんな感じです↓
データベースに保存されたアドレス帳(個人情報)をviewに呼び出し、氏名のみをセレクトボックスに表示。
セレクトボックスで特定の氏名を選択すると、フォームの入力欄にアドレス帳に登録した情報が自動で補完されるという流れです。
これらの処理をJSで行いますが、そのためにはJS側にアドレス帳の情報(JSONデータに変換)を受け渡す必要があります。
今回は、その方法について具体例を交えて解説していきます。
(Rails7からhotwireが標準装備となったことから、せっかくなのでStimulusを使って実装してみました)
開発環境
- Ruby 3.1.2
- Ruby on Rails 7.0.3
- Bootstrap 5.1.3
- jsbundling-rails (1.0.3)
- cssbundling-rails (1.1.1)
- M1 Macbook Air 2020
- mac OS Monterey (ver. 12.4)
- ターミナル bash (Rosetta 2 使用)
JS(Stimulus)でフォームの自動入力補完機能を実装する流れ
先ほどの自動入力補完機能を実装する大まかな流れは以下の通りです。
- DB取得データをJSONに変換し、HTML要素の
data属性
に設定(viewに埋め込む) - js側(stimulus controller)で設定しておいた要素の
data属性
の値を取得
今回はRails7でデフォルトとなったStimulus(JavaScriptで書かれたクライアントサイドのライブラリ)を利用してJS側の実装を行っていきます。
以下、動作の流れです↓
- 【前提条件】アドレス帳(データベース)にはすでに個人情報が登録してあるものとする(userモデルに紐付いたaddress_booksテーブル)
- データベースに登録したアドレス帳を取得
- 個人情報入力フォームに取得したアドレス帳の氏名のみをプルダウンで表示
- 該当する氏名を選択すると、取得したアドレス帳の個人情報を自動でフォームに入力(補完)する
- 「選択なし」を選択するとフォームがリセットされる
それでは、一つずつ進めていきましょう。
Stimulus を導入する
以下のコマンドを実行してstimulusコントローラーを追加します。
当記事ではコントローラー名を“user”として話を進めます。
$ rails g stimulus コントローラー名
例) $ rails g stimulus user
入力フォームを作成する
以下を参考に入力フォームを作成します。
(cssはbootstrap5を使用しています。一部カスタマイズあり)
= form_with(model: @user, local: true) do |f|
#------- セレクトボックス --------
= f.label "アドレス帳から追加", for: "selectAddresses", class: "form-label"
= f.select :selected_id,
@user.address_books.map { |address_book| [address_book.full_name, address_book.id]},
{ include_blank: "選択なし" },
{ class: "form-select mb-2",
id: "selectAddresses" }
#------- 氏名 --------
= f.label "氏名", class: "form-label"
= f.text_field :full_name, class: "form-control"
#------- 生年月日 --------
= f.label "生年月日", class: "form-label"
= f.date_select :birthday,
{use_month_numbers: true,
start_year: 1950,
end_year: (Time.now.year - 10),
default: Date.new(1990, 1, 1),
date_separator: '/ '},
{class: "form__custom"}
#------- 年齢 --------
= f.label "年齢", class: "form-label"
= f.number_field :age, class: "form-control"
#------- 性別 --------
= f.label "性別", class: "form-label"
= f.radio_button :gender, "男" , class: "form-check-input", id: "genderMale", checked: true
= f.label "男性", for: "genderMale"
= f.radio_button :gender, "女" , class: "form-check-input", id: "genderFemale"
= f.label "女性", for: "genderFemale"
#------- 住所 --------
= f.label "住所", class: "form-label"
= f.text_field :address, class: "form-control"
#------- 電話番号 --------
= f.label "電話番号", class: "form-label"
= f.text_field :phone_number, class: "form-control"
イベント発火に必要なdata属性【controller】【target】【action】をフォーム(html.erb)に組み込む
stimulusコントローラーに作成したアクション(自動入力補完)を実行させるための前準備として、先ほど作成した入力フォームに、data属性
を設定します。
設定するdata属性は以下の3つです。
- data-controller
- data-action
- data-(コントローラー名)-target
まず、data-controller
でStimulusコントローラーの適用範囲を設定します。
続いて、data-action
でアクションを実行する条件を設定します。
例えば、ボタンをクリックした時にuserコントローラーのsomeActionを実行したい場合は以下のように書きます。
<button data-action="click->user#someAction"></button>
最後に、Stimulusコントローラー側で取得したいHTML要素(input)をdata-(コントローラー名)-target
で指定します。
StimulusではHTML側にdata-(コントローラー名)-target="{name}"
属性を付与し、値に設定した{name}
をJS側の静的プロパティstatic
に設定すると{name}Target
というgetterが生成され、アクセスできるようになります。
上記を踏まえて、data属性
を以下のように設定します。
#------- userコントローラ(stimulus)の適用範囲を data-controller="user" で囲う --------
= form_with(model: @user, local: true, data: { controller: "user"}) do |f|
#------- 取得したい要素に data-user-target="任意のターゲット名" を指定する --------
#------- イベント発火時に実行したいアクションを data-action="任意のイベント" で指定する --------
= f.label "アドレス帳から追加", for: "selectAddresses", class: "form-label"
= f.select :selected_id,
@user.address_books.map { |address_book| [address_book.full_name, address_book.id]},
{ include_blank: "選択なし" },
{ class: "form-select mb-2",
id: "selectAddresses",
#------- data-user-target="select_address" を指定 --------
data: { user_target: "select_addresses",
#------- "change" イベント発火時に userコントローラーの selectAddressBook アクションを実行 --------
action: "change->user#selectAddressBook" }
}
#------- data-user-target="full_name" を指定 --------
= f.label "氏名", class: "form-label"
= f.text_field :full_name, class: "form-control", data: { user_target: "full_name" }
#------- data-user-target="birthday" を指定 --------
= f.label "生年月日", class: "form-label"
<div data-user-target="birthday">
= f.date_select :birthday,
{use_month_numbers: true,
start_year: 1950,
end_year: (Time.now.year - 10),
default: Date.new(1990, 1, 1),
date_separator: '/ '},
{class: "form__custom"}
</div>
#------- data-user-target="age" を指定 --------
= f.label "年齢", class: "form-label"
= f.number_field :age, class: "form-control", data: { user_target: "age" }
#------- data-user-target="gender" を指定 --------
<div data-user-target="gender">
= f.label "性別", class: "form-label"
= f.radio_button :gender, "男" , class: "form-check-input", id: "genderMale", checked: true
= f.label "男性", for: "genderMale"
= f.radio_button :gender, "女" , class: "form-check-input", id: "genderFemale"
= f.label "女性", for: "genderFemale"
</div>
#------- data-user-target="address" を指定 --------
= f.label "住所", class: "form-label"
= f.text_field :address, class: "form-control", data: { user_target: "address" }
#------- data-user-target="phone_number" を指定 --------
= f.label "電話番号", class: "form-label"
= f.text_field :phone_number, class: "form-control", data: { user_target: "phone_number" }
また、Stimulusコントローラー側の静的プロパティも設定しておきましょう。
import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="user"
export default class extends Controller {
// 取得したい要素の静的プロパティ(フォーム上の data-user-target="" で指定した名称)
static targets = [
"full_name",
"birthday",
"age",
"gender",
"address",
"phone_number",
"select_addresses"
]
}
フォーム上で取得したデータベースの値(アドレス帳情報)をJSONデータに変換する
続いて、フォーム上でインスタンス変数@user
から取得したアドレス帳情報@user.address_books
を、data-json
でJSONデータに変換してview上に埋め込みます。
data-json属性に指定した値は、JSONデータへ変換するために.to_json
メソッドを付け加えます。
= form_with(model: @user, local: true, data: { controller: "user"}) do |f|
#------- data-json にJSに渡したいデータを指定する( .to_json でJSONデータに変換 ) --------
= f.label "アドレス帳から追加", for: "selectAddresses", class: "form-label"
= f.select :selected_id,
@user.address_books.map { |address_book| [address_book.full_name, address_book.id]},
{ include_blank: "選択なし" },
{ class: "form-select mb-2",
id: "selectAddresses",
data: { user_target: "select_addresses",
action: "change->user#selectAddressBook",
# アドレス帳情報を .to_json でJSONデータに変換
json: "#{@user.address_books.to_json if @user.address_books.present?}" }
}
・・・
JSONデータに変換すると、フォームの要素上にJSON形式のアドレス帳情報(非表示)が埋め込まれます↓
StimulusコントローラーにJSONデータを受け渡す
Stimulusコントローラー側でJSONデータ(アドレス帳情報)を格納するための定数addressBooks
を用意し、以下のように記述してJSONデータの値を受け取りましょう。
const addressBooks = JSON.parse(this.select_addressesTarget.dataset.json)
Stimulusコントローラー全体のコードは以下の通りです。
import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="user"
export default class extends Controller {
// 取得したい要素のターゲット名(フォーム上の data-user-target="" で指定した名称)
static targets = [
"full_name",
"birthday",
"age",
"gender",
"address",
"phone_number",
"select_addresses"
]
// イベント発生時に実行するアクション
selectAddressBook(){
// アドレス帳のidを取得
const selectedValue = this.select_addressesTarget.value
// JSONデータに変換されたアドレス帳情報をフォーム(html.erb)から取得
const addressBooks = JSON.parse(this.select_addressesTarget.dataset.json)
}
}
ちなみに、console.log(addressBooks)
で受け取ったJSONデータを表示すると以下のようになります。
これで、JS側からJSONデータにアクセスできるようになりました。
Stimulusコントローラーにイベント発火時のアクションを設定する
最後に、イベント発火時(change->user#selectAddressBook)のアクションをStimulusコントローラーに記述していきます。
import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="user"
export default class extends Controller {
// 取得したい要素のターゲット名(フォーム上の data-user-target="" で指定した名称)
static targets = [
"full_name",
"birthday",
"age",
"gender",
"address",
"phone_number",
"select_addresses"
]
// イベント発生時に実行するアクション
selectAddressBook(){
// アドレス帳のidを取得
const selectedValue = this.select_addressesTarget.value
// JSONデータに変換されたアドレス帳情報をターゲット(select_addresses)から取得
const addressBooks = JSON.parse(this.select_addressesTarget.dataset.json)
// 各フォームの要素を格納
const fullNameForm = this.full_nameTarget // 氏名
const birthdayForm = this.birthdayTarget.querySelectorAll("select") // 誕生日
const defaultBirthday = ["1990", "1", "1"] // デフォルトの誕生日
const ageForm = this.ageTarget // 年齢
const genderForm = this.genderTarget.querySelectorAll("input") // 性別
const addressForm = this.addressTarget // 住所
const phoneNumberForm = this.phone_numberTarget // 電話番号
// セレクトボックスから選択した氏名(アドレス帳id)の個人情報をフォームに入力する
addressBooks.forEach((addressBook, index) => {
// セレクトボックスから選択したアドレス帳idが addressBooksのidと一致した場合
if(Number(addressBook.id) === Number(selectedValue)){
// 氏名
fullNameForm.value = addressBook.full_name
// 生年月日
for(let i=0; i<birthdayForm.length; i++){
birthdayForm[i].value = Number(addressBook.birthday.split("-")[i])
}
// 年齢
ageForm.value = addressBook.age
// 性別
if(addressBook.gender === "男"){
genderForm[0].checked = true
}else{
genderForm[1].checked = true
}
// 住所
addressForm.value = addressBook.address
// 電話番号
phoneNumberForm.value = addressBook.phone_number
}else if(selectedValue === ""){ // 「選択なし」の場合
fullNameForm.value = "" // 氏名
for(let i=0; i<birthdayForm.length; i++){ //生年月日
birthdayForm[i].value = Number(defaultBirthday[i])
}
ageForm.value = "" // 年齢
genderForm[0].checked = true // 性別
addressForm.value = "" // 住所
phoneNumberForm.value = "" // 電話番号
}
})
}
}
上記のアクションを記述することで、最終的に以下のような動作になれば完成です。
以上です。
コメント