【Rails7】JavaScript + Stimulusでフォームの自動入力補完機能を実装してみた(DBからJSへの値受け渡し)

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)でフォームの自動入力補完機能を実装する流れ

先ほどの自動入力補完機能を実装する大まかな流れは以下の通りです。

  1. DB取得データをJSONに変換し、HTML要素のdata属性に設定(viewに埋め込む)
  2. 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つです。

  1. data-controller
  2. data-action
  3. 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 = "" // 電話番号
        }
      })

  }
}

上記のアクションを記述することで、最終的に以下のような動作になれば完成です。

以上です。

参考資料

Qiita
stimulus、初歩の初歩(基本構造についての私的理解まとめ) - Qiita こちらの続き、railsプロジェクトに当てる前提でいろいろ試しています。とりあえずハンドブックを読んで、自分なりに纏めてみる(ちなみに書いている人間はWebプログラミン...
Qiita
Stimulusをはじめよう - Qiita Stimulusとは何かStimulusとは、JavaScriptで書かれたクライアントサイドのライブラリです。Basecampによって開発され、2021/12にversion2がリリースされま…
Madogiwa Blog
Ruby on Rails:インスタンス変数等をjavascriptに受け渡す方法 - Madogiwa Blog はじめに Railsで取得したデータを使ってChart.jsでグラフ描画する等、Railsで扱っている変数をjsに受け渡す方法をメモφ(..) ↓イメージはこんな感じ 手順 概要 取得データ...
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

愛知の34歳。無職で暇になり始めたプログラミング(Ruby on Rails)の忘備録をまとめたブログです。最近は別にやりたいことができたのでプログラミングほぼやっていません。気が向いたらまた再開するかも。僕の日常はメインブログの方で更新しています。

コメント

コメントする

目次