大ちゃんの駆け出し技術ブログ

RUNTEQ受講生のかわいいといわれるアウトプットブログ

【Rails】【Vue】Form Objectとの連携

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

PF制作で使用したVueからForm Objectを使用した連携方法について記事が少なかったのでここに記事として残しておこうと思いました。

PF制作では以下のようなカードを実装したいと思いました。1つのカードにタイトルみたいなものがあり、その中に質問と回答が複数ある構成です。

まず必要な設計としてはquestion_blockみたいなモデルがあり、そしてその中にquestion_itemsが3つまであるという関係です。

DBに表すと以下のようになります。

https://i.gyazo.com/9ccaf69d76011cc757f30fdd4d9a730e.png

そのため、一度のフォーム入力で上記のカードを作成したいと考えていました。こんな時に必要なRailsの機能がForm Objectです!

Form Objectについての説明は割愛して、自分が上記の実装をVue × Railsどのように行なったのかを書いていきたいと思います。

実装する順番としては

  • モデルの作成

  • コントローラーの編集

  • Form Objectの作成

  • Vueでのフォーム作成

モデルの作成

まずはモデルの作成です。

上記のDB設計をもとにモデルを作成すると以下のようになります。

# app/models/question_block.rb
class QuestionBlock < ApplicationRecord
  # association =============
  belongs_to :profile_block
  has_many :question_items, dependent: :destroy
end
class QuestionItem < ApplicationRecord
  # association =============
  belongs_to :question_block
end

※ モデルの中にあるprofile_blockというのは、profile_block has_many question_blocksの関係でアソシエーションを結んでいるだけの関係にある親モデルです。

コントローラーの編集

今回は親であるquestion_blockの作成機能を使用します。

# app/controllers/api/v1/question_blocks_controller.rb
module Api
  module V1
    class QuestionBlocksController < ApplicationController

      def create
        @question_block_item_register = QuestionBlockItemRegister.new(set_params)

        if @question_block_item_register.save
          render json: @question_block_item_register
        else
          render json: @question_block_item_register.errors, status: :bad_request
        end
      end

      private

      def set_params
        params.permit(
          :question_title, :question_item_content1,
          :question_item_answer1, :question_item_content2, :question_item_answer2, :question_item_content3,
          :question_item_answer3, :current_user).merge(profile_block_id: ProfileBlock.find_by(user_id: User.find(current_user.id)).id)
      end
    end
  end
end

set_paramsの箇所で複数のquestion_itemのパラメータを受け取ることができるようになります。

def set_params
  params.permit(
    :question_title, :question_item_content1,
    :question_item_answer1, :question_item_content2, :question_item_answer2, :question_item_content3,
    :question_item_answer3, :current_user).merge(profile_block_id: ProfileBlock.find_by(user_id: User.find(current_user.id)).id)
end

最後にmergeしている箇所ですが、これはForm Objectの中ではcurrent_userを使用することができないため、パラメータとして渡しておく必要があるためです。自分はdeviseを使用しているため、current_userを使用することで、question_blockの親であるprofile_blockを紐づけるためのprofile_block_idのパラメータを渡すことができます。

また、@question_block_item_register.attributesとすることで、JSONの値を取得できます。

Form Objectの作成

Form Objectを作成するにあたり、必要そうなものをまずはincludeします。QuestionBlockItemRegisterは仮のモデルとなるため、ActiveModelをincludeしておく必要が会います。これにより、バリデーション機能など各モデルの機能をForm Object内で使用することができます。

# app/forms/question_block_item_register.rb
class QuestionBlockItemRegister
  include ActiveModel::Model
  include ActiveModel::Attributes
  include ActiveModel::Validations
end

次にパラメータを受け取る値をattributeを使用してForm Object内で定義します。これはコントローラーで指定した各パラメーターの値の定義です。

# app/forms/question_block_item_register.rb
class QuestionBlockItemRegister
  include ActiveModel::Model
  include ActiveModel::Attributes
  include ActiveModel::Validations

  attribute :question_title,         :string
  attribute :question_item_content1, :string
  attribute :question_item_answer1,  :string
  attribute :question_item_content2, :string
  attribute :question_item_answer2,  :string
  attribute :question_item_content3, :string
  attribute :question_item_answer3,  :string
  attribute :profile_block_id,       :integer
end

次にバリデーションも定義してしまいましょう。これもモデルで定義する時と同じように設定できます。question_item_answer1以降のpresence: trueを指定しない理由ですが、question_blockが持つitemの数は必ずしも3つではなく、2つ、1つでも登録できるようにするためです。

# app/forms/question_block_item_register.rb
class QuestionBlockItemRegister
  include ActiveModel::Model
  include ActiveModel::Attributes
  include ActiveModel::Validations

  attribute :question_title,         :string
  attribute :question_item_content1, :string
  attribute :question_item_answer1,  :string
  attribute :question_item_content2, :string
  attribute :question_item_answer2,  :string
  attribute :question_item_content3, :string
  attribute :question_item_answer3,  :string
  attribute :profile_block_id,       :integer

  # validation =============
  validates :question_title,         presence: true,        length: { maximum: 30 }
  validates :question_item_content1, presence: true,        length: { maximum: 30 }
  validates :question_item_answer1,  presence: true,        length: { maximum: 30 }
  validates :question_item_content2,                        length: { maximum: 30 }
  validates :question_item_answer2,                         length: { maximum: 30 }
  validates :question_item_content3,                        length: { maximum: 30 }
  validates :question_item_answer3,                         length: { maximum: 30 }
  validates :profile_block_id,        presence: true
end

最後にDBに登録する際のトランザクションを記述します。

# app/forms/question_block_item_register.rb
class QuestionBlockItemRegister
  include ActiveModel::Model
  include ActiveModel::Attributes
  include ActiveModel::Validations

  attribute :question_title,         :string
  attribute :question_item_content1, :string
  attribute :question_item_answer1,  :string
  attribute :question_item_content2, :string
  attribute :question_item_answer2,  :string
  attribute :question_item_content3, :string
  attribute :question_item_answer3,  :string
  attribute :profile_block_id,       :integer

  # validation =============
  validates :question_title,         presence: true,        length: { maximum: 30 }
  validates :question_item_content1, presence: true,        length: { maximum: 30 }
  validates :question_item_answer1,                         length: { maximum: 30 }
  validates :question_item_content2,                        length: { maximum: 30 }
  validates :question_item_answer2,                         length: { maximum: 30 }
  validates :question_item_content3,                        length: { maximum: 30 }
  validates :question_item_answer3,                         length: { maximum: 30 }
  validates :profile_block_id,        presence: true

    def save
    return if invalid?
    ActiveRecord::Base.transaction do
      question_block = QuestionBlock.create!(title: question_title, profile_block_id: profile_block_id)

      question_block.question_items.create!(content: question_item_content1, answer: question_item_answer1) if question_item_content1.present?
      question_block.question_items.create!(content: question_item_content2, answer: question_item_answer2) if question_item_content2.present?
      question_block.question_items.create!(content: question_item_content3, answer: question_item_answer3) if question_item_content3.present?
    end
    true
  end
end

saveメソッドについてはクラスメソッドとなるので、先ほどcreateアクション内で使用しているメソッド名と同じにします。ここは、普段使用しているDB保存のためのsaveメソッドとは異なるのでお間違いなく。

def create
  @question_block_item_register = QuestionBlockItemRegister.new(set_params)

  if @question_block_item_register.save
    render json: @question_block_item_register.attributes
  else
    render json: @question_block_item_register.errors, status: :bad_request
  end
end

ActiveRecord::Base.transactionを使用する理由ですが、これはトランザクションを一つにまとめることができる機能です。なぜまとめる必要があるのかというと、create!メソッドは使用された時点で成功すればDBに登録されますが、一度に複数モデルを作成するForm Objectでは複数モデルが全て登録されたことを保証するためです。もし、仮にQuestionBlock.create!が成功して、question_block.question_items.create!が失敗すると、itemのないただのブロックが作成されてしまいます。

ActiveRecord::Base.transaction do
  question_block = QuestionBlock.create!(title: question_title, profile_block_id: profile_block_id)

  question_block.question_items.create!(content: question_item_content1, answer: question_item_answer1) if question_item_content1.present?
  question_block.question_items.create!(content: question_item_content2, answer: question_item_answer2) if question_item_content2.present?
  question_block.question_items.create!(content: question_item_content3, answer: question_item_answer3) if question_item_content3.present?
end

また、トランザクションが完了した後にtrueを返却しなければなりません。なぜかというと、@question_block_item_register.saveは普段使用しているsaveメソッドと同様に、真偽値を返す必要があるためです。trueを返却しなければnilが返却されることになり、必ず条件分岐はelseの結果を返すことになります。

def save
  return if invalid?
・
・
  true # 必ず必要
end

Vueでのフォーム作成

最後にvueでパラメータを送ってあげます。Vueのtemplateタグ内は自分のPF上の見た目を表すコードになるため割愛します。どうやってaxiosを使用してパラメータを渡すかのみを使用します。

まずフォームのコンポーネントscriptタグ内は以下のようになります。

<script>
export default {
  data() {
    return {
      questionBlock: {
        title: "",
      },
      questionItem: {
        content: "",
        answer: "",
      },
    };
  },
  methods: {
    ...mapActions({
      createQuestionBlock: "questionBlocks/createQuestionBlock",
    }),
    hundleCreateQuestionBlock(questionBlock, questionItem) {
      if (questionBlock.title == "" || questionItem.content == "") return;
      this.createQuestionBlock({
        questionBlock: questionBlock,
        questionItem: questionItem,
      });
    },
  },
};
</script>

重要なのはvuexにて定義しているcreateQuestionBlockメソッドの引数をオブジェエクトを使用して渡していることです。なぜかというと、vuexのactionsのメソッド、mutationsのメソッドは引数を一つしか持てないためです。そのため、オブジェクトの中に入れることで、引数を一つに保ちます。

...mapActions({
  createQuestionBlock: "questionBlocks/createQuestionBlock",
}),
hundleCreateQuestionBlock(questionBlock, questionItem) {
・
・
  this.createQuestionBlock({
    questionBlock: questionBlock,
    questionItem: questionItem,
  });

次にvuexで非同期処理を行うcreateQuestionBlockメソッドについてです。

createQuestionBlock({ commit }, { questionBlock, questionItem }) {
  const params = {
    question_title: questionBlock.title,
    question_item_content1: questionItem.content,
    question_item_answer1: questionItem.answer,
  };
  axios
    .post("question_blocks", params)
    .then((response) => {
      commit("addQuestionBlock", response.data);
    })
    .catch((err) => console.log(err.status));
},

引数をオブジェクトを使用して表しています。ここでサーバー側で受け取るパラメータを定数paramsとして定義します。

const params = {
  question_title: questionBlock.title,
  question_item_content1: questionItem.content,
  question_item_answer1: questionItem.answer,
};

あとはそのパラメータをpostとして送信するだけです。

post("question_blocks", params)

少し雑だったかもしれませんが、全部読めば外観はつかめたのではないかなと思います。特にVue側でのparamsの値に変換しておく方法はあまり記事では見かけませんでした。

const params = {
  question_title: questionBlock.title,
  question_item_content1: questionItem.content,
  question_item_answer1: questionItem.answer,
};

この記事がRails × Vueの実装で誰かのお役に立てればと思います。

参考文献

  • フォームオブジェクトについて

product-development.io

qiita.com

  • Vuexでのaction, mutationの引数指定での注意点

qiita.com