複数のフォームがあり空のフォームはサブミットされないようにする

したいこと

二つのフォームがあり片方は何も書かれていない空の状態であれば、そのフォームをサブミットせずに記入されている片方のフォームだけ送信する方法。
この記事のように二つのフォームが同じモデルへ送信するのであれば、これは意味がないが、それぞれが別のモデルへ保存される時に便利。
今回はこの一つのフォームで複数のモデルへ保存するフォームに実装することを想定して作例する。 ただし、そこまで深入りしないが、今回はお互いのフォームはアソシエーションで紐づいていて、親子関係にあるため、親モデルのフォームが空であっても、親モデルのレコードは作成されるようにしている。

ポイント

  • サブミットするときの関数を設定する。
  • クリックすると、jqueryでフォームが空であるか調査し、空であればそのフォームにdisabled属性を加え、フォームを送信

View.html.erb

<%= form_with(model: review, local: true, id: "review_form") do |form| %>
  <div class="field">
    <%= form.label :content %>
    <%= form.text_area :content, id: :review_content %>
  </div>
  <%= form.fields_for :phrases do |phrase_field| %>
    <div class="field">
      <%= phrase_field.text_area :content, id: :phrase_content %>
    </div>
  <% end %>
  <div class="actions">
    <button name="button" type="button" id="submit_btn">投稿</button>
  </div>
<% end %>

jquery

$(function(){
$('#review_form').on('submit', function(){
  if($("#phrase_content").val() == "") {
    $('#phrase_content').attr('disabled','true');
  };
});
});

参考

【jQuery】submit前に処理を行う方法 - Qiita

textareaの縦幅を入力行数によって自動変更させる方法

autosizeというライブラリを使います

autosize | RubyGems.org | your community gem host

デモはこちら

方法

  1. Gemfileに記入
    gem 'autosize', '~> 2.4'
    bundleinstallを実行
  2. app/assets/javascripts/application.jsに以下を追記 //= require autosize

本来ならこれでできるはずだけど、エラーが出てassets配下を探していたので、gem environmentを実行して、GEM PATHを頼りにgemがインストールされているところへ行き、app/assets/javascripts/へautosize.jsファイルをコピーした

jqueryコード

autosize($('textarea'));
たったこれだけで、縦幅が自動で変更されます。 javascriptのコードやいろんなオプションはこちらを参考に

一つのフォームで複数のモデルに保存する

したいこと

今回は映画のレビューを投稿するreview投稿フォーム画面に、 映画の好きなフレーズを投稿できるphrase投稿フォームも表示し、 一度送信ボタンへ押すだけでreviewはもちろん、そのreviewに紐づいたphraseも保存する。 なので、reviewモデルとそのreviewモデルに紐づいたphraseモデルが存在している。

重要なメソッド

  • field_forメソッド
  • accepts_nested_attributes_for

バージョン

rails 5.1.4

 カラム

reviewsテーブルにはcontentカラム
phrasesテーブルにもcontentカラムのみとする

アソシエーション定義

model/review.rb

  has_many :phrases, dependent: :destroy
  accepts_nested_attributes_for :phrases

model/phrase.rb

belongs_to :review, optional: true

Controller

def new
  review = Review.new
  2.times { @review.phrases.build }
end

def create
  @review = Review.new(review_params)
  @review.save
end

private
def review_params
  params.require(:review).permit(:content,  phrases_attributes: [:content])
end

View

<%= form_with(model: review, local: true) do |form| %>
  <div class="field">
    <%= form.label :content %>
    <%= form.text_area :content, id: :review_content %>
  </div>
  <%= form.fields_for :phrases do |phrase_field| %>
    <div class="field">
      <%= phrase_field.text_area :content, id: :phrase_content %>
    </div>
  <% end %>
  <%= form.submit %>
<% end %>

学んだこと

  • field_forメソッドで異なるモデルへ保存するためのフォームを作成できる。controller#newでtimesメソッドを使ってインタンスを作成しておけば、viewではeach文を使って保存しなくても、(field_forだけではなく)form.fields_forでコントローラーで作成したインスタンス分のフォームを表示してくれる。
  • accepts_nested_attributes_forをモデルに追加することでStrongParameterに記載しているネストされたパラメーター(phrases_attributes: [:content])を読み取り、それぞれのモデルのレコードを更新できる。
  • rails5からはbelongs_to :review, optional: trueにしないと外部キー(〇〇_id)がもしnilの場合、保存ができなくなっているため、optional: trueをつけておくとそれを無効化しておく。ただ、今回はあってもなくても、どちらでも大丈夫。

参考

deviseで作ったuserモデルにカラムを追加し、sign up時に登録されるようにする

gemを読む

GitHub - plataformatec/devise: Flexible authentication solution for Rails with Warden.

この部分

# In case you want to permit additional parameters (the lazy way™), you can do so using a simple before filter in your   
`ApplicationController:`

class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:username])
  end
end

まず追加したいカラムを通常どうりmigrationファイルで作成

def change
    add_column :users, :name, :string
end

パラメーターで送れるようにビューのフォームに追加したい項目を追記

<div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name %>
</div>

次に上記のコードをApplicationControllerに追記する。変更するのは追加したいカラム名のみ

class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
  end
end

HerokuとS3を連携

S3の初期設定

この記事のバケットの作成のところ(Imagemagikの導入の手前)までの通りすればできました。

【Rails】S3へ『CarrierWave+fog』を使って画像アップロードする方法 | vdeep

Gemfile

こちらを追加し、bundle install

gem 'fog-aws', group: :production
gem 'carrierwave', '~> 1.0'

○_uploader/rbに以下を追記

if Rails.env.production?
    storage :fog
  else
    storage :file
  end

carrier_wave.rb

$ touch config/initializers/carrier_wave.rbでファイル作成
作成したファイルに以下を追記

require 'carrierwave/storage/abstract'
require 'carrierwave/storage/file'
require 'carrierwave/storage/fog'


CarrierWave.configure do |config|
  if Rails.env.production?
    config.storage = :fog
    config.fog_provider = 'fog/aws'
    config.fog_credentials = {
      # Amazon S3用の設定
      :provider              => 'AWS',
      :region                => ENV['S3_REGION'],
      :aws_access_key_id     => ENV['S3_ACCESS_KEY'],
      :aws_secret_access_key => ENV['S3_SECRET_KEY']
    }

    config.fog_directory     =  ENV['S3_BUCKET']
  else
    config.storage :file
    config.enable_processing = false if Rails.env.test?
  end
end

ここの下の画像の人の例を参考にしました。 f:id:shoheimoment:20171215112549p:plain

herokuの環境変数の設定

S3の初期設定で取得できているcredentials を前項のENVの[ ]内の変数を下記コマンドで設定していく。

heroku config:set S3_REGION=
heroku config:set S3_ACCESS_KEY=
heroku config:set S3_SECRET_KEY=
heroku config:set S3_BUCKET=

※credentialsがわからなくなったら、アクセスキーIDとREGIONは次から確認できる。 ルートアカウント認証情報を使用してサインイン→ヘッダーのユーザ名のdropdownからセキュリティ認証情報を選択→左のユーザー→ユーザーを選択→認証情報タブ secret keyはどう確認するかわからないからユーザーを作った時にcredentials.csvをダウンロードしておこう!

デプロイ

これでデプロイしてみるとできている。できなければ、DATABASEのリセットとかを試してみるといいかも。

file_filedの注意点

今回、以下のformで画像をupすることにして、空で送ってしまったときはバリデーションにひっかかるように設定しようと思った。

<%= form_with(model: @user_icon, local: true) do |form| %>
  <%= form.file_field :image %>
  <%= form.submit "更新する" %>
<% end %>

しかし、これで送信すると、以下のようにparameterにrequire(:user_icon)のkeyがそもそもないため、コントローラーのstrong_paramsでエラーになってしまう。
user_icons_controller.rb

def create
    @user_icon = UserIcon.new(user_icon_params)
    if @user_icon.save
      redirect_to user_path(@user_icon.user_id), notice: 'アイコンを設定しました'
    else
      render 'users/show'
    end
  end
#省略
private
  def user_icon_params
    params.require(:user_icon).permit(:user_id, :image)
  end

エラー詳細

[2] pry(#<UserIconsController>)> params
=> <ActionController::Parameters {"utf8"=>"✓", "authenticity_token"=>"62WAR0vsxA+HqJabeXbmOOOCF7TfE+x9WLp0yXFOTes2+dFb8AHh0hq36kOsR7eH4zmbk8GvSjdqdAgvKwjnjA==", "commit"=>"更新する", "controller"=>"user_icons", "action"=>"create"} permitted: false>

とりあえずは、下記のようにform.file_fieldタグの他に、今回アソシエーションで紐づいて必要だったuser_idをhiddenタグをparameterに仕込むことで解決できた。

<%= form_with(model: @user_icon, local: true) do |form| %>
  <%= form.hidden_field :user_id, value: "#{current_user.id}" %>
  <%= form.file_field :image %>
  <%= form.submit "更新する" %>
<% end %>

ただ、今までnameやemailを全て空欄で送信してもバリデーションにひっかけることができたのに、なぜ今回はkey(この例では:user_icon)が設定されないのか不思議である。

RailsでAjaxとjQueryを使ってコメントの編集方法

Ajax化概要

ビューのフォームでアクションを指定し、コントローラーでそれを実行するのだが、remote: trueが記入されていることで、そのアクション名.js.erbというファイルが実行され、その中にjqueryコードを書いておく。
そのjqueryコードが実行され、実際にビューに反映されるものはrubyのコードなので大抵、パーシャルhtml.erbに書き込まれrenderされる。

準備

今回、このコメントの編集と削除をjqueryajaxを使って実装していく。

f:id:shoheimoment:20171209154022p:plain

まず、初めのビューファイル view/pictures/index.html.erb

<div class="other_comments">
  <% if picture.other_comments.present? %>
    <% all_other_comments = picture.other_comments.order(created_at: :asc) %>
    <% all_other_comments.each do |each_other_comment| %>
      <div class="for_edit_comment">
        <%= render 'other_comments/each_other_comment', each_other_comment: each_other_comment %>
      </div>
    <% end %>
  <% end %>

render部分 view/other_coments/_each_other_comment.html.erb

<div class="each_other_comment row" data-comment="<%= each_other_comment.id %>">
  <p class="col-xs-10"><strong><%= each_other_comment.user.name %></strong>&nbsp;&nbsp;<span id="for_edit"><%= each_other_comment.other_comment %></span></p>
  <div class="col-xs-2">
    <div class="dropdown">
      <button class="btn btn-default dropdown-toggle" type="button" id="dropdownMenu1" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
      <span class="caret"></span>
      </button>
      <ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
      <li><%= link_to '編集', edit_other_comment_path(each_other_comment.id), remote: true %></li>
      <li><%= link_to '削除', other_comment_path(each_other_comment.id),
      remote: true, data: { confirm: '削除しますか?' },
      method: :delete %>
      </li>
      </ul>
    </div>
  </div>
</div>

上の写真では写ってはいないがマウスオーバーすると編集削除ボタンがでる。

f:id:shoheimoment:20171209161705p:plain

 編集ボタンを押すとフォームに変わる

_each_other_comment

<li><%= link_to '編集', edit_other_comment_path(each_other_comment.id), remote: true %></li>

ここのremote: trueを加えることにより、ajaxで行うことを意味する。

※フォームに直すのはjqueryだけでできるが、要素の取得がajaxでやれば簡単だったので今回はajaxで行う。 編集ボタンを押すとother_commentコントローラーのeditアクショへ行く。

other_comments_controller.rb #edit

def edit
    @other_comment = OtherComment.find(params[:id])
    respond_to do |format|
      format.html { redirect_to pictures_path }
      format.js { @id_comment = @other_comment.id }
    end
  end

ajaxで命令が来ると、format.jsが適応されて、デフォルトでview/other_comments/edit.js.erbが呼ばれて実行される。

view/other_comments/edit.js.erb

var idComment = <%= @id_comment %>;

$('.row[data-comment="'+idComment+'"]').parent().html("<%= escape_javascript(render 'edit_other_comment') %>");

他の人のコメントはeach文繰り返し処理を行っているので、classやidだけで要素を取得するのが難しい。そこであらかじめdivタグにdata-comment属性にそのレコードのid値を持たせておく。
この部分 view/other_coments/_each_other_comment.html.erb

<div class="each_other_comment row" data-comment="<%= each_other_comment.id %>">

それを元に取得している。 これがajaxでこの処理を行った理由。

なので、edit.js.erbでは取得した要素の一つ上の親要素(for_edit_comment)を取得してhtmlをrender先の_edit_other_commentに書き換えなさいという意味。
※ここでparent()メソッドを使っているのは、html()メソッドは取得した要素の子孫要素を書き換えるメソッドであり、その要素自体を取得して書き換えると、コメントを編集する度に取得した要素が加えられていってしまい、cssがおかしくなったりするから。

次にedit.js.erbからrenderされる部分
_edit_other_comment

<div class="each_other_comment row" data-comment="<%= @id_comment %>">
  <%= form_with(model: @other_comment, remote: true) do |form| %>
    <div class="add_other_comment row">
      <%= form.text_area :other_comment, rows: 3, placeholder: "コメントを追加...", class: "col-xs-11" %>
      <%= form.submit '投稿する', data: { confirm: '投稿しますか?' }, class: 'btn btn-default' %>
    </div>
  <% end %>
</div>

これで編集ボタンを押すとフォームに切り替わる。 f:id:shoheimoment:20171209164103p:plain

コメントを更新する

ここからコメントを更新する作業である _edit_other_comment

 <%= form_with(model: @other_comment, remote: true) do |form| %>

この部分のremote: trueでajax化している。先ほどと同様にコントローラーでDBの命令を出すと同時にupdate.js.erbが実行される。

other_comments_controller.rb #update

def update
    @other_comment = OtherComment.find(params[:id])
    respond_to do |format|
      if @other_comment.update(other_comment: params[:other_comment][:other_comment])
        format.html { redirect_to pictures_path }
        format.js { @id_comment = @other_comment.id }
      else
        format.html { render 'pictures/index' }
        format.js { @status = 'fail' }
      end
    end
  end

update.js.erb

var idComment = <%= @id_comment %>;

$('.row[data-comment="'+idComment+'"]').parent().html("<%= escape_javascript(render 'each_other_comment', each_other_comment: @other_comment) %>");

編集の時と同様に、取得した要素のhtmlを書き換えている。 render先は上の方で書いているview/other_coments/_each_other_comment.html.erbである。 これで投稿ボタンを押すとajaxで更新される。

f:id:shoheimoment:20171209170119p:plain

まとめ

link_toのedit→#edit→edit.js.erb→edit_other_comment.html.erb→元のビューに反映されてフォームの記入ができるようになる。→#update→update.js.erb→each_other_comment.html.erbで更新される