Chapter 03

チュートリアル1 Railsで管理画面を作る

shita
shita
2022.05.08に更新

チュートリアル全体の説明

🐱 今回はチュートリアルとしてSPA風の管理画面を作っていくよ。Rails AdminやActive Adminのような管理画面を想定しているよ。

🐱 具体的にはCRUD(一覧・編集・登録・削除)ができて、そこにページネーション・検索・ソートなどのプラスαな機能があるイメージだよ。これをSPA風にしたものを作っていくよ。

🐱 ベースはただのCRUDなので管理対象はなんでもいいんだけど、ねこが好きなのでcatsテーブルを管理対象にするよ。後はほかにもいくつか動物のテーブルを用意して、AMS(Animal Management System: 動物管理システム)というシステムとして作っていく。

🐱 まずチュートリアル1では、Hotwireを使わずに、素のRailsで管理画面を開発するよ。


チュートリアル1

🐱 チュートリアル1のデモはこちらから触れるよ。
https://cat-hotwire-1.herokuapp.com/cats

🐱 チュートリアル2ではTurboを使って、JavaScriptを書かずに管理画面をSPA風にしていくよ。


チュートリアル2

🐱 チュートリアル2のデモはこちらから触れるよ。
https://cat-hotwire-2.herokuapp.com/cats

🐱 チュートリアル3ではStimulusを使って、管理画面をもっとSPA風にしていくよ。


チュートリアル3

🐱 チュートリアル3のデモはこちらから触れるよ。
https://cat-hotwire-3.herokuapp.com/cats

チュートリアル1の説明

🐱 まずチュートリアル1では素のRailsで管理画面を作っていくよ。

🐱 Hotwireの素晴らしい点の1つとして、既存のRailsアプリに対して後付けで段階的にSPA風の挙動を追加できる点があるよ。既存のRailsアプリのコードを少し修正するだけで、SPAのメリットを享受できるようになるんだ。初めからHotwireを使って開発してもいいんだけど、今回はまずは慣れ親しんだ素のRailsで管理画面を作って、その後でHotwireを使ってSPA風にしていくよ。

🐱 このチャプターではHotwireは使わないので、不要だと思ったら次のチャプターまで読み飛ばしちゃってもOKだよ。チュートリアル1終了時点のコードは https://github.com/shita1112/cat-hotwire-demo にあるからそこからgit cloneできるよ。

$ git clone https://github.com/shita1112/cat-hotwire-demo
$ cd cat-hotwire-demo
$ git checkout tutorial-1

🐱 チュートリアル1で作る機能は以下の通りだよ。

  • CRUD(一覧・編集・登録・削除)
  • ページネーション
  • 検索
  • ソート
  • 編集・登録時のインラインバリデーション

rails new

🐱 それじゃあさっそく開発していこう!

🐱 まずは$ rails newでRailsアプリを作るよ。

$ rails new cat-hotwire --css=bootstrap --skip-jbuilder --skip-action-mailbox --skip-action-mailer --skip-test --skip-active-storage --skip-action-text

🐱 オプションの意味は以下の通りだよ。

  • --css=bootstrap: CSSフレームワークにBootstrapを利用
  • --skip-jbuilder: jbuilderの導入をskip
  • --skip-action-mailbox: action-mailboxの導入をskip
  • --skip-action-mailer: action-mailerの導入をskip
  • --skip-test: testの導入をskip
  • --skip-active-storage: active-storageの導入をskip
  • --skip-action-text: action-textの導入をskip

🐱 CSSフレームワークにBootstrapを使うので、オプションに--css bootstrapを指定してね。これはRails7で追加されたオプションで、これを使うとBootstrapとBootstrap Iconsの設定をいい感じにやってくれるよ。

🐱 あとjbuilder等の不要な機能はスキップしてるよ。

コミット
https://github.com/shita1112/cat-hotwire-demo/commit/79f42af2d453b6eae72d272e3f4814e5b84a36cd

日本向けに設定を変更

🐱 日本語と日本時間を使うように設定を修正するよ。

config/application.rb
+   # タイムゾーンをTokyo(日本)にする
+   config.time_zone = "Tokyo"
+   # デフォルトのロケールを日本にする
+   config.i18n.default_locale = :ja

🐱 日本語のロケールファイルをja.ymlに配置するよ。rails-i18nのこちらの翻訳を利用させていただくよ。

ja.yml(長いので折りたたみ)
config/locales/ja.yml
---
ja:
  activerecord:
    errors:
      messages:
        record_invalid: 'バリデーションに失敗しました: %{errors}'
        restrict_dependent_destroy:
          has_one: "%{record}が存在しているので削除できません"
          has_many: "%{record}が存在しているので削除できません"
  date:
    abbr_day_names:
    -------abbr_month_names:
    - 
    - 1月
    - 2月
    - 3月
    - 4月
    - 5月
    - 6月
    - 7月
    - 8月
    - 9月
    - 10月
    - 11月
    - 12月
    day_names:
    - 日曜日
    - 月曜日
    - 火曜日
    - 水曜日
    - 木曜日
    - 金曜日
    - 土曜日
    formats:
      default: "%Y/%m/%d"
      long: "%Y年%m月%d日(%a)"
      short: "%m/%d"
    month_names:
    - 
    - 1月
    - 2月
    - 3月
    - 4月
    - 5月
    - 6月
    - 7月
    - 8月
    - 9月
    - 10月
    - 11月
    - 12月
    order:
    - :year
    - :month
    - :day
  datetime:
    distance_in_words:
      about_x_hours:
        one: 約1時間
        other: 約%{count}時間
      about_x_months:
        one: 約1ヶ月
        other: 約%{count}ヶ月
      about_x_years:
        one: 約1年
        other: 約%{count}almost_x_years:
        one: 1年弱
        other: "%{count}年弱"
      half_a_minute: 30秒前後
      less_than_x_seconds:
        one: 1秒以内
        other: "%{count}秒未満"
      less_than_x_minutes:
        one: 1分以内
        other: "%{count}分未満"
      over_x_years:
        one: 1年以上
        other: "%{count}年以上"
      x_seconds:
        one: 1秒
        other: "%{count}秒"
      x_minutes:
        one: 1分
        other: "%{count}分"
      x_days:
        one: 1日
        other: "%{count}日"
      x_months:
        one: 1ヶ月
        other: "%{count}ヶ月"
      x_years:
        one: 1年
        other: "%{count}年"
    prompts:
      second:minute:hour:day:month:year:errors:
    format: "%{attribute}%{message}"
    messages:
      accepted: を受諾してください
      blank: を入力してください
      confirmation: と%{attribute}の入力が一致しません
      empty: を入力してください
      equal_to: は%{count}にしてください
      even: は偶数にしてください
      exclusion: は予約されています
      greater_than: は%{count}より大きい値にしてください
      greater_than_or_equal_to: は%{count}以上の値にしてください
      inclusion: は一覧にありません
      invalid: は不正な値です
      less_than: は%{count}より小さい値にしてください
      less_than_or_equal_to: は%{count}以下の値にしてください
      model_invalid: 'バリデーションに失敗しました: %{errors}'
      not_a_number: は数値で入力してください
      not_an_integer: は整数で入力してください
      odd: は奇数にしてください
      other_than: は%{count}以外の値にしてください
      present: は入力しないでください
      required: を入力してください
      taken: はすでに存在します
      too_long: は%{count}文字以内で入力してください
      too_short: は%{count}文字以上で入力してください
      wrong_length: は%{count}文字で入力してください
    template:
      body: 次の項目を確認してください
      header:
        one: "%{model}にエラーが発生しました"
        other: "%{model}に%{count}個のエラーが発生しました"
  helpers:
    select:
      prompt: 選択してください
    submit:
      create: 登録する
      submit: 保存する
      update: 更新する
  number:
    currency:
      format:
        delimiter: ","
        format: "%n%u"
        precision: 0
        separator: "."
        significant: false
        strip_insignificant_zeros: false
        unit:format:
      delimiter: ","
      precision: 3
      separator: "."
      significant: false
      strip_insignificant_zeros: false
    human:
      decimal_units:
        format: "%n %u"
        units:
          billion: 十億
          million: 百万
          quadrillion: 千兆
          thousand:trillion:unit: ''
      format:
        delimiter: ''
        precision: 3
        significant: true
        strip_insignificant_zeros: true
      storage_units:
        format: "%n%u"
        units:
          byte: バイト
          eb: EB
          gb: GB
          kb: KB
          mb: MB
          pb: PB
          tb: TB
    percentage:
      format:
        delimiter: ''
        format: "%n%"
    precision:
      format:
        delimiter: ''
  support:
    array:
      last_word_connector: "、"
      two_words_connector: "、"
      words_connector: "、"
  time:
    am: 午前
    formats:
      default: "%Y年%m月%d日(%a) %H時%M分%S秒 %z"
      long: "%Y/%m/%d %H:%M"
      short: "%m/%d %H:%M"
    pm: 午後

コミット
https://github.com/shita1112/cat-hotwire-demo/commit/057b9d9b9c97ddc98601b0bc15b64a8a76a01bd9

Turbo Driveを無効化

🐱 Rails7ではデフォルトでTurbo Driveが有効になっている。チュートリアル1ではHotwireを使わずにアプリを開発したいので、無効にしておくよ(チュートリアル2で有効にするよ)。

Turbo.session.drive = false

コミット
https://github.com/shita1112/cat-hotwire-demo/commit/e00da2fa82fe6c814ebe7321c9857d3a8362b610

動物5種のScaffoldを追加

🐱 Cat(ねこ)のScaffoldを作成するよ。

# DBを作成
$ rails db:create

# ねこのScaffoldを作成
$ rails g scaffold Cat name:string age:integer

# Catテーブルを作成
$ rails db:migrate

🐱 これでCatのCRUDができたよ。サーバーを起動して http://localhost:3000/cats にアクセスして確認してみてね。

$ bin/dev

🐱 ($ rails serverではなく$ bin/devな理由については次節で説明するよ。)

🐱 Catの日本語訳を追加しておくよ。

config/locales/activerecord.ja.yml
ja:
  activerecord:
    models:
      cat: ねこ
    attributes:
      cat:
        name: 名前
        age: 年齢

🐱 実際に作り込んでいくのはCatの画面だけだけど、管理画面にテーブルが1つだと寂しいので、他にもいくつかScaffoldを作っておくよ。

# 上からいぬ、ひよこ、ハリネズミ、フクロウ
$ rails g scaffold Dog name:string age:integer
$ rails g scaffold Chick name:string age:integer
$ rails g scaffold Hedgehog name:string age:integer
$ rails g scaffold Owl name:string age:integer

# テーブルを作成
$ rails db:migrate

コミット
https://github.com/shita1112/cat-hotwire-demo/commit/383f1e868913fba0b6c72202aa9fcd4e1c2d4a15
https://github.com/shita1112/cat-hotwire-demo/commit/55ace79bca4dfb093f77e8572ffb7678ba50e595

importmap-railsとjsbundling-rails

🐱 今回、サーバーの起動に$ rails serverではなく$ bin/devを使ったよ。その理由について説明するね。

🐱 Rails6ではnpmパッケージ(JavaScriptのライブラリ)の管理にはwebpackerというgemを利用していたよ。これがRails7では廃止になって、代わりに2つの方法が提供されるようになったよ。

🐱 1つ目はimportmap-railsというgemを使う方法で、これがRailsのデフォルトのやり方だよ。これはwebpackやesbuild等のバンドラーを利用しない方法だよ。自前のJavaScriptはES6で書いてバンドルせずにHTTP/2で配信して、サードパーティーのJavaScriptライブラリはCDNから取得する方法になる。今回は使わないので、詳しく知りたい場合は以下の記事を参考にしてね。

参考
https://zenn.dev/takeyuweb/articles/996adfac0d58fb
https://world.hey.com/dhh/modern-web-apps-without-javascript-bundling-or-transpiling-a20f2755
https://github.com/rails/importmap-rails

🐱 2つ目がjsbundling-rails(とcssbundling-rails)を使う方法で、今回はこちらを使っているよ。$ rails newのオプションで--css bootstrapを指定したけれども、その場合は自動的にjsbundling-railsを使うことになるよ。

🐱 jsbundling-railsはバンドラーを使うよ(デフォルトではwebpackではなく、ビルドが高速なesbuildを使う)。ただし$ rails serverを起動するだけでは自動でJavaScript・CSSをビルドしてくれないんだ。JavaScript・CSSのビルドをするためにはforemanというプロセス管理のツールを使って、サーバーのプロセスと、JavaScript・CSSの自動ビルドのプロセスを同時に立ち上げる必要があるよ。--css bootstrapを指定した場合、↓のようなファイルが用意されていい感じにやってくれるので、開発する側は$ bin/devを叩くだけでOKだよ。

`$ bin/dev`の詳細
bin/dev
#!/usr/bin/env bash

# foremanがインストールされていなければインストール
if ! command -v foreman &> /dev/null
then
  echo "Installing foreman..."
  gem install foreman
fi

# Procfile.devを元にforemanを起動
foreman start -f Procfile.dev "$@"
Procfile.dev
# foremanが立ち上げるプロセス

# サーバー起動
web: bin/rails server -p 3000
# ファイルの変更を監視して、esbuildでJavaScriptを自動ビルド(package.jsonのbuildに対応)
js: yarn build --watch
# ファイルの変更を監視して、sassでapplication.bootstrap.scssを自動ビルド(package.jsonのbuild:cssに対応)
css: yarn build:css --watch
package.json
  "scripts": {
    // yarn buildに対応
    "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds",
    // yarn build:cssに対応
    "build:css": "sass ./app/assets/stylesheets/application.bootstrap.scss ./app/assets/builds/application.css --no-source-map --load-path=node_modules"
  }

🐱 JSのビルドに関してはjsbundling-railsが、CSSのビルドに関してはcssbundling-railsが担当するよ。

🐱 ということで、このチュートリアルではサーバー起動には$ bin/devを使っていくよ。

参考
https://techracho.bpsinc.jp/hachi8833/2022_03_17/115294
https://github.com/rails/jsbundling-rails

seedsの作成

🐱 ねこのレコードを100件作るよ。

🐱 まずはseedsファイルを作るよ。

seeds.rb(長いので折りたたみ)
db/seeds.rb
# 参考: https://nekorich.com/cat-names
Cat.create(
  [
    {name: "アーチー", age: 0 },
    {name: "アーロ", age: 1 },
    {name: "アイヴァン", age: 2 },
    {name: "アイリス", age: 3 },
    {name: "アヴァロン", age: 4 },
    {name: "アクセル", age: 5 },
    {name: "あずき", age: 6 },
    {name: "アディ", age: 7 },
    {name: "アディソン", age: 8 },
    {name: "アニー", age: 9 },
    {name: "アビー", age: 10 },
    {name: "アポロ", age: 11 },
    {name: "アリエル", age: 12 },
    {name: "アリス", age: 13 },
    {name: "アルド", age: 14 },
    {name: "アルフィ", age: 15 },
    {name: "アレックス", age: 16 },
    {name: "アンバー", age: 17 },
    {name: "イザベラ", age: 18 },
    {name: "イジィ", age: 19 },
    {name: "ウィスカー", age: 0 },
    {name: "ウィリアム", age: 1 },
    {name: "ウィロー", age: 2 },
    {name: "うに", age: 3 },
    {name: "エイヴァ", age: 4 },
    {name: "エヴァ", age: 5 },
    {name: "エコー", age: 6 },
    {name: "エディ", age: 7 },
    {name: "エマ", age: 8 },
    {name: "エラ", age: 9 },
    {name: "エリー", age: 10 },
    {name: "エルサ", age: 11 },
    {name: "エルビス", age: 12 },
    {name: "エンジェル", age: 13 },
    {name: "オータム", age: 14 },
    {name: "オーティス", age: 15 },
    {name: "オーディン", age: 16 },
    {name: "オードリー", age: 17 },
    {name: "おくら", age: 18 },
    {name: "オジー", age: 19 },
    {name: "オスカー", age: 0 },
    {name: "おもち", age: 1 },
    {name: "オリィ", age: 2 },
    {name: "オリオン", age: 3 },
    {name: "オリバー", age: 4 },
    {name: "オリビア", age: 5 },
    {name: "オレオ", age: 6 },
    {name: "ガーフィールド", age: 7 },
    {name: "かい", age: 8 },
    {name: "カエサル", age: 9 },
    {name: "ガナー", age: 10 },
    {name: "カリー", age: 11 },
    {name: "キキ", age: 12 },
    {name: "ギズモ", age: 13 },
    {name: "キティ", age: 14 },
    {name: "きなこ", age: 15 },
    {name: "キャスパー", age: 16 },
    {name: "キャンディ", age: 17 },
    {name: "ぎんじ", age: 18 },
    {name: "クイナ", age: 19 },
    {name: "くう", age: 0 },
    {name: "グース", age: 1 },
    {name: "クーパー", age: 2 },
    {name: "クッキー", age: 3 },
    {name: "クラッシュ", age: 4 },
    {name: "グリフィン", age: 5 },
    {name: "グレイシー", age: 6 },
    {name: "グレース", age: 7 },
    {name: "クレオ", age: 8 },
    {name: "くろ", age: 9 },
    {name: "クロエ", age: 10 },
    {name: "ケイシー", age: 11 },
    {name: "ケイティ", age: 12 },
    {name: "ケティ", age: 13 },
    {name: "ここ", age: 14 },
    {name: "コスモ", age: 15 },
    {name: "こたろう", age: 16 },
    {name: "こてつ", age: 17 },
    {name: "コナ", age: 18 },
    {name: "ゴンゾ", age: 19 },
    {name: "サーシャ", age: 0 },
    {name: "さくら", age: 1 },
    {name: "サシー", age: 2 },
    {name: "サディ", age: 3 },
    {name: "サミー", age: 4 },
    {name: "サム", age: 5 },
    {name: "サレム", age: 6 },
    {name: "シーダ", age: 7 },
    {name: "ジェイク", age: 8 },
    {name: "シェビー", age: 9 },
    {name: "ジギー", age: 10 },
    {name: "じじ", age: 11 },
    {name: "ししまる", age: 12 },
    {name: "シナモン", age: 13 },
    {name: "シバ", age: 14 },
    {name: "ジプシー", age: 15 },
    {name: "ジャスティス", age: 16 },
    {name: "ジャスパー", age: 17 },
    {name: "ジャスミン", age: 18 },
    {name: "ジャック", age: 19 },
  ]
)

🐱 このデータをDBに投入するよ。

$ rails db:seed

コミット
https://github.com/shita1112/cat-hotwire-demo/commit/bf4130bb986d2ed850b7f5a3b57bee43027da935

ページネーションの実装

🐱 次にページネーション機能を実装するよ。

🐱 ページネーションの実装にはkaminariというgemを使うよ。

🐱 Gemfileにkaminariを追加して、bundle installするよ。

Gemfile
gem "kaminari"
$ bundle install

🐱 コードを以下のように修正するよ。

app/controllers/cats_controller.rb
  def index
-   @cats = Cat.all
+   # @catsに対してページネートできるようにする
+   @cats = Cat.page(params[:page])
  end
app/views/cats/index.html.erb
    <% end %>
  </div>

+ <%# ページネーションを表示する %>
+ <%= paginate @cats %>
+
  <%= link_to "New cat", new_cat_path %>

🐱 これでページネーションできるようになったよ。

🐱 ただ、これだとページネーションの表記が英語なので日本語化しておくよ。

config/locales/kaminari.ja.yml
# 参考: https://github.com/tigrish/kaminari-i18n/blob/master/config/locales/ja.yml
ja:
  helpers:
    page_entries_info:
      more_pages:
        display_entries: "<b>%{total}</b>中の%{entry_name}を表示しています <b>%{first} - %{last}</b>"
      one_page:
        display_entries:
          one: "<b>%{count}</b>レコード表示中です %{entry_name}"
          other: "<b>%{count}</b>レコード表示中です %{entry_name}"
          zero: "レコードが見つかりませんでした %{entry_name}"
  views:
    pagination:
      first: "&laquo; 最初"
      last: "最後 &raquo;"
      next: "次 &rsaquo;"
      previous: "&lsaquo; 前"
      truncate: "&hellip;"

🐱 あとkaminariはデフォルトだと1ページにつきレコードを25件を表示するのだけど、このアプリでは1ページにつき10件の表示にしたいので、その設定をしておくよ。

# kaminariの設定ファイルを生成する
$ rails g kaminari:config
      create  config/initializers/kaminari_config.rb
config/initializers/kaminari_config.rb
# frozen_string_literal: true
Kaminari.configure do |config|
  # 1ページ10件に変更する
  config.default_per_page = 10
  # config.max_per_page = nil
  # config.window = 4
  # config.outer_window = 0
  # config.left = 0
  # config.right = 0
  # config.page_method_name = :page
  # config.param_name = :page
  # config.params_on_first_page = false
end

🐱 これでページネーション機能は完成だよ。

コミット
https://github.com/shita1112/cat-hotwire-demo/commit/f1f704c6196a43dbbae070b2ee45bcd9a0bccf05

検索の実装

🐱 次に検索機能を実装するよ。

🐱 検索の実装にはransackというgemを使うよ。

🐱 まずransackをinstallするよ。

Gemfile
gem "ransack"
$ bundle install

🐱 コードはこんな感じになるよ。

app/controllers/cats_controller.rb
  def index
-   @cats = Cat.page(params[:page])

+   # `Cat.ransack`でCatに対してransackを使う
+   # params[:q]には検索フォームで指定した検索条件が入る
+   @search = Cat.ransack(params[:q])
+
+   # デフォルトのソートをid降順にする
+   @search.sorts = 'id desc' if @search.sorts.empty?
+
+   # `@search.result`で検索結果となる@catsを取得する
+   # 検索結果に対してはkaminariのpageメソッドをチェーンできる
+   @cats = @search.result.page(params[:page])
  end
app/views/cats/index.html.erb
 <h1>Cats</h1>

+ <%# ransackを利用した検索フォームでは、form_withの代わりにsearch_form_forを使う %>
+ <%= search_form_for @search do |f| %>
+ 
+   <%# `カラム名_cont`とすることで、カラムに対してLIKEを使った曖昧一致検索ができる %>
+   <%= f.label :name_cont, "名前" %>
+   <%= f.search_field :name_cont %>
+ 
+   <%# `カラム名_eq`とすることで、カラムに対して完全一致検索ができる %>
+   <%= f.label :age_eq, "年齢" %>
+   <%= f.search_field :age_eq %>
+   <%= f.submit %>
+ 
+   <%# 検索結果と検索フォームをクリアする %>
+   <%= link_to "クリア", cats_path %>
+ <% end %>

 <div id="cats">
   <% @cats.each do |cat| %>
     <%= render cat %>

🐱 これで検索フォームは完成だよ。

コミット
https://github.com/shita1112/cat-hotwire-demo/commit/a132ee9c15e429eaee27a60797404bdb4af9f148

ソートの実装

🐱 次にソート機能を実装するよ。

🐱 ソートの実装もransackがやってくれるよ。

app/views/cats/index.html.erb
+ <%# ソートのリンク。このリンクをクリックするとname属性でソートされる %>
+ <%= sort_link(@search, :name) %>
+ <%# ソートのリンク。このリンクをクリックするとage属性でソートされる %>
+ <%= sort_link(@search, :age) %>

 <div id="cats">
   <% @cats.each do |cat| %>
     <%= render cat %>

🐱 これでソート機能は完成だよ。

コミット
https://github.com/shita1112/cat-hotwire-demo/commit/6806e705f37ed030e59fe67cf8a33213b8a94370

Bootstrap5で見た目を整える

🐱 一通りの機能はできたので、Bootstrap5を使って見た目を整えるよ。

🐱 変更量が多いので、こちらのコミットを参考にしてね。

コミット
https://github.com/shita1112/cat-hotwire-demo/commit/94c959338e7d09b59d522c10158360066e1f82df

インラインバリデーションの実装

🐱 最後にインラインバリデーションを実装するよ。

🐱 実装にはbootstrap_formというgemを使うよ。bootstrap_formを使うと、インラインバリデーションに対応したBootstrap用のフォームを簡単に実装できるよ。

🐱 まずモデルにバリデーションを実装するよ。

app/models/cat.rb
class Cat < ApplicationRecord
  # 名前: 必須
  validates :name, presence: true

  # 年齢: 必須 + integer + 0以上
  validates :age, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
end

🐱 bootstrap_formをインストールするよ。

Gemfile
gem "bootstrap_form"
$ bundle install

🐱 次にbootstrap_formを使ってフォームを書くよ。

app/views/cats/_form.html.erb
-<%= form_with(model: cat) do |form| %>
- <% if cat.errors.any? %>
-   <div style="color: red">
-     <h2><%= pluralize(cat.errors.count, "error") %> prohibited this cat from being saved:</h2>
-
-     <ul>
-       <% cat.errors.each do |error| %>
-         <li><%= error.full_message %></li>
-       <% end %>
-     </ul>
-   </div>
- <% end %>
-
- <div>
-   <%= form.label :name, style: "display: block" %>
-   <%= form.text_field :name %>
- </div>
-
- <div>
-   <%= form.label :age, style: "display: block" %>
-   <%= form.number_field :age %>
- </div>
-
- <div>
-   <%= form.submit %>
- </div>
-<% end %>

+<%# bootstrap_formを使ったフォームでは、form_withの代わりにbootstrap_form_withを使う %>
+<%# バリデーションエラーの表示はbootstrap_formが用意してくれるので、自前で用意する必要がなくなる %>
+<%= bootstrap_form_with(model: cat) do |form| %>
+
+ <%# ラベルはbootstrap_formが用意してくれるので、自前で用意する必要がなくなる %>
+ <%# Bootstrapのclass属性もbootstrap_formが設定してくれるので、自前で用意する必要はない %>
+ <%= form.text_field :name %>
+ <%= form.number_field :age %>
+
+ <%# Bootstrapのprimary色のsubmitボタン %>
+ <%= form.primary %>
+<% end %>

🐱 だいぶコードがすっきりしたね。

🐱 bootstrap_formはバリデーションでpresence: trueに設定した属性のラベルに、自動でrequiredクラス属性を付与してくれる。それを利用して必須属性は赤*でマークされるようにCSSを設定しておくよ。

app/assets/stylesheets/application.bootstrap.scss
  @import 'bootstrap/scss/bootstrap';
  @import 'bootstrap-icons/font/bootstrap-icons';

+ label.required:after {
+   content:" *";
+   color: red;
+ }

コミット
https://github.com/shita1112/cat-hotwire-demo/commit/1f6fa9bf0e91f2e0f421176f6202e030c75b707e

まとめ

🐱 これでチュートリアル1は終わりだよ!完成したアプリケーションがこちら。


チュートリアル1

🐱 チュートリアル1のデモはこちらから触れるよ。
https://cat-hotwire-1.herokuapp.com/cats

🐱 お疲れさまでした〜。

Hotwireのお仕事を探しています🙇‍♂️

現在、業務委託で入れるHotwireのお仕事を探しています。

Hotwireの経験があるRailsエンジニアをお探しの方は、ぜひ https://twitter.com/shita1112 にDMください🙇‍♂️

(Hotwire便利で楽しいのでもっと使っていきたいのですが、現状だとHotwireの案件はなかなか見つからない感じなのでした......この本が参考になったと感じて、これからHotwireやっていくぞ〜となった方は、ぜひぜひお気軽にDMください〜。とても喜びます!)