LoginSignup
1
2

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

Last updated at Posted at 2023-10-30

はじめに

前回は

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

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

について学びました。

今回は

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

  • bootstrap5

について学びます。

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

1. Bootstrap5の主な変更点

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

脱JQuery

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

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

SVGアイコン

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

ブレークポイント

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

その他

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

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

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

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

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

app/builds/applicationl.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【確認】

@import 'bootstrap/scss/bootstrap';
@import 'bootstrap-icons/font/bootstrap-icons';

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

popper.js

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

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

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

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

app/views/layouts/application.html.erb【確認】

<!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の読み込み部分が、

<%= javascript_importmap_tags %>

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

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

todo_app % bin/importmap pin bootstrap

すると、

 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.jsimport します。

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

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"
import * as bootstrap from "bootstrap"

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

import "@popperjs/core"

を足します。

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"
import * as bootstrap from "bootstrap"
// 以下を追記
import "@popperjs/core"

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

app/views/layouts/application.html.erb【確認】

<!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>

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

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

<!-- 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【追記】

<!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 を使っていきます。

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

ではターミナルより、

todo_app % bin/dev

と打ってみましょう。

 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

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

3. 各ファイルの編集

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

application.html.erb

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

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

app/views/layouts/application.html.erb【編集】

<!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【編集】

<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【編集】

<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【編集】

<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【編集】

<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 の中には

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

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

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

基本的な構文

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

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

render メソッドの引数が、 formtask: @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【確認】

<%= 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 %>

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

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

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

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

app/views/tasks/_form.html.erb【編集】

<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>

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

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

これを、

<% 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【確認】

<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【編集】

<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>

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

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

todo_app % bin/dev

で起動します。

Image from Gyazo

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

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

5. git

git

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

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

GitHub

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

おわりに

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

次回は

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

を学びます。

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

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2