DeviseのSign upとLog inをBootstrapでModalポップアップにカスタマイズする

#はじめに
DeviseのSign upとLog inをModalポップアップにしたいという要望は多いと思うが、日本語の情報が見当たらなかったのでrails newするところからの手順を書いておく。
#やり方
Deviseが分からななくても簡単にコピペだけでやれるように細かく書きます。
##1. 新しくプロジェクトを作る
私はプロジェクト名をdevise_modalとした。
`$ rails new devise_modal`
###(1) トップpageを作る。
私はpagesとしましたが、任意です。
`$ rails g controller Pages index`
###(2) routingに作ったトップpageを `localhost:3000` でアクセスできるように追加する。

```ruby:config/routes
Rails.application.routes.draw do
  root 'pages#index'  <=追加rootは特別なので / ではなく # を使う事に注意
  get 'pages/index'   <=rails gで自動作成されたもの
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end
```

##2. Deviseのインストール
Deviseをインストールします。
###(1) GemfileにDeviseを追加

```ruby:/Gemfile
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
途中省略
gem 'devise'　<=一番下に追加
```

###(2) bundle install
`$ bundle install`
###(3) Deviseをインストール
`$ rails generate devise:install`
###(4) Userモデルを作成
`$ rails generate devise Users`
###(5) DBを作る
/config/database.yml の設定が正しいことを確認後に実行する。
`$ rake db:create`
`$ rake db:migrate`
###(6) defaultのdevise viewをカスタマイズするためにアプリにViewを追加する。
`$ rails generate devise:views`
###(7) 動作確認
この時点で基本的にDeviseは動いているので localhost:3000 をブラウザで見てみる。

##3. Bootstrapをインストール
BootstrapのModalを使うのでインストールする。Gemfileに次の2行を一番下に追加する。

```ruby:Gemfile
gem 'bootstrap', '~> 4.1.1'
gem 'jquery-rails'
```

再度bundle installを実行する。
`$ bundle install`

>注意点
sprockets-railsがv2.3.2.以上である必要がある。

```ruby:sprockets-rails`バージョン確認方法
$ bundle show |fgrep sprockets-rails
  * sprockets-rails (3.2.1)
```

##4. カスタマイズ作業
###(1) application.cssをrenameする。
>Railsアプリの作成時に生成されるapplication.cssをapplication.scssにリネームするなどして、.scss(Sass構文の場合は.sass)ファイルを用意する。
`$ mv assets/stylesheets/application.css assets/stylesheets/application.scss`

###(2) application.scssでbootstrapをimportさせる。

```ruby:app/assets/stylesheets/application.scss
@import "bootstrap";
/*
 * 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.
 *
 */
```

>注意点
- Sassファイルでは*= require、*= require_treeを削除する
- Sassファイルではインポートに@importを利用する
- Sassファイルで*= requireを利用すると他のスタイルシートではBootstrapのmixinや変数を利用できなくなるので削除する

###(3) Bootstrapと依存関係を`application.js`に追記
以下の3行を一番下に追加する。

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

>補足
- Bootstrapのtooltipsやpopoverはpopper.jsに依存している
- bootstrapの依存gemにpopper_jsが指定されているため新たにインストールは不要
- コンパイルを高速化したい場合はbootstrap-sprocketsの代わりにbootstrapを指定する

##5. Modal Formの作成
###(1) ポップアップ用Sign upページ
私は _new.html.erb としました。_new.html.erbファイルを下記のように作成する。

```ruby:views/devise/registrations/_new.html.erb
<div class="modal fade" id="signUpModal">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title"><h1>Sign Up</h1></h5>
        <button type="button" class="close" data-dismiss="modal">
          <span>&times;</span>
        </button>
      </div>
      <div class="modal-body">
        <%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
          <%= devise_error_messages! %>

          <div class="field">
            <%= f.label :email %><br />
            <%= f.email_field :email, autofocus: true, autocomplete: "email", size: "30" %>
          </div>

          <div class="field">
            <%= f.label :password %>
            <% if @minimum_password_length %>
              <em>(<%= @minimum_password_length %> characters minimum)</em>
            <% end %><br />
            <%= f.password_field :password, autocomplete: "off", size: "30" %>
          </div>

          <div class="field">
            <%= f.label :password_confirmation %><br />
            <%= f.password_field :password_confirmation, autocomplete: "off", size: "30" %>
          </div>

          <div class="actions">
            <%= f.submit "Sign up", class: 'btn btn-primary' %>
          </div>
        <% end %>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-danger" data-dismiss="modal">Close</button>
      </div>
    </div>
  </div>
</div>
```

###(2) ポップアップ用Log inページ
同様に以下を作成する。

```ruby:views/devise/sessions/_new.html.erb
<div class="modal fade" id="logInModal">
      <div class="modal-dialog">
        <div class="modal-content">
              <div class="modal-header">
                <h5 class="modal-title">Log In</h5>
                <button type="button" class="close" data-dismiss="modal">
                      <span>&times;</span>
                </button>
              </div>
              <div class="modal-body">    
                <div>
                    <h1>Log In</h1>
                    <br>
                    <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
                        <div class="wrap">
                              <div class="field">
                                <label for="email">Email</label>
                                <%= f.email_field :email %>
                              </div>
                        
                             <div class="field">
                                <label for="password">Password</label>
                                <%= f.password_field :password, autocomplete: "off" %>
                              </div><br>

                              <div>
                                  <% if devise_mapping.rememberable? -%>
                                    <div class="field">
                                          <%= f.check_box :remember_me %>
                                          <%= f.label :remember_me %>
                                    </div>
                                  <% end -%>     
                              </div>

                            <div>
                              <%= f.submit "Log in", class: 'btn btn-primary' %>
                            </div> 

                            <div>
                                <% render "devise/shared/links" %>
                            </div>
                            
                        </div>
                      <% end %>
                </div>
              </div>
              <div class="modal-footer">
                <button type="button" class="btn btn-danger" data-dismiss="modal">Close</button>
              </div>
        </div>
    </div>
</div>
```

###(3) ApplicationHelperにメソッドを追加
上記で作ったDeviseフォームをdefaultの場所以外でレンディングするためにいくつかメソッドを追加する。

```ruby:app/helpers/application_helper.rb
module ApplicationHelper
  def resource_name
    :user
  end

  def resource
    @resource ||= User.new
  end

  def resource_class
    User
  end

  def devise_mapping
    @devise_mapping ||= Devise.mappings[:user]
  end
end
```

###(4) 新しいpartialsのレンディング
`application.html.erb`の`<%= yield %>`の下に以下の4行を追加する。ユーザーがログインしていない時のみModalポップアップを出したいので`unless user_signed_in?`で制御する。

```ruby:views/layouts/application.html.erb
<body>
<%= yield %>
<% unless user_signed_in? %>
  <%= render partial: 'devise/registrations/new' %>
  <%= render partial: 'devise/sessions/new' %>
<% end %>
</body>
</html>
```

###(5) ナビバーをレイアウトに作成
以下のように`signup`と`login`リンクのナビバーをレイアウトに作ってModalのトリガーにする。

```ruby:app/views/layouts/_navbar.html.erb
<nav class="navbar navbar-expand-lg navbar-light bg-light">
    <a class="navbar-brand" href="#">Navbar</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarNav">
      <ul class="navbar-nav">
        <li class="nav-item active">
          <a class="nav-link" type="button" data-toggle="modal" data-target="#signUpModal">Sign Up</span></a>
        </li>
        <li class="nav-item">
          <a class="nav-link" type="button" data-toggle="modal" data-target="#logInModal">Log In</a>
        </li>
      </ul>
    </div>
</nav>
```

###(6) application.html.erb
これでlayouts/application.html.erbでModalがレンディングできるはず。完成したapplication.html.erbは次のようになる。

```ruby:app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
  <title>DeviseModals</title>
  <%= csrf_meta_tags %>
  <%= csp_meta_tag %>

  <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
  <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>

<body>
<%= render 'layouts/navbar' %>
<%= yield %>
<% unless user_signed_in? %>
  <%= render partial: 'devise/registrations/new' %>
  <%= render partial: 'devise/sessions/new' %>
<% end %>
</body>
</html>
```

###(7) ナビバーでログイン状態の制御
ユーザーがログインしている時はModalは表示されないようにしているので、ナビバーも同様にユーザーがログインしている時はログアウトボタンをログイン・サインアップに代わりに表示させる制御を作る。

```ruby:app/views/layouts/_navbar.html.erb
<nav class="navbar navbar-expand-lg navbar-light bg-light">
  <a class="navbar-brand" href="#">Navbar</a>
  <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
    <span class="navbar-toggler-icon"></span>
  </button>
  <div class="collapse navbar-collapse" id="navbarNav">
    <ul class="navbar-nav">
      <% if user_signed_in? %>
        <li class="nav-item active">
          <%= link_to('Logout', destroy_user_session_path, method: :delete) %>
        </li>
      <% else %>
        <li class="nav-item active">
          <a class="nav-link" data-toggle="modal" data-target="#signUpModal">Sign Up</span></a>
        </li>
        <li class="nav-item">
          <a class="nav-link" data-toggle="modal" data-target="#logInModal">Log In</a>
        </li>
      <% end %>
    </ul>
  </div>
</nav>
```

##参考記事
https://stackoverflow.com/questions/32237818/devise-sign-in-sign-up-in-popup
https://zayne.io/blog/rails-devise-bootstrap-sign-up-and-login-modals
https://qiita.com/NaokiIshimura/items/c8db09daefff5c11dadf

#終わりに
実際に使う場合には画面遷移がDeviseオリジナルのLoginやSign upページに行かないようにカスタマイズしないとModalではないLoginやSign upページが表示されてしまうので注意する。

#関連記事
Rails5でdeviseをAmazon SES SMTPサーバーを使って動かす
https://qiita.com/NYC-Blue/items/e6bf37388c44ea5936c6