Rails Bootstrap with Modal Form

## Rails Bootstrap with Modal Form
Railsの勉強をするにあたり、マスタメンテ画面を作ろうと思いました。
そこで[こちら](https://qiita.com/niwaken/items/ffbce52fb024fd369f24)の記事を参考に、イマドキのかっこいいModal画面を作ろうと思いました。

## 要件

* CUDはModal Formから、作成できるようにする。
* Bootstrapを使ってできるだけかっこよくする。
* 難しい実装にしない。
* 自学のために、記録する。

## Railsアプリケーションの生成

[こちら](https://railsguides.jp/getting_started.html)のガイドを勉強用に使っていたのでプロジェクト的にはRailsアプリケーションがある状態ですが、作ります。

```bash:terminal
$ rails new blog
```

(sqlite3が起動しなかった気がするので、あとで追記しときます。)

Railsアプリディレクトリに移動

```bash:terminal
$ cd blog
```

## 手順

記事通りに記載しましたが、うまくいかず、[GitHubのREADME.md](https://github.com/twbs/bootstrap-rubygem)を参考にしつつ、アプリ立ち上げまで

### Bootstrap導入

Gemfileにgemを追加します。

```bash:blog/Gemfile
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '2.5.4'

# for Bootstrap4
gem 'jquery-rails'
gem 'bootstrap', '~> 4.3.1'

# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 5.2.2', '>= 5.2.2.1'
```

```
# for Bootstrap4
gem 'jquery-rails'
gem 'bootstrap', '~> 4.3.1'
```
を追記しました。

で、```bundle install```でインストール。

```bash:terminal
$ bundle install
```

ライブラリがインストールされます。

### application.cssの拡張子変更

まず、```blog/app/assets/stylesheets/application.css```がRailsアプリケーション生成時に自動生成されるので、```application.css```を```application.scss```に変更します。

```bash:terminal
$ cd blog/app/assets/stylesheets/
$ mv application.css application.scss
```

### application.scssの編集

この```application.scss```を編集します。

```scss:applicatino.scss
/*
 * This is a manifest file that'll be compiled into application.css, which will include all the files
 * listed below.
 *
 * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
 * vendor/assets/stylesheets directory can be referenced here using a relative path.
 *
 * You're free to add application-wide styles to this file and they'll appear at the bottom of the
 * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
 * files in this directory. Styles in this file should be added after the last require_* statement.
 * It is generally better to create a new file per style scope.
 *
 *= require_tree .
 *= require_self
 */
```

コメントだけのこのファイルに以下を追記します。

```scss:application.scss
// for Bootstrap4
@import "bootstrap";
```

そして、コメントの

```scss:application.scss
 *= require_tree .
 *= require_self
```
の2行を削除します。

README.mdより

```text
Do not use *= require in Sass or your other stylesheets will not be able to access the Bootstrap mixins and variables.
```

### application.jsの編集

次に```blog/app/assets/javascripts/application.js```を編集します。

```js:application.js
// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's
// vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file. JavaScript code in this file should be added after the last require_* statement.
//
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require rails-ujs
//= require activestorage
//= require turbolinks
//= require_tree .
```

に

```js:application.js
//= require jquery3
//= require popper
//= require bootstrap-sprockets
```

の３行を追加します。

### Scaffoldで雛形の作成

記事に沿ってscaffoldを使って、MVCの雛形を作ります。
user雛形を作成します。

```bash:terminal
$ cd blog
$ rails g scaffold user name:string
```

### Railsアプリケーションの起動

```bash:terminal
$ cd blog
$ rails s
```

とりあえず、起動させて、```localhost:3000/users```にアクセスしましょう。

画面が表示されたら問題なく起動しています。

### Bootstrap適用確認

作成した```blog/app/views/users/index.html.erb```を開き、以下のコードにclass属性を付与し、Bootstrapが適用されているか確認します。

```erb:index.html.erb
<%= link_to 'New User', new_user_path %>
⬇︎
<%= link_to 'New User', new_user_path, class: "btn btn-lg, btn-primary" %>
```

リンクが青色の"New User"と書かれたボタンに変化していたら、OKです。
![users.png](https://qiita-image-store.s3.amazonaws.com/0/75277/8861fe39-391a-6ff1-b06b-6cbd4c1a94dc.png)

![bootstrap.png](https://qiita-image-store.s3.amazonaws.com/0/75277/79c091c9-1eb0-02c5-2cd8-3efaa948f01e.png)



## CREATE
### Ajax化

先ほどのリンクに属性を追加します。

```erb:index.html.erb
<%= link_to 'New User', new_user_path, remote: true, class: "btn btn-lg, btn-primary" %>
```
remote: trueとし、呼び出し先を```new.html.erb```から```new.js.erb```に変更します。

### new.js.erbの作成

```bash:terminal
$ cd blog/app/views/users
$ touch new.js.erb
```

```erb:new.js.erb
$("#user-modal").html("<%= escape_javascript(render 'form') %>")
$("#user-modal").modal("show")
```


上記は```index.html.erb```の```id="user-modal"```の```<div>```タグに対し、新規作成用formをレンダリング・表示する。

### _form.html.erbの編集

Bootstrapを用いて```_form.html.erb```をmodal formとして表示させるため、```<div>```タグで囲みます。

```erb:_form.html.erb
<div class="modal-dialog" role="document">
  <div class="modal-content">
    <%= form_with(model: user, local: true) do |form| %>
      <% if user.errors.any? %>
        <div id="error_explanation">
          <h2><%= pluralize(user.errors.count, "error") %> prohibited this user from being saved:</h2>
          <ul>
          <% user.errors.full_messages.each do |message| %>
            <li><%= message %></li>
          <% end %>
          </ul>
        </div>
      <% end %>
      
      <%= form.label :name %>
      <%= form.text_field :name %>
        
      <div class="actions">
        <%= form.submit %>
      </div>
    <% end %>
  </div>
</div>
```

```erb:足したコード
<div class="modal-dialog" role="document">
  <div class="modal-content">
  ...
  ...
  </div>
</div>
```

ただこれだけですと、modal formが表示されません。
ボタンを押下すると、terminalにrailsログが出力されます。

```terminal:terminal
ActionView::Template::Error (undefined local variable or method `user' for #<#<Class:0x00007fc68f71bc10>:0x00007fc69165c1b0>
Did you mean?  @user):
```

定義していないローカル変数（メソッド）としてuserがエラーとなっています。ログの通り、インスタンス変数化しましょう(@user)

```erb:_form.html.erb
<div class="modal-dialog" role="document">
  <div class="modal-content">
    <%= form_with(model: @user, local: true) do |form| %>
      <% if @user.errors.any? %>
        <div id="error_explanation">
          <h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:</h2>
          <ul>
          <% @user.errors.full_messages.each do |message| %>
            <li><%= message %></li>
...
```

すると、

![modal.png](https://qiita-image-store.s3.amazonaws.com/0/75277/e7d996a5-9681-7b63-1bdb-ee245cb6012d.png)

おお、modal formが表示されました。
ただ、これだけだとダサいので、Bootstrapを適用していきます。

```erb:_form.html.erb
<div class="modal-dialog" role="document">
  <div class="modal-content">
    <%= form_with(model: @user, local: true) do |form| %>
    <% if @user.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:</h2>
      <ul>
        <% @user.errors.full_messages.each do |message| %>
        <li><%= message %></li>
        <% end %>
      </ul>
    </div>
    <% end %>
    <div class="modal-header">
      <h5 class="modal-title">New User</h5>
      <button type="button" class="close" data-dismiss="modal" aria-label="Close">
        <span aria-hidden="true">&times;</span>
      </button>
    </div>
    <div class="modal-body">

      <div class="form-group field">
        <%= form.label :name , class: "form-control-label"%>
        <%= form.text_field :name, class: "form-control" %>
      </div>
    </div>
    <div class="modal-footer actions">
      <%= form.submit class: "btn btn-primary"%>
    </div>

    <% end %>
  </div>
</div>
```

![modal-bootstrap.png](https://qiita-image-store.s3.amazonaws.com/0/75277/bfd34d5e-5e8d-4c09-7be3-d6555e3bb88b.png)

変更点：

* Modal Headerの追加
* Modal Headerにタイトル追加
* Modal Headerに「×」(Close)ボタンを追加
* Modal Bodyの追加
* Modal Bodyにラベルとテキストフィールドを追加
* ラベルとテキストフィールドをform-groupクラスのdivタグで囲み、それぞれに、form-control-labelとform-controlクラスを適用
* Modal Footerの追加
* Modal FooterにSubmitボタン追加
* Submitボタンにbtnとbtn-lgとbtn-primaryクラスを適用

見た目はそれっぽくなりました。

### 新規作成処理のAjax化

前述の```app/views/users/_form.html.erb```に```remote```オプションをつけて、javascript指定でコントローラに流れるようにします。

```erb:_form.html.erb
<%= form_with(model: @user, local: true) do |form| %>
⬇︎local: trueをremote: trueに修正
<%= form_with(model: @user, remote: true) do |form| %>
```

また、```app/controllers/user_controller.rb```の```create```メソッドを修正します。

```rb:user_controller.rb
  # POST /users
  # POST /users.json
  def create
    @user = User.new(user_params)

    respond_to do |format|
      if @user.save
        format.html { redirect_to @user, notice: 'User was successfully created.' }
        format.json { render :show, status: :created, location: @user }
        # 追加
        format.js { @status = "success" }
      else
        format.html { render :new }
        format.json { render json: @user.errors, status: :unprocessable_entity }
        # 追加
        format.js { @status = "fail" }
      end
    end
  end
```

この```create```メソッドに```format.js{}```を記述します。
DBにINSERTしたのち、インスタンス変数@statusに成功の場合はsuccess、失敗の場合はfailを代入しています。
モーダルフォームを閉じるまでに処理結果によってAjax処理をしています。

### 登録後の処理

上記```create```メソッド処理後、```remote: true```にしているため、```create.html.erb```ではなく```create.js.erb```を呼びます。```app/views/users/create.js.erb```を作成します。

```erb:create.js.erb
<% if @status == 'success' %>
    $("tbody").append("<%= j(render("tr", user: @user)) %>");
<%  elsif @status == 'fail' %>
    alert('error!');
<% end %>
$("#user-modal").modal("hide");
```

先ほど、インスタンス変数@statusに処理成功・失敗時にsuccess・failを代入しました。
成功時は```tbody```タグに```_tr.html.erb```を```user```をインスタンス変数@userを用いてappendしています。
失敗時はalertを表示しています。
そのあと、モーダルフォームを非表示にしています。


```erb:app/views/users/_tr.html.erb
<tr>  
    <td><%= user.name %></td>
    <td><%= link_to 'Show', user %></td>
    <td><%= link_to 'Edit', edit_user_path(user) %></td>
    <td><%= link_to 'Destroy', user, method: :delete, data: { confirm: 'Are you sure?'} %></td>
</tr>
```

### 試す!!

上記ののち、アプリ起動して
ボタン押下→モーダルフォーム表示→name入力→Submit
と操作しましたが、モーダルフォームが非表示になりませんでした。
サーバのコンソールログは200をレスポンスとして返しており、登録処理でも失敗していない模様。
実際、```http://localhost:3000/users```を再読み込みしましたところ、先ほど登録したデータが表示される。

JS側で問題かと思い、Chromeの開発者モードで開くと、JSでエラーが発生している。

```text
...
Uncaught SyntaxError
    at processResponse (rails-ujs.XXX
...
```

ブレークポイントを配置し、調べてもよくわからず
rails-ujsで調べてみると[こちら](https://qiita.com/fursich/items/c2ab11acad918000a8ea)に興味深いことが

```text
5.1からjquery依存が廃止されたのに伴い､ujs関連はrails-ujsとして切り出されたようです｡
```

rails-ujsが足りないから発生したのか？

Gemfileをみても、rails-ujsらしき記載はなく

で入れてみる。

```text:Gemfile
gem 'rails-ujs'
```

```bash:terminal
$ bundler install
```

とすると、

```bash:terminal
The dependency tzinfo-data (>= 0) will be unused by any of the platforms Bundler is installing for. Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`.
Fetching gem metadata from https://rubygems.org/............
Fetching gem metadata from https://rubygems.org/.
Resolving dependencies...
Using rake 12.3.2

...

Fetching rails-ujs 0.1.0
Installing rails-ujs 0.1.0

...
```

インストールされた。

最初の方で```application.js```には```//= require rails-ujs```と記載されているので、てっきりインストールされているとばかり思っていましたが。

再度操作すると、
![modal.gif](https://qiita-image-store.s3.amazonaws.com/0/75277/c64be261-68f1-b1c4-48fd-9171edf18ec9.gif)

できた!!!

## UPDATE(DETAIL)

目標：一覧表の１行を押下すると、更新Modalが表示される

### 一覧表にBootstrapを当ててみる

デフォルトの一覧表は、イケていなかったのでまずBootstrapを当てます。

```erb:index.html.erb
...
<table class="table table-hover"> 
  <thead>
...
```

とりあえず、```class="table table-hover"```を適用し、Bootstrapのテーブルデザインと、rowをhoverすると色が変化するtable-hoverを適用しました。

![table.png](https://qiita-image-store.s3.amazonaws.com/0/75277/e1018cba-66a6-7624-cc68-1965cac22939.png)

### row押下でModalの表示

以前、[こちら](https://www.webantena.net/javascriptjquery/jquery-tr-data-href/)を参考に、row押下で詳細画面へ遷移するようにはできました。

しかし、今回はModalで表示したい！(できるなら楽して)ということで

trタグにこんなコードを書いてみました。

```erb:index.html.erb
<tr class="tbody_link clickable" data-href="<%= url_for controller: 'users', action: 'edit', id: user %>">
```

```javascript:js
<script>
    jQuery( function($) {
      $('tbody tr[data-href]').click( function() {
        window.location = $(this).attr('data-href');
      }).find('a').hover( function() {
        $(this).parents('tr').unbind('click');
        }, function() {
          $(this).parents('tr').click( function() {
          window.location = $(this).attr('data-href');
        });
      });
    });
</script>
```

だめでした…

結果として、遷移しているし(ajaxじゃないし)

link_to remote: trueを使ってなんとか、できないものか…

1. trタグをlink_toで囲む→そもそも、リンクが表示されない(生成されているがtableタグ外になぜか生成)：×
2. 上記ソースでajaxを呼ぶようにする→複雑になる、登録はlink_to remote: trueで書いているのにソースの統一ができなくなる：△
3. trタグの中にlink_toを書く→できたけど、trタグ全体に反映されない：△
4. ３を修正し、trタグ全体までlink_to（要はaタグ)の適用領域を広げる：○

ということで４がうまういきました。

### trタグ全体にaタグを適用

[こちら](https://www.billionwallet.com/homepage/cell_link.html)を参考に、CSSを適用しました。

```css:css
table tr a {
  display:block;
  width:100%;
  height:100%;
}

table tr a:hover {
  text-decoration: none;
}
```

上の```table tr a```だけでもいいのですが、hoverした時に、リンクですよの下線を表示させたくなかったので、```table tr a:hover```を適用しました。

### trタグ押下時の処理ajax化

trタグにaタグ(link_to)を書きます。

```erb:index.html.erb
  <tbody id="user-tbody">
    <% @users.each do |user| %>
      <tr>
        <td><%= user.id %></td>
        <td><%= user.name %></td>
        <td><%= link_to 'Show', user %></td>
        <td><%= link_to 'Edit', edit_user_path(user) %></td>
        <td><%= link_to 'Destroy', user, method: :delete, data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
⬇︎
  <tbody id="user-tbody">
    <% @users.each do |user| %>
      <tr>
        <td><%= link_to user.id, edit_user_path(user), remote: true %></td>
        <td><%= link_to user.name, edit_user_path(user), remote: true %></td>
    <% end %>
  </tbody>
```

遷移先に```edit_user_path(user)```と```remote: true```を指定し、```edit.html.erb```から```edit.js.erb```に向き先を変更しました。

### 更新

まず、```edit.js.erb```を編集します

```erb:edit.js.erb
$("#user-modal").html("<%= escape_javascript(render 'form') %>");
$("#user-modal").modal("show");
```

内容としては、```_form.html.erb```を生成していますが、これは、新規作成の時にも、同じテンプレートを使用しています。うまくいくのでしょうか？

これは、テンプレートの```form_with()```の挙動が吸収してくれるようです。

[この記事](https://qiita.com/kakiuchis/items/9ab8dc212ed5c57a2ddd)によると、```@user```オブジェクトが空オブジェクトの場合、```create```アクション、つまり新規登録アクションを呼び出し、```@user```オブジェクトが空オブジェクトでない場合、editアクション、つまり更新アクションを呼び出します。
実際```submit```ボタンもラベルが変わっています。

そして、```submit```ボタンを押すと、```update```アクションが呼ばれます。```user_controller.rb```を編集します。

```rb:user_controller.rb
  def update
    respond_to do |format|
      if @user.update(user_params)
        format.html { redirect_to @user, notice: 'User was successfully updated.' }
        format.json { render :show, status: :ok, location: @user }
        format.js { @status = "success"}
      else
        format.html { render :edit }
        format.json { render json: @user.errors, status: :unprocessable_entity }
        format.js { @status = "fail" }
      end
    end
  end
```

これも```create```アクション同様、```format.js```を追記します。そして、```update.js.erb```を作成します。

```erb:update.js.erb
<% if @status == 'success' %>
<%  elsif @status == 'fail' %>
    alert('error!');
<% end %>
// modal-formを消す
$("#user-modal").modal("hide");
```

以前作った、```create.js.erb```と同じ処理、UPDATE結果で変化させています。

### 削除
削除を実装します。
下記Gemでconfirmをalertではなく、modalで表示してくれます。

```:Gemfile
gem 'data-confirm-modal'
```

```bash:terminal
$ cd blog
$ bundle install
```
そして、```application.js```に以下を記載します。

```js:application.js
//= require data-confirm-modal
```

```erb:*.html.erb
<%= link_to 'Destroy', @user, method: :delete,
 title: 'Destory User', data: { confirm: 'Are you sure?',
                                commit: 'Destroy',
                                cancel: 'Cancel' 
                              }, class: "btn btn-danger" %>

```
各属性について

* title: modalのヘッダのタイトル
* data-confirm: bodyのテキスト
* data-commit: 決定ボタンのテキスト
* data-cancel: キャンセルボタンのテキスト

実際にこんな感じです。

![destory-modal.png](https://qiita-image-store.s3.amazonaws.com/0/75277/911e0acd-891b-b85f-e0e2-7e000b938e35.png)

処理自体は修正していません。

## 最終的に…

一覧表示・新規作成・更新・削除までできるようになりました。

![ezgif.com-video-to-gif.gif](https://qiita-image-store.s3.amazonaws.com/0/75277/87469c70-48d1-3a73-3d57-999c09fdf962.gif)

## 今後の展望

今後は、これに対して一覧検索機能をつけたいと思います！
一旦はこれで、要件は満たしたかな…


## 参考URL
[Rails5.1のform_withはデフォルトでremote: true](https://qiita.com/kamelo151515/items/e7c2225dc19936a31756)
[Rails5とBootetrapでモーダルウィンドウを作るまで](https://logist3.com/rails5-bootstrap-modalwindow/)
[Modal · Bootstrap](https://v4-alpha.getbootstrap.com/components/modal/)
[覚えておくと超便利！Ruby on Railsのscaffoldの使い方【初心者向け】](https://techacademy.jp/magazine/7204)
[[Rails 5.1] 'form_with' APIドキュメント完全翻訳](https://techracho.bpsinc.jp/hachi8833/2017_05_01/39502)
[Rails5.1のform_withはデフォルトでremote: true](https://qiita.com/kamelo151515/items/e7c2225dc19936a31756)
[Qiitaにアップする画面動画をgifアニメで簡単に作ろう](https://qiita.com/bohebohechan/items/f8bf6d4bbe0f14497a7b)
[Rails5と BootstrapでAjax-modalform](https://qiita.com/niwaken/items/ffbce52fb024fd369f24)
[[JS]jQuery を利用して table の tr にリンク先を指定できるようにする方法](https://www.webantena.net/javascriptjquery/jquery-tr-data-href/)
[【CSS:link】テーブルでセル全体をリンクする](https://www.billionwallet.com/homepage/cell_link.html)
[Rails5のform_withのデータの行き先](https://qiita.com/kakiuchis/items/9ab8dc212ed5c57a2ddd)
[Data-Confirm Modalを使って Rails の削除確認ダイアログをいい感じにする](https://fiveteesixone.lackland.io/2015/02/06/rails-nice-delete-verification-modal/)
[GitHub - ifad/data-confirm-modal: Makes Rails' link_to confirm: 'foo' build a Bootstrap Modal instead of calling the browser's confirm() API.](https://github.com/ifad/data-confirm-modal)