devise+ウィザード形式でのユーザ登録

##この記事で学べること
deviseを導入した状態でウィザード形式(画面遷移がある)のユーザ登録する方法
ウィザード形式での遷移時のバリデーションチェックをする方法

##概要
ウィザード形式の導入手順(sessionメソッドを使う)
deviseを導入しているときに自分でユーザ新規登録機能を実装するときの手順

新規登録→自分でコードを書く(ウィザード形式)
ログイン/編集/ログアウト→deviseのメソッドを活用
　　　　　　  ↓
sign_in/current_userなどのdeviseのメソッドを活用できる
自分で記述するコード量をへらすことができる

なぜdeviseのコントローラーをカスタムしないのか
→頑張ればできるかもだけど難しそうだし自分で実装したほうが楽な気がする😆

<img width="861" alt="スクリーンショット 2019-11-17 23.34.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/465913/06e02815-0f98-9087-85ac-abef3ada53fb.png">
<img width="788" alt="スクリーンショット 2019-11-17 23.35.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/465913/b899cfc3-81fe-dd90-91ce-faee84a93a69.png">
##開発環境
MacOS Mojave 10.14.6
ruby 2.5.1
Rails 5.2.3
mysql  Ver 14.14 Distrib 5.6.43, for osx10.14

deviseの設定は終わっている前提で進めます

##モデル/テーブル
``` user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

 validates  ...
 下記省略...
end
```
--------------
deviseコマンドで作成したuserテーブルに必要なカラムを追加してください

```schema.rb
create_table "users", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
    t.string "email", default: "", null: false
    t.string "encrypted_password", default: "", null: false
    t.string "reset_password_token"
    t.datetime "reset_password_sent_at"
    t.datetime "remember_created_at"
    t.string "nickname", null: false
    t.string "lastname", null: false
    t.string "firstname", null: false
    t.string "lastname_kana", null: false
    t.string "firstname_kana", null: false
    t.integer "birthday_year", null: false
    t.integer "birthday_month", null: false
    t.integer "birthday_day", null: false
    t.string "phonenumber", null: false
    t.string "image"
    t.text "introduction"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["email"], name: "index_users_on_email", unique: true
    t.index ["phonenumber"], name: "index_users_on_phonenumber", unique: true
    t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
  end
```
-------
##ルーティング
deviseの機能とは別なので新規登録用のコントローラーとルーティングを設定。
画面の遷移回数に応じてstepの数は調整してください

```routes.rb
 resources :signups, only: [:index] do
    collection do
      get 'step1'   # 登録情報の一部を入力してもらう
      post 'step1', to: 'signups#step1_validates'  #step1で入力したデータのバリデーションチャックをする(詳しくはコントローラーで説明) render後にリロードしてもエラーが出ないようにget'step1'とURLは同じに設定する
      get 'step2'  # 残りの登録情報を入力してもらう
      post 'step2', to: 'signups#create' # 入力した情報すべてを保存 こっちもrenderするなら揃える
    end
 省略...
  end
```
collectionについては下記を参考にしてください
>[railsのroutes.rbのmemberとcollectionの違いをわかりやすく解説してみた。〜rails初心者から中級者へ〜](https://qiita.com/hirokihello/items/fa82863ab10a3052d2ff)

##コントローラー
stepごとの入力データはsessionメソッドで保持する
deviseを導入しているとparamsでpasswordを渡そうとしても隠されてsessionに代入することができなかった(詳しくはどうなっているのかはわからなかったです。詳しい方は教えていただけると助かります!)
　　　　　　　　　　↓
　　password_confirmationだと隠されない
　　　　　　　　　　↓
　passwordのかわりにpassword_confirmation代入

セキュリティ的に大丈夫なのかは不明なのでいいやり方を知ってる方がいたら教えていただけると助かります(_ _)
保存されるパスワード自体は暗号化されます

詳しい説明はコントローラーファイルにコメントしてます
sessionについての詳しい説明は下記を参考にしてください
>[【Rails】Sessionの使い方について](https://qiita.com/ozackiee/items/4ee774c81b2a0c571c05)



```signups_controller.rb
class SignupsController < ApplicationController
  def step1
    @user = User.new
  end

  def step1_validates  #step1で入力された情報のバリデーションチャックをするためのアクション
    # バリデーションチャックするためにインスタンスを作成
    @user = set_user_when_email(user_params)
    #バリデーションチャック
    @user.valid?  #無効な値の場合はfalseとインスタンスに対してエラーメッセージを追加してくれる

    #今回はstep1でphonenumberを入力しないので設定しているpresence: trueに引っかかっている
    #このメソッドでphonenumberのエラー内容を削除してバリデーションを通過させる。メソッドはprivateにあります
    skip_phonenumber_validate(@user.errors) 
    # verify_recaptcha~~ はロボット確認の為のメソッドで今回は殆ど関係ないです。詳しくはgem 'recaptcha'で調べてください
    if verify_recaptcha(model: @user, message: "選択してください") && @user.errors.messages.blank? && @user.errors.details.blank?
      create_session(user_params)
      redirect_to step2_signups_path  #step1で入力したデータにバリデーションエラーがない場合はstep2に遷移する
    else
      @user.errors.messages[:birthday_day] = change_birthday_validate_message(@user) #エラーメッセージを変更するメソッドで今回は関係ないので説明は省略します
      render :step1
    end
  end

  def step2
    #入力フォームのためのインスタンスを作成
    @user = User.new
  end

  def create
    set_user_with_session
    @user[:phonenumber] = user_params[:phonenumber] ## step2で入力した電話番号を代入
    if @user.save
      sign_in User.find(@user.id) unless user_signed_in?
      delete_session
      redirect_to addresses_path
    else
      render :step2
    end
  end

  private

  def user_params
    params.require(:user).permit(:nickname, :email, :password, :password_confirmation, :lastname, :firstname,
                                 :lastname_kana, :firstname_kana, :birthday_year,
                                 :birthday_month, :birthday_day, :phonenumber)
  end

  def set_user_when_email(user_params)
    User.new(
      nickname: user_params[:nickname],
      email: user_params[:email],
      password: user_params[:password_confirmation],
      password_confirmation: user_params[:password_confirmation],
      lastname: user_params[:lastname],
      firstname: user_params[:firstname],
      lastname_kana: user_params[:lastname_kana],
      firstname_kana: user_params[:firstname_kana],
      birthday_year: user_params[:birthday_year],
      birthday_month: user_params[:birthday_month],
      birthday_day: user_params[:birthday_day]
    )
  end

  def set_user_with_session
    @user = User.new(
      nickname: session[:nickname],
      email: session[:email],
      password: session[:password_confirmation],
      password_confirmation: session[:password_confirmation],
      lastname: session[:lastname],
      firstname: session[:firstname],
      lastname_kana: session[:lastname_kana],
      firstname_kana: session[:firstname_kana],
      birthday_year: session[:birthday_year],
      birthday_month: session[:birthday_month],
      birthday_day: session[:birthday_day]
    )
  end

  # 電話番号はstep1で入力しないのでバリデーションエラーがあったときにスキップさせるためのメソッド
  # step1で入力しないカラムのバリデーションエラーメッセージを削除する。
  def skip_phonenumber_validate(errors)
    errors.messages.delete(:phonenumber)  #stepの回数や入力するデータに合わせて変更してください
    errors.details.delete(:phonenumber)
  end

  # 生年月日のどれかにひとつでもバリデーションエラーがあった場合は同じエラーメッセージを表示する
  def change_birthday_validate_message(user)
    if user.errors.messages[:birthday_year].any? || user.errors.messages[:birthday_month].any? || user.errors.messages[:birthday_day].any?
      user.errors.messages.delete(:birthday_year)
      user.errors.messages.delete(:birthday_month)
      user.errors.messages[:birthday_year] = ["生年月日は正しく入力してください"]
      user.errors.messages[:birthday_year]
    end
  end

  def create_session(user_params)
    session[:nickname] = user_params[:nickname]
    session[:email] = user_params[:email]
    session[:password] = user_params[:password_confirmation]
    session[:password_confirmation] = user_params[:password_confirmation]
    session[:lastname] = user_params[:lastname]
    session[:firstname] = user_params[:firstname]
    session[:lastname_kana] = user_params[:lastname_kana]
    session[:firstname_kana] = user_params[:firstname_kana]
    session[:birthday_year] = user_params[:birthday_year]
    session[:birthday_month] = user_params[:birthday_month]
    session[:birthday_day] = user_params[:birthday_day]
  end

  def delete_session
    session.delete(:nickname)
    session.delete(:email)
    session.delete(:password)
    session.delete(:password_confirmation)
    session.delete(:lastname)
    session.delete(:firstname)
    session.delete(:lastname_kana)
    session.delete(:firstname_kana)
    session.delete(:birthday_year)
    session.delete(:birthday_month)
    session.delete(:birthday_day)
    session.delete(:pid)
    session.delete(:provider)
  end
end
```

今回はバリデーションチェックをするためにpostアクションを使ったが、before_acitonでやる方法もある
→before_acitonだとrender後にリロードするとエラーが出たので採用しませんでした。なぜエラーが出るかは下記を参考にしてください
>[現場から学ぶ、RailsのControllerアンチパターン](https://qiita.com/yuyasat/items/49e3296f3c64fccc7811)
　アンチパターン2で解説されてます

なにか解決方法があるかもしれないのでbefore_aciton使用するときのリンク貼っておきます
>[ウィザード形式の登録フォームでバリデーション](https://qiita.com/ATORA1992/items/c073620be3469bfa1f54)

##view
普段どおりに入力フォームを作ってください

```app/views/signups/step1.html.haml
= form_for @user, url: step1_signups_path, method: :post do |f|
      - required = '必須'
      .user_information
        .field
          .field-label
            = f.label :"ニックネーム", class: 'left-label'
            %span.min #{required}
　　　　　　#renderされたときにエラーの有無でフォームを変えている
          - if @user.errors.messages[:nickname].any?
            .field-input
              = f.text_field :nickname, autofocus: true, class: 'user_nickname_error', id: 'nickname', placeholder: '例）フリマ123'
              = render 'shard/signups_errors', errors: @user.errors.full_messages_for(:nickname)
          - else
            .field-input
              = f.text_field :nickname, autofocus: true, class: 'user_nickname', id: 'nickname', placeholder: '例）フリマ123'

下記省略...
```

今回は2ページで全情報を入力するので残りのデータを入力するためのフォームを作る

```app/views/signups/step2.html.haml
 = form_for @user, url: step2_signups_path, method: :post do |f|
      .user_phonenumber
        .field
          .number_presentment
            = f.label :"携帯電話の番号"
          - if @user.errors.messages[:phonenumber].any?
            .number_input
            = f.text_field :phonenumber, class: "number_text_error", id: 'phonenumber_error', placeholder: '携帯電話の番号を入力'
            = render 'shard/signups_errors', errors: @user.errors.full_messages_for(:phonenumber)
          - else
            .number_input
              = f.text_field :phonenumber, class: "number_text", id: 'phonenumber', placeholder: '携帯電話の番号を入力'
        .next_btn
          = f.submit :"次へ進む", class: "next_submit"
```

##最後に
機能は実装できましたが、正しいやり方なのか自信がないので皆さんからの指摘や助言をお待ちしています。
この記事が少しでも学習の助けになればと思います。
簡単ですがテストやomnuauth認証を利用したユーザ登録も記事にしたのでよかったら参考にしてください
>[devise+ウィザード形式でのユーザ登録のRspecテスト](https://qiita.com/tanegashiman/items/edebbe7f1dcd29f511ba)
[devise+omniauth-rails_csrf_protection+[facebook/google]を使ったユーザ登録とログイン](https://qiita.com/tanegashiman/items/53fd8228cc69388493bc)

##参考にした記事
>[[Rails]新規登録画面のウィザードフォーム実装(sessionでデータ保持する）](https://qiita.com/NT90957869/items/56ca4101d7ba37778076)
[[Rails]gemを使わないWizard形式の登録フォームを作った](https://qiita.com/chiha-chiha/items/0013241b30d483d0d212)
[[Rails]ウィザードフォーム実装でfields_forを使って複数のテーブルに保存する](https://qiita.com/NT90957869/items/614842934feff9812203)