Rails7 で TODOアプリを作ろう ③ (TaskのViewにBootstrap5を適用)

## はじめに

### 前回は

#### ScaffoldでTaskを作成・rootを設定 として

https://qiita.com/tomoaki-kimura/items/7936127388d235b72011

- scaffold
- MVC
- マイグレーション
- ルーティング

について学びました。

### 今回は

#### ScaffoldでTaskを作成・rootを設定として

- bootstrap5

について学びます。

では、はじめていきましょう。

## 1. Bootstrap5の主な変更点

全てではないですが、気をつけておきたい点をあげておきましょう。Bootstrap4系とは少しずつ書き方が違っていますので、必ず公式サイトを確認しながら進めていきましょう。

https://getbootstrap.com/

### 脱JQuery

今回のバージョンよりJQueryに依存しなくなりました。

また、Rails7への導入時の方法も変わっている事から、Rails６までの資料が参考にならず、少し注意が必要です。 

### SVGアイコン

Bootstrap用にアイコンが追加されましたので、FontAwesome 等への依存をしなくても良くなっています。

https://icons.getbootstrap.com/

### ブレークポイント

`xxl` が追加され、1400px以上の幅に対応するようになりました。

https://getbootstrap.com/docs/5.3/layout/breakpoints/

### その他

`form-group` などのような、クラス名に対する固有の名称が減ってきているようです。

単純にマージンの調整だけに使っていたものは、プロパティクラスを使ったりしていますので、地味に間違いそうな部分になります。

## 2. Bootstrap5の導入

#### 【注意】 このプロジェクトは、 `Rails7.1.x` を使っています。 `7.0.x` の場合 `importmap` ではなく `esbuild` が使われており、一部の導入に違いがあります。

### css

今回すでに、`rails new` の時のオプションで、 `-c bootstrap` を使っており、CSSにはBootstrapを導入しています。

ただ、Rails7では、 `cssbundling-rails` というgemが使われており、少し階層構造が違っているようです。

少し確認しておきます。

[![Image from Gyazo](https://i.gyazo.com/452a4c95b4208bff997db08951e76fbb.png)](https://gyazo.com/452a4c95b4208bff997db08951e76fbb)

`app/seets/builds` が増えていますね。

Rails7では、 `assets/stylesheets` の中に書いた `scss` がコンパイルされて、 `assets/builds/application.css` に書き込まれていくようですので、基本的に記述場所は、今まで同様 `app/assets/stylesheets` 以下に書けば良いようです。

`app/assets/builds/application.css` 内のコードには触らないようにしておきます。

各ファイルは以下の通りです。

app/builds/applicationl.css【確認】
```css
@charset "UTF-8";
/*!
 * Bootstrap  v5.3.2 (https://getbootstrap.com/)
 * Copyright 2011-2023 The Bootstrap Authors
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
 */
（略　この下にコンパイルされたコードが書かれていく）
```

コンパイルされた後のコードが `css` として書かれているようです。

app/assets/stylesheets【確認】
```scss
@import 'bootstrap/scss/bootstrap';
@import 'bootstrap-icons/font/bootstrap-icons';
```

こちらは、元のコードですね。ライブラリを読みに行っているようですので、 `@import` しか書かれていません。

### popper.js

導入がすごく簡単になったかと思いますが、実はここまででは問題があり、

現状では、ポップアップや、ハンバーガーメニューなどの一部機能が使えない状態となっています。

ですので、 `popper.js` を別途入れてあげる必要があります。

ただし、Rails７ではjsの扱いも変わってしまいましたので、一旦 `application.html.erb` を確認しておきましょう。

app/views/layouts/application.html.erb【確認】
```html
<!DOCTYPE html>
<html>
  <head>
    <title>TodoApp</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= javascript_importmap_tags %>
  </head>

  <body>
    <%= render "layouts/global_navigation" %>
    <div class="container my-4">
      <%= render "layouts/flash_messages" %>
      <%= yield %>
    </div>
  </body>
</html>
```

JSの読み込み部分が、

```html
<%= javascript_importmap_tags %>
```

となっていますね。 新機能の `importmap` を読んでいますので、これを使ってJS側の必要物を導入します。

では、コマンドを打ってみましょう。

```zsh
todo_app % bin/importmap pin bootstrap
```

すると、

```zsh
 todo_app % bin/importmap pin bootstrap
Pinning "bootstrap" to https://ga.jspm.io/npm:bootstrap@5.3.2/dist/js/bootstrap.esm.js
Pinning "@popperjs/core" to https://ga.jspm.io/npm:@popperjs/core@2.11.8/lib/index.js
```

このような結果となり、`popper.js` が導入されている事がわかります。

しかし、これではまだ読み込んでいませんので、`application.js` に `import` します。

ファイルの確認をしておきましょう。

app/javascript/application.js【確認】
```javascript
// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
import "@hotwired/turbo-rails"
import "controllers"
import * as bootstrap from "bootstrap"
```

この部分でライブラリを読み込みますので、

```javascript
import "@popperjs/core"
```

を足します。

app/javascript/application.js【追記】
```javascript
// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
import "@hotwired/turbo-rails"
import "controllers"
import * as bootstrap from "bootstrap"
// 以下を追記
import "@popperjs/core"
```

では、この状態でポップアップの確認をしますので、 `application.html.erb` のファイルを一度書き換えてみましょう。

app/views/layouts/application.html.erb【確認】
```html
<!DOCTYPE html>
<html>
  <head>
    <title>TodoApp</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= javascript_importmap_tags %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>
```

ここに、Bootstrap５の公式より、モーダルのコードをコピペします。

https://getbootstrap.com/docs/5.3/components/modal/#fullscreen-modal

以下のコードを使ってみましょう。

```html
<!-- Button trigger modal -->
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModal">
  Launch demo modal
</button>

<!-- Modal -->
<div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <h1 class="modal-title fs-5" id="exampleModalLabel">Modal title</h1>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <div class="modal-body">
        ...
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
        <button type="button" class="btn btn-primary">Save changes</button>
      </div>
    </div>
  </div>
</div>
```

各ページの内容が入ってくる `<%= yield %>` の上に仮貼りしてみます。

app/views/layouts/application.html.erb【追記】
```html
<!DOCTYPE html>
<html>
  <head>
    <title>TodoApp</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= javascript_importmap_tags %>
  </head>

  <body>
    <!-- ここから追記分 -->
    <!-- Button trigger modal -->
    <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModal">
      Launch demo modal
    </button>

    <!-- Modal -->
    <div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
      <div class="modal-dialog">
        <div class="modal-content">
          <div class="modal-header">
            <h1 class="modal-title fs-5" id="exampleModalLabel">Modal title</h1>
            <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
          </div>
          <div class="modal-body">
            ...
          </div>
          <div class="modal-footer">
            <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
            <button type="button" class="btn btn-primary">Save changes</button>
          </div>
        </div>
      </div>
    </div>
    <!-- ここまで追記分 -->
    <%= yield %>
  </body>
</html>
```

では動かしてみます。

Railsコマンドなのですが、今回から `rails s` ではなく `bin/dev` を使っていきます。

コンパイル作業が行われますので、 `css` や `js` を正しく使う為に必要になってきます。

ではターミナルより、

```zsh
todo_app % bin/dev
```

と打ってみましょう。

```zsh
 todo_app (main #%)% bin/dev                                  
08:15:58 web.1  | started with pid 37662
08:15:58 css.1  | started with pid 37663
08:15:59 css.1  | yarn run v1.22.19
08:15:59 css.1  | $ nodemon --watch ./app/assets/stylesheets/ --ext scss --exec "yarn build:css"
08:15:59 web.1  | DEBUGGER: Debugger can attach via UNIX domain socket (/var/folders/k7/wp537bh155j3db7cjqhw63300000gn/T/ruby-debug-sock-501/ruby-debug-kimuratomoaki-37662)
08:15:59 web.1  | => Booting Puma
08:15:59 web.1  | => Rails 7.1.1 application starting in development 
08:15:59 web.1  | => Run `bin/rails server --help` for more startup options
08:15:59 css.1  | [nodemon] 3.0.1
08:15:59 css.1  | [nodemon] to restart at any time, enter `rs`
08:15:59 css.1  | [nodemon] watching path(s): app/assets/stylesheets/**/*
08:15:59 css.1  | [nodemon] watching extensions: scss
08:15:59 css.1  | [nodemon] starting `yarn build:css`
08:15:59 web.1  | Puma starting in single mode...
08:15:59 web.1  | * Puma version: 6.4.0 (ruby 3.2.2-p53) ("The Eagle of Durango")
08:15:59 web.1  | *  Min threads: 5
08:15:59 web.1  | *  Max threads: 5
08:15:59 web.1  | *  Environment: development
08:15:59 web.1  | *          PID: 37662
08:15:59 web.1  | * Listening on http://127.0.0.1:3000
08:15:59 web.1  | * Listening on http://[::1]:3000
08:15:59 css.1  | $ yarn build:css:compile && yarn build:css:prefix
08:15:59 web.1  | Use Ctrl-C to stop
08:15:59 css.1  | $ sass ./app/assets/stylesheets/application.bootstrap.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules
08:16:00 css.1  | $ postcss ./app/assets/builds/application.css --use=autoprefixer --output=./app/assets/builds/application.css
08:16:00 css.1  | [nodemon] clean exit - waiting for changes before restart
```

このようにログが流れてきます。

これで動作するはずです。 `localhost:3000` でトップページにアクセスしましょう。

[![Image from Gyazo](https://i.gyazo.com/80e819044b4a9ba82dac1ea3f2dee85f.png)](https://gyazo.com/80e819044b4a9ba82dac1ea3f2dee85f)

ボタンを押下し、モーダルが開けば成功です。

## 3. 各ファイルの編集

これで準備が整いましたので、各ファイルを編集していきましょう。

### application.html.erb

先程編集した `app/views/layouts/application.html.erb` ですが、元に戻すのと同時にBootstrapを適用します。

以下のように編集します。

app/views/layouts/application.html.erb【編集】
```html
<!DOCTYPE html>
<html>
  <head>
    <title>TodoApp</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= javascript_importmap_tags %>
  </head>
    
  <body>
    <div class="container my-4">
      <%= yield %>
    </div>
  </body>
</html>
```

`<%= yield %>` は各コントローラーから表示される動的なテンプレートが表示されますので、その全てに `container` を適用します。

あとは、プロパティクラス `my-4` で縦方向の隙間調整をしておきましょう。

### tasks/index.html.erb

`app/views/tasks/index.html.erb` は以下の通りです。

app/views/tasks/index.html.erb【編集】
```html
<p style="color: green"><%= notice %></p>
<h1>Tasks</h1>
<div id="tasks">
  <% @tasks.each do |task| %>
    <%= render task %>
  <% end %>
</div>

<%= link_to "New task", new_task_path, class: "btn btn-primary me-2" %>
```

### tasks/show.html.erb

`app/views/tasks/show.html.erb` は以下の通りです。

app/views/tasks/show.html.erb【編集】
```html
<p style="color: green"><%= notice %></p>

<%= render @task %>

<div class="d-flex">
  <%= link_to "Edit this task", edit_task_path(@task), class: "btn btn-warning me-2" %>
  <%= link_to "Back to tasks", tasks_path, class: "btn btn-secondary me-2" %>
  <%= button_to "Destroy this task", @task, method: :delete, class: "btn btn-danger me-2", data: { turbo_confirm: "本当に削除しますか？" } %>
```

### tasks/new.html.erb

`app/views/tasks/new.html.erb` は以下の通りです。

app/views/tasks/new.html.erb【編集】
```html
<h1>New task</h1>
<%= render "form", task: @task %>
<br>

<div>
	<%= link_to "Back to tasks", tasks_path, class: "btn btn-secondary me-2" %>
</div>
```

### tasks/edit.html.erb

`app/views/tasks/edit.html.erb` は以下の通りです。

app/views/tasks/edit.html.erb【編集】
```html
<h1>Editing task</h1>

<%= render @task %>
<%= render "form", task: @task %>

<br>

<div class="d-flex">
	<%= link_to "Show this task", @task, class: "btn btn-primary me-2" %>
	<%= link_to "Back to tasks", tasks_path, class: "btn btn-secondary me-2" %>
</div>
```

## 4.部分テンプレート

### テンプレートの呼び出し

上記のファイルには、本来もっと色々書かれているはずですが、 例えば、 `edit.html.erb` の中には

```html
<%= render @task %>
<%= render "form", task: @task %>
```

のように何か省略されたような形跡があります。

これが部分テンプレート（パーシャル）となります。 `render` メソッドを使ってファイルを呼び出します。

### 基本的な構文

`render` メソッドの使い方ですが、

```html
<%= render "form", task: @task %>
```

render メソッドの引数が、 `form` と `task: @task` のようになっています。

`form` の部分が呼び出すファイルのパスで必須の引数となります。

`task: @task` は呼び出すファイルで使いたい変数を定義しているオプション部分

になります。 こちらは、`key: value` の形でいくつでも入れられます（入れすぎも良くないですが）

部分テンプレートの解説は一旦ここまでとして、実際に呼び出されているファイル `app/views/tasks/_form.html.erb` を見てみましょう。

### tasks/_form.html.erb

`tasks/_form.html.erb` を確認しましょう。

app/views/tasks/_form.html.erb【確認】
```html
<%= form_with(model: task) do |form| %>
  <% if task.errors.any? %>
    <div style="color: red">
      <h2><%= pluralize(task.errors.count, "error") %> prohibited this task from being saved:</h2>

      <ul>
        <% task.errors.each do |error| %>
          <li><%= error.full_message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div>
    <%= form.label :title, style: "display: block" %>
    <%= form.text_field :title %>
  </div>

  <div>
    <%= form.label :description, style: "display: block" %>
    <%= form.text_area :description %>
  </div>

  <div>
    <%= form.label :start_time, style: "display: block" %>
    <%= form.datetime_field :start_time %>
  </div>

  <div>
    <%= form.label :end_time, style: "display: block" %>
    <%= form.datetime_field :end_time %>
  </div>

  <div>
    <%= form.label :status, style: "display: block" %>
    <%= form.number_field :status %>
  </div>

  <div>
    <%= form.submit %>
  </div>
<% end %>
```

このようにフォームの内容が書かれています。これを１箇所で書いて `new` `edit` で共有しているので、基本的には１箇所触るだけでカスタマイズ可能という事になります。

ただし、その内容によっては共有化しない方が良い場合などもありますので、適宜使い分けると良いでしょう。

今回はこのまま編集します。

下記のように書き換えていきましょう。


app/views/tasks/_form.html.erb【編集】
```html
<div class="row">
  <div class="col-md-6">
    <%= form_with(model: task) do |form| %>
      <% if task.errors.any? %>
        <div style="color: red">
          <h2><%= pluralize(task.errors.count, "error") %> prohibited this task from being saved:</h2>

          <ul>
            <% task.errors.each do |error| %>
              <li><%= error.full_message %></li>
            <% end %>
          </ul>
        </div>
      <% end %>
      <% if params[:action] == "new" %>
        <div class="mb-3">
          <%= form.label :title, style: "display: block" %>
          <%= form.text_field :title, class: "form-control" %>
        </div>

        <div class="mb-3">
          <%= form.label :description, style: "display: block" %>
          <%= form.text_area :description, class: "form-control" %>
        </div>

        <div class="mb-3">
          <%= form.label :start_time, style: "display: block" %>
          <%= form.datetime_field :start_time, class: "form-control" %>
        </div>

        <div class="mb-3">
          <%= form.label :end_time, style: "display: block" %>
          <%= form.datetime_field :end_time, class: "form-control" %>
        </div>
      <% end %>
      <% if params[:action] == "edit" %>
        <div class="mb-3">
          <%= form.label :status, style: "display: block" %>
          <%= form.number_field :status, class: "form-control" %>
        </div>
      <% end %>

      <div class="mb-3">
        <%= form.submit class: "btn btn-primary" %>
      </div>
    <% end %>
  </div>
</div>
```

今回は、 `new` と `edit` で入力できる項目を変えたいと考えています。

具体的には、編集時には `未着手` `着手中` `完了` などのステータスしか触れなくしたいという方向です。

これを、

```html
<% if params[:action] == "アクション名" %>
<% end %>
```

のようにして条件分岐しています。

### 特殊ケース

いままでの構文に当たらないものとして、

`<%= render @task %>` がありますが、これは、 `@task` にモデルオブジェクトを与える事で、それが単数複数に限らず オブジェクトの要素分該当のテンプレートにデーターを入れた状態で繰り返されます。

これを表示している部分が、 `app/views/tasks/_task.html.erb` となり、ファイル名にモデル名を使っている事が特殊ケースの条件となります。

### tasks/_task.html.erb

では `app/views/tasks/_task.html.erb` を確認してみましょう。

app/views/tasks/_task.html.erb【確認】

```html
<div id="<%= dom_id task %>">
  <p>
    <strong>Title:</strong>
    <%= task.title %>
  </p>

  <p>
    <strong>Description:</strong>
    <%= task.description %>
  </p>

  <p>
    <strong>Start time:</strong>
    <%= task.start_time %>
  </p>

  <p>
    <strong>End time:</strong>
    <%= task.end_time %>
  </p>

  <p>
    <strong>Status:</strong>
    <%= task.status %>
  </p>

</div>
```

ここで、 `task` という変数が渡されていますが、これは自動で変数が定義されています。

これも編集しましょう。

app/views/tasks/_task.html.erb【編集】
```html
<div class="row">
  <div class="col-md-8">
    <table id="<%= dom_id task %>" class="table table-bordered">
      <tr>
        <th>Title:</th>
        <td><%= link_to_if params[:action] == "index", task.title, task %></td>
      </tr>
      <tr>
        <th>Description:</th>
        <td><%= task.description %></td>
      </tr>
      <tr>
        <th>Start time:</th>
        <td><%= task.start_time %></td>
      </tr>
      <tr>
        <th>End time:</th>
        <td><%= task.end_time %></td>
      </tr>
      <tr>
        <th>Status:</th>
        <td><%= task.status %></td>
      </tr>
    </table>
  </div>
</div>
```

編集後はこのようになります。

では、表示を確認しましょう。

```zsh
todo_app % bin/dev
```

で起動します。

[![Image from Gyazo](https://i.gyazo.com/62139c3688f3bf0ca4006e18a6fbd768.png)](https://gyazo.com/62139c3688f3bf0ca4006e18a6fbd768)

このようになればOKです。

一通りCRUD操作をやってみてください。

## 5. git

### git

ここまでを保存しておきましょう。

```zsh
todo_app % git add .
todo_app % git commit -m "TaskのViewにBootstrap5を適用"
```

### GitHub

Githubの設定ができているのであれば、同名のリポジトリを作成し、pushしておきましょう。

## おわりに

本チャプターはここまでとなります。

### 次回は

https://qiita.com/tomoaki-kimura/items/94c167b438e0485e2262

- バリデーション
- enum
- i18n

を学びます。

ここまでお疲れ様でした。