【Rails】下書き機能を実装してみる

ポートフォリオとしてレシピ投稿サイトを作成した際、下書き機能を実装しました。
条件分岐のみで実現できるものの、更新（updateアクション）で苦戦したので、メモしておきます！

## 前提
- post_recipesコントローラーのcreateアクションで、新規投稿（公開）と下書き保存（非公開）を実装
- 同コントローラーのupdateアクションで、①下書き（非公開）を公開にする、②公開済の投稿を更新する、③下書き（非公開）を非公開のまま更新する
- post_recipesテーブルに「is_draft」カラム（boolean）を追加しておき、公開/非公開（下書き）のステータスを更新する
- 投稿あるいは編集画面では「公開」「下書き保存（あるいは更新）」ボタンを設置する👇
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1215197/057555fc-daab-2c78-a993-e23c5454337f.png)


## 新規投稿時の下書き機能（createアクション）
では、まず一から作成した投稿を公開せず、下書き保存する処理を記述していきます！
前提で述べたモデル・コントローラー・アクションは生成済みの前提で進めていきます

### STEP1: createアクション記述
👇```if params[:post]```で公開/非公開（下書き）の分岐を記述しています。```[:post]```の部分には、この後ビューファイルのボタンに追記するname属性の値が入ります（name属性は任意の値でOK）。

name属性については[こちら](https://www.mitsue.co.jp/knowledge/blog/frontend/201412/04_1400.html#:~:text=HTML%E3%81%AE%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0%E3%82%92%E4%BD%9C%E3%82%8B,%E3%81%8C%E5%8F%AF%E8%83%BD%E3%81%AB%E3%81%AA%E3%82%8A%E3%81%BE%E3%81%99%E3%80%82)を参考にしました。

また、saveメソッドの後ろに```(context: :publicize)```がありますが、これは「バリデーションをある状況では実行して、ある状況では実行しない」といった実装をしたい場合に使用します。後ほどモデルの部分で詳述します。

```ruby:app/controllers/post_recipes_controller.rb
  def create
    @post_recipe = PostRecipe.new(post_recipe_params)
    # 投稿ボタンを押下した場合
    if params[:post]
      if @post_recipe.save(context: :publicize)
        redirect_to post_recipe_path(@post_recipe), notice: "レシピを投稿しました！"
      else
        render :new, alert: "登録できませんでした。お手数ですが、入力内容をご確認のうえ再度お試しください"
      end
    # 下書きボタンを押下した場合
    else
      if @post_recipe.update(is_draft: true)
        redirect_to user_path(current_user), notice: "レシピを下書き保存しました！"
      else
        render :new, alert: "登録できませんでした。お手数ですが、入力内容をご確認のうえ再度お試しください"
      end
    end
  end
```

### STEP2: ビュー記述
👇name属性によって、フォーム内の2つの送信ボタンの処理を分けます（ボタンの記述のみ抜粋しています）

```html:app/views/new.html.erb
<%= form_with model: @post_recipe, local:true do |f| %> 
<!--- 中略 --->
  <div class="row mt-4 pb-5">
    <div class="form-inline mx-auto">
      <%= f.submit 'レシピを公開', :name => 'post', class: 'mr-5 btn btn-sm btn-warning text-white' %>
      <%= f.submit '下書きに保存', :name => 'draft', class: 'mr-5 btn btn-sm btn-outline-secondary' %>
    </div>
  </div>
<% end %>
```

### STEP3: モデルにバリデーション記述
👇先ほどコントローラーに```(context: :publicize)```を記述したと思います。publicizeというcontextが指定されている時に、バリデーションを適用するということになりますが、肝心の「何のバリデーションを適用するのか」をモデルに記述していきます。なお、contextの名前は自由に設定できます。

contextを使用する場合、バリデーションには```on: :publicize```のオプションが必要です。なお、```with_options```はまた別のオプションですので、詳細はググってみてください。

```ruby:app/models/post_recipe.rb
 with_options presence: true, on: :publicize do
    validates :recipe_image
    validates :serving
    validates :title
    validates :introduction
  end
  validates :title, length: { maximum: 14 }, on: :publicize
  validates :introduction, length: { maximum: 80 }, on: :publicize
```

余談ですが、そもそもなぜバリデーションをわざわざcontextで分けるのでしょうか・・・？

例えば、「投稿を公開する時には空欄を許可したくない項目」（投稿のタイトルなど。今回で言うとレシピ名）が何かしらあるとしても、下書き段階で全ての必須項目を埋めてもらう必要はないからです。「下書きにしておくのに一旦全ての必須項目を埋めなければならない」というのは、ユーザーにとって使いづらいですし、何のための下書き機能かわからなくなってしまうので、下書きの時にはpresenceやlengthのバリデーションを適用しない、という仕様にしたかったわけです。


## 下書きの更新機能の実装（updateアクション）
次に、一度作成した投稿を、更新する処理を記述していきます！

### STEP4: updateアクション記述
👇ここが苦戦したところなのですが、contextをupdateメソッドに付してもエラーになってしまいます。

①「下書き→公開」、②「公開→（内容編集の上）公開」の場合はバリデーションをかけ、③「下書き→（内容編集の上）公開せず下書きに再保存」の場合はバリデーションをかけない、というようにする必要があるため、①②についてはupdateメソッドを使わず、attributesとsaveメソッドで更新し、contextでバリデーションを指定するようにしています。

また、①については「下書き→公開」になるので、パラメーターから取得した値に（is_draft: false)をmergeしてあげます。

```ruby:app/controllers/post_recipes_controller.rb
  def update
    @post_recipe = PostRecipe.find(params[:id])
    # ①下書きレシピの更新（公開）の場合
    if params[:publicize_draft]
      # レシピ公開時にバリデーションを実施
      # updateメソッドにはcontextが使用できないため、公開処理にはattributesとsaveメソッドを使用する
      @post_recipe.attributes = post_recipe_params.merge(is_draft: false)
      if @post_recipe.save(context: :publicize)
        redirect_to post_recipe_path(@post_recipe.id), notice: "下書きのレシピを公開しました！"
      else
        @post_recipe.is_draft = true
        render :edit, alert: "レシピを公開できませんでした。お手数ですが、入力内容をご確認のうえ再度お試しください"
      end
    # ②公開済みレシピの更新の場合
    elsif params[:update_post]
      @post_recipe.attributes = post_recipe_params
      if @post_recipe.save(context: :publicize)
        redirect_to post_recipe_path(@post_recipe.id), notice: "レシピを更新しました！"
      else
        render :edit, alert: "レシピを更新できませんでした。お手数ですが、入力内容をご確認のうえ再度お試しください"
      end
    # ③下書きレシピの更新（非公開）の場合
    else
      if @post_recipe.update(post_recipe_params)
        redirect_to post_recipe_path(@post_recipe.id), notice: "下書きレシピを更新しました！"
      else
        render :edit, alert: "更新できませんでした。お手数ですが、入力内容をご確認のうえ再度お試しください"
      end
    end
  end

  private

  def post_recipe_params
    params.require(:post_recipe).permit(
      :user_id,
      :title,
      :introduction,
      :recipe_image,
      :is_draft,
      :serving
    )
  end
```

（if文が入れ子になっていたり、書き方としてはやや冗長なのですが、もし他に良い実装の仕方があればぜひ教えてください）

### STEP5: ビュー記述
👇非公開（下書き）の投稿を編集する場合は「レシピを公開」「下書きを更新」の２択、公開済みの投稿を編集する場合は「レシピを更新」の１択となるように記述します。それぞれに適当なname属性を加えておきましょう。

あと、そもそも投稿なり下書きなりを「削除したい」というユーザーのために、削除リンクも入れておきました。

```html:app/views/edit.html.erb
  <%= form_with model: @post_recipe, local:true do |f| %>
  <!---- 中略 ---->
    <div class="row mt-4">
      <div class="form-inline mx-auto">
        <% if @post_recipe.is_draft == true %>
          <%= f.submit "レシピを公開", :name => 'publicize_draft', class:'mr-5 btn btn-sm btn-warning' %>
          <%= f.submit "下書きを更新", :name => 'update_draft', class:'mr-5 btn btn-sm btn-outline-secondary' %>
        <% else %>
          <%= f.submit "レシピを更新", :name => 'update_post', class:'btn btn-sm btn-warning text-light' %>
        <% end %>
      </div>
    </div>
    <div class="mt-4 pb-3 text-center">
      <u><%= link_to "レシピを削除", "/post_recipes/#{@post_recipe.id}", method: :delete, data: {confirm: "このレシピを削除しますか？"}, class:'text-danger' %></u>
    </div>
  <% end %>
```

以上です！