【rails】deviseでのバリデーションチェックのかけ方【同期通信】

おはようございます。
今日はデバイスのバリデーションについて書いていきたいと思います。
実装後のイメージ動画はこちらになります。

gyazo.com

実装の流れ

実装の流れは以下の①〜④になります。すごく簡単に実装することができます。
① バリデーションを定義
② エラーメッセージを表示させるようviewを編集
③ ja.ymlを編集しカラム名の日本語化
CSSで見た目を整える


① バリデーションを定義

下記が実際のコードです。簡略化のため実装に直接関係のない記載は省略しています。

class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable   #:validatableを削除

  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
  VALID_PASSWORD_REGEX = /\A(?=.*[a-z])(?=.*\d)[a-z\d]{8,}+\z/
  validates :nickname, presence: true, length: { in: 1..10 }
  validates :email, presence: true, uniqueness: true, format: { with: VALID_EMAIL_REGEX, message: "を○○@○○.○○の形式で入力して下さい" }, length: { maximum: 255 }
  validates :password, presence: true, length: { maximum: 75 }, format: { with: VALID_PASSWORD_REGEX, message: "を半角英数字8文字以上で入力して下さい" }
end


まず、deviseを導入するとデフォルトのバリデーションが自動で実装されているので、これを無効にするため:validatableを削除します。後はかけたいバリデーションを記載していきます。

VALID_EMAIL_REGEXとVALID_PASSWORD_REGEX

メールアドレスとパスワードの正規表現を記載しています。メールアドレスは○○@○○.○○の形式のとき、パスワードは半角英数字8文字以上のときのみマッチするようになっています。パスワードの正規表現についてはこちらに詳しく書いたので参考にどうぞ
https://shun-0211.hatenablog.com/entry/2020/06/03/205335

nicknameカラムのバリデーション

nicknameカラムには「空でないこと」「1文字以上8文字以下であること」のバリデーションをかけています。

emailカラムのバリデーション

emailカラムには「空でないこと」「同じメールアドレスが存在しないこと」「定義した正規表現にマッチすること」「255文字以下であること」のバリデーションをかけています。「定義した正規表現にマッチすること」のデフォルトのエラーメッセージが「不正な値です」なので、message:以下でエラーメッセージを定義しています。

passwordカラムのバリデーション

passwordカラムには「空でないこと」「75文字以下であること」「半角英数字8文字以上であること」のバリデーションをかけています。emailカラムと同じくエラーメッセージを定義し直しています。


② エラーメッセージを表示させるようviewを編集

編集したviewの一部を下記に示します。

.input__form
    = form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f|
      .input__form__nickname
        .input__form__nickname__title
          ニックネーム
          %span.required
            必須
        .input__form__nickname__form
          = f.text_field :nickname, placeholder: "タロウ", class: "nickname__form"
      - if resource.errors.include?(:nickname)    #以下追加
        %ul.nickname__error__messages
          - resource.errors.full_messages_for(:nickname).each do |error_message| 
            %li
              = error_message

- if resource.errors.include?(:nickname)

この記述により、バリデーションチェックによりnicknameカラムにエラーメッセージがあるときのみ以下のHTMLを表示させます。この記述により、フォームのすぐ下にエラーメッセージを表示させることが可能になります。

- resource.errors.full_messages_for(:nickname).each do |error_message|

この記述により、nicknameカラムのエラーメッセージを配列に格納し、一つ一つのメッセージを表示させることができます。


③ ja.ymlを編集しカラム名の日本語化

デフォルトのままではカラム名が英語で表示されるため、ja.ymlを編集します。日本語化するのに必要な処理は省略しています。詳しくは下記の記事をご覧ください。

ja:
  activerecord:
    models:
      user: ユーザー
    attributes:
      user:
        nickname: ニックネーム
        email: メールアドレス
        password: パスワード


CSSで見た目を整える

最後にCSSで見た目を整えます。

.email__error__messages{
   font-size: 14px;
   color: red;
   padding-bottom: 10px;
}

基本的には以上で実装は終わりですが、一点だけ注意点です。
バリデーションエラーが出る状態で送信ボタンを押し画面遷移するとviewが崩れる場合があります。これはエラー時にinputタグをfield_with_errorsのクラス名を持ったdivタグで囲う仕様になっているからです。これにより、viewが崩れる場合があります。ので、整えてあげる必要がある場合があるのでご注意を。

終わり!



以上になります。ここまで読んでくださり、ありがとうございました。分かりにくいやアドバイス等ありましたらコメントくださると幸いです。では!

【正規表現】パスワードの正規表現をひもとく

こんばんわ!
今日はパスワードの正規表現について書いていきたいと思います。

参考サイトで「英数字それぞれ1文字以上を含む8文字以上100文字以下の文字列」を正規表現で表すと下のようになるとあったので、今回はこれについてひもといて丸裸にしていきたいと思います。

/\A(?=.*[a-z])(?=.*\d)[a-z\d]{8,100}+\z/i



まず、単語ごとの意味を詳しく見ていきます。

文字 説明
. 任意の1文字にマッチ
+ 直前の文字が一回以上繰り返す場合にマッチ
* 直前の文字が0回以上繰り返す場合にマッチ
\A 文頭を示す
\z 文末を示す
{n, m} 直前の文字がn文字以上、m文字以下の場合にマッチ
[...] 角括弧の中の文字いずれか1文字にマッチ
\d 数字にマッチ
i 大文字小文字を区別しない検索



パスワードの正規表現ではAND(英字を1文字以上含むかつ数字を1文字以上含む)を表現するがあります。ANDを表現するのには位置指定子を使います。


位置指定子を使い、英字を1文字以上含むかつ数字を1文字以上含む場合、文頭をマッチさせる

位置指定子には大きく4種類ありますが、上の例で使われているのは肯定先読みなので、今回は肯定先読みを例を使って説明します。

肯定先読み

(?=条件)で表される位置指定子で位置を指定します。条件にマッチしたときその文字の前の位置を指定します。
例1) /(?=[a-z])/
(結果) 1234 a
.................↑

先読みというのは誤解しやすいですが、(自分も誤解してました)、前の位置を指定します。先読みはlook-aheadとした方が直感的に理解しやすいです。
次に肯定先読みをつかった検索の例を示します。
例2) /4(?=[a-z])/
(結果) 123 4 a
.................↑

4と例1の結果の位置で囲われた箇所がマッチするので、例2では4がマッチします。

例3) /\A(?=.*[a-z])/
(結果) abcde
.........↑

少し複雑になってきましたが、例3では英字が含まれていた場合、文頭にマッチします。

位置指定子は文字ではないため、複数条件を追加できます。この性質を利用してANDを表現できます。
例4) /\A(?=.*[a-z])(?=.*\d)/
(結果) abcde1
.........↑

例の4では、英字、数字いずれも一文字以上含まれていない限りマッチしません。


文字列全体をマッチさせる

ここまでで英字を1文字以上含むかつ数字を1文字以上含むと文頭にマッチさせることができています。ここまでくれば後は簡単です。

例5) /\A(?=.*[a-z])(?=.*\d)[a-z\d]+/
(結果) abcd123
..........文全体にマッチ

まず、例4)に[a-z\d]を足すことで、文頭とa~zまたは数字で囲まれた文字がマッチします。つまり、文の最初の文字(例でいうとa)のみがマッチします。そこに+をつけることで、文全体をマッチさせることができます。

また、8文字以上100文字以下という制限も加えたいので、{8, 100}を足します。
例6) /\A(?=.*[a-z])(?=.*\d)[a-z\d]{8,100}+/
(結果) abcd1234
..........文全体にマッチ

そして最後文末にメタ文字である\zを足せばパスワードの正規表現の完成です。


まとめ

以上がパスワードの正規表現を詳しく見てみた記事になります。
ここまで読んでくださり、ありがとうございました。
分かりにくいやアドバイス等ありましたらコメントくださると幸いです。では!

【Ajax通信】いいね機能の非同期通信での実装

おはようございます。
今日はいいね機能の非同期通信での実装を書いていきたいと思います。今回は同期通信での実装はできているものとして書いていきますのでご承知ください。

前提

同期通信でのいいね機能の実装が済んでいる状態
(同期通信のいいね機能がまだできていないのであればまずはそちらを行うことを勧めます。)

実装の流れ

① 部分テンプレートの作成とオプション追加
② コントローラーでのページ遷移の記述削除
③ create.js.hamlとdestroy.js.hamlファイルを作成し、いいねボタン部のみviewを変更

① 部分テンプレートの作成とオプション追加

いいね機能を実装するにあたり、いいねボタン部分のviewを部分テンプレートに移します。なぜ移すかというといいねが押された際、当然viewを変更しないといけませんが、(いいねの数が変わるため)そのviewの変更を部分テンプレートを使って行います。そのため、いいねボタン部分を部分テンプレートで書く必要があります。
また、非同期通信を行うためのオプション(remote: true)をlink_toに付けます。

_like.html.haml

- if user_signed_in?
  - if set_list.liked_by?(current_user)
    = link_to event_set_list_likes_path(event, set_list), method: :delete, remote: true do
      .like__box
        .heart__mark__unregistered
          &#9829
        .like__number
          = set_list.likes.count
  - else
    = link_to event_set_list_likes_path(event, set_list), method: :post, remote: true do
      .like__box
        .heart__mark__registered
          &#9829
        .like__number
          = set_list.likes.count
- else
  .like__box
    .heart__mark__registered
      &#9829
    .like__number
      = set_list.likes.count

show.html.haml

略
.button__box
   .like__button
      = render partial: "likes/like", locals: { event: @event, set_list: @set_list }
   .twitter__box
      %i.fab.fa-twitter
略


remote: true

link_toにremote: trueの記述を加えることにより、非同期通信でコントローラーに値を送信してくれます。

render partial: "likes/like", locals: { event: @event, set_list: @set_list }

renderメソッドを使って部分テンプレートを呼び出します。
localsオプションの中身は@event = @eventのようにはできないので注意。(2時間返して(涙))


② コントローラーでのページ遷移の記述削除

非同期通信でページ遷移は行わないため、redirect_to ~~の記述は消します。代わりに部分テンプレートでの記載にeventやset_listのインスタンス変数が必要になってくるので、こちらを定義します。インスタンス変数の定義忘れがちなので、要注意です。(1日返して(涙))

likes_controller

class LikesController < ApplicationController
  def create
    @like = Like.create(set_list_id: params[:set_list_id], user_id: current_user.id)
    @set_list = SetList.find(params[:set_list_id])
    @event = Event.find(params[:event_id])
  end

  def destroy
    @like = Like.find_by(user_id: current_user.id, set_list_id: params[:set_list_id])
    @set_list = SetList.find(params[:set_list_id])
    @event = Event.find(params[:event_id])
    @like.destroy
  end
 end


③ create.js.hamlとdestroy.js.hamlファイルを作成し、いいねボタン部のみviewを変更

コントローラーでの処理を終えた後、createアクションの場合はcreate.js.hamlへdestroyアクションの場合はdestroy.js.hamlへ処理の権限が移ります。(これらはデフォルトではないので自分で作る必要有りです。)
そしてこれらファイルの中で部分テンプレートを使っていいねボタンのいいね数を更新してあげます。

create.js.haml

$(".like__button").html("#{escape_javascript(render partial: 'likes/like', locals: { event: @event, set_list: @set_list })}");

destroy.js.haml

$(".like__button").html("#{escape_javascript(render partial: "like", locals: { event: @event, set_list: @set_list })}");


html()メソッド

セレクタの中のHTML要素を引数のHTMLで置き換えます。

escape_javascript関数

公式のチュートリアルには「JavaScriptファイル内にHTMLを挿入するときに実行結果をエスケープする (画面に表示しない) ために必要です。」とありますが、意味がよく分かっていません。また、分かれば記事書きたいと思います。とりあえずJavaScriptでHTMLを挿入するときに必要になるという理解で問題ないと思います。



以上になります。
こうやってまとめてみると簡単なように見えますが、二箇所ハマった部分があったので個人的には苦労した印象です。コントローラーでのインスタンス変数の定義し忘れには十分注意して下さい(初心者丸出しすいません。汗)
ここまで読んでくださり、ありがとうございました。分かりにくいやアドバイス等ありましたらコメントくださると幸いです。では!

【Ajax通信】非同期通信でのコメント削除機能

おはようございます。
今日は前回記事を書いたコメント投稿機能に削除機能をAjax通信により実装を行っていきます。
初心者コードで恐縮ですが、簡単にまとめていきたいと思います。

【Ajax通信】非同期でのコメント機能の実装 - さかいの気ままにプログラミング日記

実装までの流れ

① viewに削除ボタンをつくる
② JavaScriptで削除ボタンを押したときコメントidをコントローラーに送信する
③ controllerで受け取ったidのコメントをdestroyする
④ JavaScriptでコメントのHTMLを削除する

① viewに削除ボタンをつくる

まずはビューに削除ボタンを作っていきます。

.comment__box
    .comment__contents
      - @comments.each do |comment|
        .comment__posted{data: {comment_id: "#{comment.id}"}}
          .comment__posted__information
            .comment__user__nickname
              = comment.user.nickname
            .comment__post__time
              = comment.created_at.strftime("%Y年%m月%d日 %H:%M")
          .comment__posted__box
            .comment__posted__content
              = comment.content
            - if current_user.id == comment.user_id 
              %button.comment__delete__button
                %i.far.fa-trash-alt



.comment__posted{data: {comment_id: "#{comment.id}"}}

投稿されたコメントの識別をするためにカスタムデータ特性を追加しています。hamlの記法では文字列の中に#{}で囲うことで変数を使用することができます。


- if current_user.id == comment.user_id

コメント削除はログイン中のユーザーとコメントしたユーザーが一致したときのみ行えるようにします。


JavaScriptで削除ボタンを押したときコメントidをコントローラーに送信する

$(document).on('click', '.comment__delete__button', function(){
    var comment_data_id = $(this).parent().parent().data("comment-id")
    var url = "/events/" + gon.event.id + "/set_lists/" + gon.set_list.id + "/comments/" + comment_data_id
    $.ajax({
      url: url,
      type: "DELETE",
      data: comment_data_id,
      datatype: "json"
    })


$(document).on('click', '.comment__delete__button', function(){

ユーザーがコメントを送信した直後でも削除ができるように(コメント投稿後にページ遷移することなく削除ボタンを押した場合)、セレクタの取得は$(document).on(~~ で記載するようにしています。


var comment_data_id = $(this).parent().parent().data("comment-id")

削除するコメントidを取得しています。.data("comment-id")では①で追加したカスタムデータ属性を取得しています。このカスタムデータ特性はコメントidと紐付いているので、削除ボタンを押したコメントを削除することができるというロジックになります。


var url = "/events/" + gon.event.id + "/set_lists/" + gon.set_list.id + "/comments/" + comment_data_id

遷移させるurlを指定しています。今回はルーティングをネストさせているので複雑ですが、gonのGemを使いながら記載しています。gonはコントローラーで定義したインスタンス変数をJavaScriptで使えるようにしてくれるGemですごく便利で簡単に実装できるのでよく使っています。gonの導入の仕方は下記事に乗せておきます。

【gem gon】Railsで定義した変数をさくっとJavascriptで使う - Qiita


$.ajax({ ~~

Ajax通信するための記載をしています。
url : 上で定義した遷移先のurlを指定します。
type : 今回はHTTPメソッドはDELETEを使用するので、DELETEを指定します。
data : DELETEメソッドを行う場合は、コントローラーに送るデータは削除するレコードのidです。なので、上で定義した削除するcomment_idを指定します。
datatype : もちろんjsonです。

③ controllerで受け取ったidのコメントをdestroyする

次にコントローラーに処理を書いていきます。

def destroy
    @comment = Comment.find(params[:id])
    @comment.destroy
    respond_to do |format|
      format.json { render json: @comment.id }
    end
  end


respond_to do |format| ~~ end

リクエストのフォーマット毎の処理をするために記載しています。これを記載しないとサーバーエラーになります。


format.json { render json: @comment.id }

次にJavaScript側でコメントを削除しないといけないので、返すデータは削除したコメントのidを書いています。
ここで、format.html { redirect_to ~~ } としてもなぜか走ってしまいます。format.json ~~ の後ろに書くとformat.jsonが走り、前に書くとformat.htmlが走るのが確認できました。コントローラーに値がjson形式で送られてきたらformat.jsonが走るものだと思っていたのですが、ここがいまだに謎です。。
また、分かれば記事書くかもです。

④ JavaScriptでコメントのHTMLを削除する

Ajax通信が成功したときの処理を書いて終了です。

.done(function(comment_id){
      $('[data-comment-id = '+ comment_id + ']').remove();
    })


$('[data-comment-id = '+ comment_id + ']').remove()

カスタムデータ特性から削除するコメントのセレクタを取ってきてremove()します。カスタムデータ属性からセレクタを取ってくるには[ ]を使います。

終わり!!



ここまで読んでくださり、ありがとうございました。
次はいいね機能の実装について書いていきたいと思います
分かりにくいやアドバイス等ありましたらコメントくださると幸いです。では!

【Ajax通信】非同期でのコメント機能の実装

こんばんは
今日は個人アプリへコメント機能の実装を行ったので、それについてまとめていきたいと思います。

前提

 コメント機能を実装したいビューができていること

① フォームを作る

 まずは、ビューにフォームを作ります。

.comment__box
    .comment__contents
 .comment__form__box
    = form_for [@set_list, @comment], url: {controller: 'comments', action: 'create', set_list_id: '1'} do |f|
        = f.text_field :content, placeholder: "コメントを入力して下さい", class: "comment__form"
        = f.button type: "submit", class: "comment__post__button" do
           %i.far.fa-comment

 モデルをネストさせたルーティングを組んでいるので、インスタンス変数を配列の形で2つ記載しています。
しかし、このままだと下のようなエラーがでます。ActionController::UrlGenerationErrorはrailsがうまくURLを見つけれてないため起こるエラーで、ネストさせている場合に起こるようです。なので、rails にURLを指定してあげましょう。
 また、この際、set_list_idも渡してあげないとどのセットリストに対してコメントをするのか不明なため、こちらも指定します。


f:id:shun_0211:20200525202557p:plain


② JavaScriptでフォーム情報をJson形式でコントローラーに送信する。

 JavaScriptにフォーム情報をコントローラーに送信する処理を書いていきます。

$(function(){
  $('#new_comment').on('submit', function(e){
    e.preventDefault();
    var formData = new FormData(this);
    var url = $(this).attr('action');
    $.ajax({
      url: url,
      type: "POST",
      data: formData,
      dataType: "json",
      processData: false,
      contentType: false
    })
  })
})

e.preventDefault();

 e.preventDefault()はデフォルトのイベントを止める処理です。これがないとフォームを同期通信で送信してしまうのでこちらは入れます。

var formData = new FormData(this);

 formDataオブジェクトを作成しフォーム情報を入れ込んでいます。

var url = $(this).attr('action');

 Jqueryのattr()は属性を取得するメソッドでaction属性を取得しています。action属性は、フォームの送信データの送信先を設定するための属性なのでこれを取得しています。

$.ajax({ ~~ })

 ajax以下ではAjax通信するのに必要な情報を記載しています。


③ コントローラーでフォーム情報を保存し、javascriptへ返す処理を書く

 コントローラーではJson形式で送られてきたフォーム情報をデータベースへ保存し、またJavaScript側に返します。

def create
    @comment = Comment.new(comment_params)
    @comment.user_id = current_user.id
    @comment.set_list_id = params[:set_list_id]
    respond_to do |format|
      if @comment.save
        format.html { redirect_to :back }
        format.json
      end
    end
  end

  private
  def comment_params
    params.require(:comment).permit(:content)
  end


@comment.user_id = current_user.id

 user_idカラムへコメントを送信したユーザーのidを入れています。

@comment.set_list_id = params[:set_list_id]

 なぜかparamsの中にset_list_idが入っているのにも関わらず、ストロングパラメーターで持ってこれなかったのでこのような記載をしています。なぜとってこれないのかは調査中で分かったらブログにアップします。

respond_to do |format| ~ end

 リクエストフォーマットに応じた処理を書いています。
format.jsonの後に何も書いていないのはjbuilderを使用してJavaScriptファイルに返すデータを作るからです。
※ ここにrenderメソッドを使用した場合はdone(function(data)~~の引数がrenderメソッド指定した情報が格納されるので、何も書いてはいけません。

 入力したフォーム情報が辿る流れとしては、JavaScriptからフォーム情報がコントローラーへ送られ、その後、一旦json.jbuilderを経由してjavascriptへ戻ってきます。json.jbuilderではインスタンス変数からjson形式のデータを作ってくれます。次ではそのjson.jbuilderファイルを編集します。


④ create.json.jbuilderを作成し、json形式のデータを作成する。

 デフォルトではファイルは作られないので、views/commentsディレクトリにcreate.json.jbuilderファイルを作成します。ファイル名は該当するactionと同じ名前にする必要があります。必ずactionに紐付いたファイル名にしましょう。actionがjbuilderの名前を通じて紐付いているため、actionで定義したインスタンス変数を使うことができます。
 では、json.(キー) (バリュー)というように記載してjson形式のデータを準備していきます。

json.content @comment.content
json.user_id @comment.user.id
json.user_nickname @comment.user.nickname
json.created_at @comment.created_at.strftime("%Y年%m月%d日 %H:%M")


JavaScriptで返ってきた値を使って、HTMLを作成しviewに挿入

 最後はブラウザへコメントを反映させる作業になります。

$(function(){
  function buildHTML(comment){
    var html = `<div class = "comment__posted">
                  <div class = "comment__posted__information">
                    <div class = "comment__user__nickname">
                      ${comment.user_nickname}
                    </div>
                    <div class = "comment__post__time">
                      ${comment.created_at}
                    </div>
                  </div>
                  <div class = "comment__posted__content">
                    ${comment.content}
                  </div>
                </div>`
    return html;
  }
  $('#new_comment').on('submit', function(e){
    e.preventDefault();
    var formData = new FormData(this);
    var url = $(this).attr('action');
    $.ajax({
      url: url,
      type: "POST",
      data: formData,
      dataType: "json",
      processData: false,
      contentType: false
    })
    .done(function(data){
      var comment_html = buildHTML(data);
      $('.comment__contents').append(comment_html);
      $('.comment__form').val("");
      $('.comment__post__button').prop("disabled", false);
    })
  })
})

.done(function(data){ ~~

 done以降でコントローラーから返ってきた値を使った処理が書かれます。functionの引数dataはコントローラーから返ってきた値が格納されています。

var comment_html = buildHTML(data);

 上の方に書かれたbuildHTML関数を呼び出してコメントを挿入するためのブロックを作っています。

function buildHTML(comment){ ~~

 コメントを挿入するためのHTMLのブロックを作っています。改行をそのまま書けるテンプレートリテラル記法を利用しています。
 コントローラーから返ってきている値はcreate.json.jbuilderを経由した値なので、comment.user_nicknameでコメントしたユーザーのニックネームを取得することができています。

$('.comment__contents').append(comment_html);

 先程作ったコメントを挿入したブロックをveiwに挿入しています。

$('.comment__form').val(""); と $('.comment__post__button').prop("disabled", false);

 コメントフォームを空にする処理と再度ボタンをクリックできるようにする処理です。

終わり!!


ここまで読んでくださり、ありがとうございました。
form_forでのネストしたルーティングの場合、URLを指定しないといけない点やストロングパラメーターでset_list_idがとってこれない点に少し苦戦しましたが、それ以外は特に躓くことは少なかったです。時間はかかりましたが、、
分かりにくいやアドバイス等ありましたらコメントくださると幸いです。では!

【Ajax通信】フォーム入力画面で非同期通信によるエラーメッセージ表示 ②

おはようございます。
今日は前回から続いて、Ajax通信を利用したフォーム送信機能の実装をまとめていけたらと思います。前回まででコントローラーへフォーム情報を送信するところまでは実装できています。
【Ajax通信】フォーム入力画面で非同期通信によるエラーメッセージ表示 ① - さかいの気ままにプログラミング日記

そこで今回はバリデーションを設定し、フォーム情報が適切でないときにエラーメッセージを表示させます。
実装後の挙動です。

gyazo.com


今回編集するのは、モデル、コントローラー、JavaScriptファイルです。
では順番に見ていきます。

モデル

ここではモデルへバリデーションをかけます。今回書けるバリデーションは3つだけです。

validates :artist, :first_song, presence: true
validates :artist, uniqueness: true


コントローラー

ここではjson形式でコントローラーへ送られてきたフォーム情報をテーブルへ保存する記載をします。

  def create
    @set_list = SetList.new(set_list_params)
    respond_to do |format|
      if @set_list.valid?
        @set_list.save
        format.html { redirect_to root_path }
        format.json { render json: @set_list.errors.full_messages }
      else
        format.json { render json: @set_list.errors.full_messages }
      end
    end
  end

  private
  def set_list_params
    params.require(:set_list).permit(
      :event_id,
      :artist,
      :first_song,
      :
     略
      :
      :tenth_song
    )
  end

復習も兼ねて基礎中の基礎からまとめていきます。(カリキュラムもあと一ヶ月弱で見れなくなるので・・)

@set_list = SetList.new(set_list_params)

SetListクラスのインスタンスを生成し、引数のset_list_paramsの中身をインスタンスに追加しています。
もっとイメージしやすい言葉に言い換えると、SetList.newで一行レコードを追加し、カラムにparamsで送られてくる値を代入しています。下の例だと分かりやすいと思います。

例)
SetList.new
=> #<SetList:0x00007fa9ea72d5f8
id: nil,
event_id: nil,
artist: nil,
first_song: nil,
:
:
updated_at: nil


SetList.new(artist: "sumika")
=> #<SetList:0x00007fa9eb879b40
id: nil,
event_id: nil,
artist: "sumika",
first_song: nil,
:
:
updated_at: nil

paramsの中身はこんな感じです。
f:id:shun_0211:20200523060438p:plain

ここで、セキュリティー上の問題に対処する手法としてストロングパラメーターを使用します。ストロングパラメーターを使用することで、指定したパラメーター以外のキーを受け取れなくします。
ストロングパラメーターを使用した際のset_list_paramsの中身はこんな感じになります。
f:id:shun_0211:20200523060443p:plain



(補足)
・paramsとはコントローラーに送られてくるデータが格納されている箱(ハッシュオブジェクト)のことです。
・private以下のメソッド全体をストロングパラメーターと呼びます。
・form_forやform_withを使用する際はrequire(:モデル名)が必要です。(ハッシュ構造の場合は記載が必要)

respond_to do |format| ~ end

respond_to メソッドを使い、リクエストフォーマット(HTML, JSON, etc...)に応じた処理の記述をしています。(今回はリクエストはJSONのみなので、format.htmlが走ることはありません。)
valid?がtrueでもfalseでもJavaScriptにはエラーメッセージを返すように記載しています。理由はJavaScript側での処理をエラーメッセージのありなしで分けているからです。
format.jsonの後には、JavaScriptに返すデータを記載します。ここでは、renderメソッドを使用して@set_list.errors.full_messagesをjson形式のデータに変換しています。
次ではJavaScript側での処理を詳しく見ていきます。


(補足)
json形式 ⇒ {"キー" : "バリュー", "キー" : "バリュー"}
・renderメソッドでのデータの変換

render json: @set_list    #@set_listをjson形式のデータに変換

・エラーメッセージのオブジェクト

@set_list.error.messages    #=> {:アーティスト名=>["を入力してください", "はすでに存在します"], :一曲目=>["を入力してください"]}
@set_list.error.full_messages    #=> ["アーティスト名を入力してください", "アーティスト名はすでに存在します", "一曲目を入力してください"]

・エラーメッセージの日本語化
i18nというGemを使いました。参考記事をみて簡単に実装することができました。

JavaScriptファイル

ここではコントローラーから値が返ってきた後の処理を.done以下に記載していきます。

中略
$.ajax({
      url: url,
      type: "POST",
      data: formData,
      dataType: 'json',
      contentType: false,
      processData: false
    })
    .done(function(error_messages){
      var error_num = Object.keys(error_messages)
      if (error_num == 0){
        window.location.href = `/events/${gon.event.id}/choise_artist`;
      }
      else{
        error_messages.forEach(function(error_message){
          var error_alert = `<div class = error_message>
                               * ${error_message}
                             </div>`
          $('.error_messages_box').append(error_alert);
          $('.error_messages_box').css({
            'padding': '30px 20% 0',
            'font-size': '13px',
            'color': 'red'
          })
        })
      }
    })
  })
})


バリデーションが通ったときの記述
.done(function(error_messages){
      var error_num = Object.keys(error_messages)
      if (error_num == 0){
        window.location.href = `/events/${gon.event.id}/choise_artist`;
      }

doneの後のfunctionの引数はコントローラーから返ってきた値です。
まず、Object.keysでオブジェクトのキーを取得します。今回のような配列の場合、[0, 1, 2 …]のように返します。これはエラーメッセージが返ってきているかどうかを判別するためです。もしエラーメッセージが返ってきていない場合、指定のURLへ遷移させています。

バリデーションが通らなかったときの記述
else{
        error_messages.forEach(function(error_message){
          var error_alert = `<div class = error_message>
                               * ${error_message}
                             </div>`
          $('.error_messages_box').append(error_alert);
          $('.error_messages_box').css({
            'padding': '30px 20% 0',
            'font-size': '13px',
            'color': 'red'
          })
        })
      }

エラーメッセージが通らなかった場合は、エラーメッセージをviewに表示させる記述を行います。エラーメッセージは配列で格納されているのでforEach文を使って一つ一つを、viewに予め作っておいたerror_messages_boxへappendしていきます。
そして同時に見た目を整えるCSSを付与しています。

終わり!


これで非同期通信によるエラーハンドリングが完了しました。非同期での実装以外にも同期通信やjavascriptだけでの実装も可能なようなので、挑戦していきたいと思います。
ここまで読んでくださり、ありがとうございました。
分かりにくいやアドバイス等ありましたらコメントくださると幸いです。では!



参考サイト
忘れがちなrenderメソッドの使い方まとめ [Rails] - Qiita
【Rails】バリデーションのエラーメッセージを表示する - Yohei Isokawa
Railsのバリデーションエラーのメッセージの日本語化 - Qiita

【Ajax通信】フォーム入力画面で非同期通信によるエラーメッセージ表示 ①

おはようございます。
今日はAjax通信を利用したフォーム送信機能の実装をまとめていけたらと思います。

Ajax通信とは

「Asynchronous JavaScript + XML」の略でブラウザが再読み込みされることなくサーバーと通信を行う通信方法です。

なぜフォーム送信機能にAjax通信を使うのか

フォーム送信した際にバリデーションエラーが起きているとき、Javascriptでエラーメッセージを表示させたいからです。
イメージはこんな感じです。


gyazo.com


実装方法

まず、Javascript側でコントローラーまでフォーム情報をJSON形式で送信するところまで書きます。
やる操作は以下の4点です。

  1. 全フォーム情報を取得し、form_dataに定義します。
  2. FormDataオブジェクトを作成し、取得したフォーム情報を追加します。
  3. 遷移するURLを定義します。
  4. Ajax通信するための記述を書きます。


実際に実装するとこのようになりました。

$(function(){
  $(".post__button").on("click", function(){
    var form_data = document.forms.new_set_list
    var formData = new FormData(form_data)
    var url = "/events/" + gon.event.id + "/set_lists"
    $.ajax({
      url: url,
      type: "POST",
      data: formData,
      dataType: 'json',
      contentType: false,
      processData: false
    })
    # コントローラーにフォーム情報を送るところまでを書いているので、割愛します。
    })
  })
})

一つ一つ詳しく見ていきます。

1. 全フォーム情報を取得し、form_dataに定義

document.forms.(フォームのname属性)とすることで<form>~</form>で囲まれたHTMLを取得することができます。

これはDocument インターフェイスの forms プロパティが読み取り専用で、文書内に含まれるすべての <form> を列挙した HTMLCollection を返す仕様だからだそう
詳しくはこちらをのぞいてみて下さい。
Document.forms - Web API | MDN
HTMLCollection - Web API | MDN


2. FormDataオブジェクトを作成し、取得したフォーム情報を追加

FormDataオブジェクトとは、XMLHttpRequest を使用してサーバーにフォーム情報を送信するためのオブジェクトで、キーバリューストアの方式が取られています。
XMLHttpRequestとは、JavaScriptでHTTPリクエストを行うための組み込みのブラウザオブジェクトです。これにより、JavaScriptでサーバーとやりとりをして処理をさせたり、必要な情報を持ってくることができます。
フォーム情報の場合の使い方は、主に下の2つです。

var formData = new FormData();
formData.append("キー", "バリュー");
var formData = new FormData(document.forms.(formのname属性))

これらの方法でajax通信でフォーム情報をコントローラーに送る準備をします。
実装したコードではフォーム情報を一度に追加できる2番目の方法を採用しています。


3. 遷移するURLを定義

ここではrails routesで投稿処理を行いたいコントローラーのcreateアクションに該当するURLを確認し、定義します。
f:id:shun_0211:20200521203349p:plain

set_listsコントローラーのcreateアクションに該当するURLは「/events/:id/set_lists」となっていて、idが必要です。
idを表すのにインスタンス変数を使いたかったので、gonというGemを使用しています。
使い方は、下の記事を参考に簡単に導入することができました。

【gem gon】Railsで定義した変数をさくっとJavascriptで使う - Qiita


4. Ajax通信するための記述

ここまでで、Ajax通信を行うための準備はできたので、実際に記述を行っていきます。主に4つの指定を行います。
① url
遷移するurlを記載します。上で定義したurlを指定します。
② type
行うHTTPメソッドを記載します。今回はPOSTを指定します。
③ data
送信するデータを記載します。上で定義したformDataオブジェクトを指定します。
④ dataType
サーバーに送信するデータの型を記載します。今回はjsonを指定します。


その他のオプション
Ajax通信を行う際、仕様上、自動でデータを別の形に変換してしまいます。
それを防ぐため、以下の2つのオプションをつけてform.submitと同じ形式でデータを送信するようにしています。

contentType: false
processData: false


以上、復習がてらコントローラーにフォーム情報を送信するところまでまとめました。
次はコントローラー側でエラーメッセージを返すための準備までのところを書いていきたいと思います。
ここまで読んでくださりありがとうございました。
分かりにくいやアドバイス等ありましたらコメントくださると幸いです。では!