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

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

【Vue】【Rails】selectタグのv-modelとActive Hashの更新

はじめに

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

Active Hashを使用して出身地を登録するフォームを作っていたのですが、そこで少しつまずいたので備忘録として残しておきます。

つまづいた箇所

以下のようにActive Hashを使用して県名を登録しています。

class Prefecture < ActiveHash::Base
  self.data = [
    { id: 1, name: '北海道' }, { id: 2, name: '青森県' }, { id: 3, name: '岩手県' },
    { id: 4, name: '宮城県' }, { id: 5, name: '秋田県' }, { id: 6, name: '山形県' },
    { id: 7, name: '福島県' }, { id: 8, name: '茨城県' }, { id: 9, name: '栃木県' },
    { id: 10, name: '群馬県' }, { id: 11, name: '埼玉県' }, { id: 12, name: '千葉県' },
    { id: 13, name: '東京都' }, { id: 14, name: '神奈川県' }, { id: 15, name: '新潟県' },
    { id: 16, name: '富山県' }, { id: 17, name: '石川県' }, { id: 18, name: '福井県' },
    { id: 19, name: '山梨県' }, { id: 20, name: '長野県' }, { id: 21, name: '岐阜県' },
    { id: 22, name: '静岡県' }, { id: 23, name: '愛知県' }, { id: 24, name: '三重県' },
    { id: 25, name: '滋賀県' }, { id: 26, name: '京都府' }, { id: 27, name: '大阪府' },
    { id: 28, name: '兵庫県' }, { id: 29, name: '奈良県' }, { id: 30, name: '和歌山県' },
    { id: 31, name: '鳥取県' }, { id: 32, name: '島根県' }, { id: 33, name: '岡山県' },
    { id: 34, name: '広島県' }, { id: 35, name: '山口県' }, { id: 36, name: '徳島県' },
    { id: 37, name: '香川県' }, { id: 38, name: '愛媛県' }, { id: 39, name: '高知県' },
    { id: 40, name: '福岡県' }, { id: 41, name: '佐賀県' }, { id: 42, name: '長崎県' },
    { id: 43, name: '熊本県' }, { id: 44, name: '大分県' }, { id: 45, name: '宮崎県' },
    { id: 46, name: '鹿児島県' }, { id: 47, name: '沖縄県' }
  ]
end

コントローラーはprefecture_idをパラメーターとして受け取り登録しています。

def create
  @profile = current_user.build_profile(profile_params)

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

private

def profile_params
  params.require(:profile).permit(:height, :gender, :blood_type, :prefecture_id)
end

ここで、フロント側に渡すJSONの値として、prefecture_idをそのまま渡してしまうと、Active Hashで定義しているidが返ります。例えば、北海道ならprefecture_idが1と返ってきます。フロント側では県名を表示したいので、ActiveModelSerializersを使用して、prefecture.nameとすることで県名の値(idが1なら北海道)を返却します。

# app/serializers/profile_serializer.rb
class ProfileSerializer < ActiveModel::Serializer
  attributes :id,:height, :gender, :blood_type, :prefecture_id
  belongs_to :user

  def prefecture_id
    object.prefecture.name
  end
end

このActiveModelSerializersの箇所は過去記事で説明しているのでよければ参照ください。

sakitadaiki.hatenablog.com

さて、詰まった箇所は以下の部分です。

県名の編集のためにvue側でも同じようにprefecturesで定義し、それを<template>タグ内で繰り返し処理でoptionに展開していました。

data() {
    return {
      prefectures: [
        { text: "北海道", value: "1" },
        { text: "青森県", value: "2" },
        { text: "岩手県", value: "3" },
        { text: "宮城県", value: "4" },
        { text: "秋田県", value: "5" },
        { text: "山形県", value: "6" },
        { text: "福島県", value: "7" },
        { text: "茨城県", value: "8" },
        { text: "栃木県", value: "9" },
        { text: "群馬県", value: "10" },
        { text: "埼玉県", value: "11" },
        { text: "千葉県", value: "12" },
        { text: "東京都", value: "13" },
        { text: "神奈川県", value: "14" },
        { text: "新潟県", value: "15" },
        { text: "富山県", value: "16" },
        { text: "石川県", value: "17" },
        { text: "福井県", value: "18" },
        { text: "山梨県", value: "19" },
        { text: "長野県", value: "20" },
        { text: "岐阜県", value: "21" },
        { text: "静岡県", value: "22" },
        { text: "愛知県", value: "23" },
        { text: "三重県", value: "24" },
        { text: "滋賀県", value: "25" },
        { text: "京都府", value: "26" },
        { text: "大阪府", value: "27" },
        { text: "兵庫県", value: "28" },
        { text: "奈良県", value: "29" },
        { text: "和歌山県", value: "30" },
        { text: "鳥取県", value: "31" },
        { text: "島根県", value: "32" },
        { text: "岡山県", value: "33" },
        { text: "広島県", value: "34" },
        { text: "山口県", value: "35" },
        { text: "徳島県", value: "36" },
        { text: "香川県", value: "37" },
        { text: "愛媛県", value: "38" },
        { text: "高知県", value: "39" },
        { text: "福岡県", value: "40" },
        { text: "佐賀県", value: "41" },
        { text: "長崎県", value: "42" },
        { text: "熊本県", value: "43" },
        { text: "大分県", value: "44" },
        { text: "宮崎県", value: "45" },
        { text: "鹿児島県", value: "46" },
        { text: "沖縄県", value: "47" },
      ],

<template>タグ内で繰り返し処理で展開

<select
  v-model="editProfile.prefecture_id"
  name="profile[prefecture_id]"
>
  <option
    v-for="prefecture in prefectures"
    :key="prefecture.value"
    :value="prefecture.value"
  >
    {{ prefecture.text }}
  </option>
</select>

:value="prefecture.value"とすることでサーバー側に渡すパラメータはidで渡すようにしています。これはActive Hashで更新するためのパラメータがidであるためです。

※この部分については少し冗長かなと思ったので、のちほど改善したいと思っています。例えば、Active Hashで登録している値をAPIレスポンスで受け取りそれをフロント側で表示することで、サーバー側のActive Hashの値とフロント側の値を同じにすることができます。今だと、フロント側とサーバー側で別々で県名を指定しているので冗長かなと思います。

ここで問題となったのがv-modelの値はoptionの中のvalueと一致しないと編集画面を表示したときに、何も選択されていない状態で表示されてしまうことです。

https://i.gyazo.com/d873656fca281710872e0dd3bf51b1a6.png

editProfile.prefecture_idは上述したように県名を表示するために、値はidではなく県名となっています。しかし、selectタグ内ではvalueidとなっているために県名がどのoptionのvalueにも当てはまらない状態です。そのため、editProfile.prefecture_idには値が格納されていますが、図のようにフォームに何も選択されていないような状態で表示されてしまうのです。

解決策

ベストプラクティスかは不明なのですが、optionvalueを県名と一致させる方法を取りました。つまり、:value="prefecture.text"とすることで、valueが県名になるようにしました。

<option
  v-for="prefecture in prefectures"
  :key="prefecture.value"
  :value="prefecture.text"
>
  {{ prefecture.text }}
</option>

これで編集画面を開いたときに、valuev-modelの値が一致するために初期状態で値が選択されているようになります。

https://i.gyazo.com/29b86400058f90ed491247fd2f7e5550.png

しかし、これだとサーバー側で受け取るパラメーターとしてはidではなく県名になってしまい、正常に値を更新できなくなってしまいます。そこで、更新するときにパラメータを変更するようにしました。

const selectedPrefecture = this.prefectures.find(
  (prefecture) => prefecture.text == editBasicProfile.prefecture_id
);
editBasicProfile.prefecture_id = selectedPrefecture.value;

dataで登録している県名の中からパラメーターとして渡っているeditBasicProfile.prefecture_idと一致する県名のオブジェクトを取得します。そして、そのオブジェクトに格納されているvalue(id)を代入することで値をidに無理やり変更しています。

これにより無事値を更新することができました!

【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

【Vue】vee-Validatorのメッセージをリセット

vee-Validatorの副作用

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

今日は文章量少なめです💦

PF制作時にvee-Validatorのフォームで少し困ったことがありました。

下記のように入力フォームにバリデーションをしていたとします。未入力の場合バリデーションメッセージを表示すると言うものですね。

https://i.gyazo.com/93262c14e0843df8b9592c3dadb85255.png

この入力フォームはvuetifyのダイアログを使用していたのですが、この入力フォームは閉じる時に値のリセットを行います。

clearTextBlock() {
  this.textFormat = {};
},

しかし、これを行うと不具合が生じました。

何も入力しないでダイアログを閉じて再度入力フォームを開くと、バリデーションメッセージが表示された状態で開かれてしまいます。


何も入力しないでダイアログを閉じる

https://i.gyazo.com/935ab30d78cc970bd7e91128a457034d.png

ダイアログを開く

https://i.gyazo.com/93262c14e0843df8b9592c3dadb85255.png


これが起きる理由として、clearTextBlockメソッドが実行されることでフォームが空になったと認識されてしまうためです。空になったと認識されれば、フォームに指定しているValidationProviderが実行されてしまい、未入力と判断されてしまいます。結果、再度ダイアログを表示した時に、バリデーションメッセージが表示されてしまうわけです。

解決方法

これについては解決方法が複数あるようでしたが、公式の情報を参考に実装しました。

Validation Observer | VeeValidate

フォームの値を空にする処理の後に、vee-Validatorのバリデーションを一度リセットすることができる処理を入れます。

clearTextBlock() {
  this.textBlock = {};
  this.$refs.observer.reset();
},

これにより、ダイアログを閉じる際に表示されていたバリデーションメッセージが消え、次に開く時にはバリデーションメッセージが表示されていないダイアログを表示できます。

意外と簡単に解決できてよかったです。

参考文献

vee-validate.logaretm.com

コメント

次はいよいよ50記事目です!目標としていただけあってすごく嬉しい!

【Gem】ActiveModelSerializers

はじめに

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

RailsAPIモードを使用してJSONデータを受け取る時にActiveModelSerializersというgemがとても重宝したので紹介します。JSONのレンダーを簡易的に行ってくれるので、Vue.jsなどを使用する場合は是非こちらのgemを使ってみてください。

github.com

インストール

現在の最新版を使用します。Gemファイルに記載し、bundle install

# Gemfile
gem 'active_model_serializers', '~> 0.10.0'
$ bundle install
Installing jsonapi-renderer 0.2.2
Installing case_transform 0.2
Fetching active_model_serializers 0.10.12
Installing active_model_serializers 0.10.12

JSONを返却するシリアライザーを作成します。作成方法は下記コマンドのとおりです

$ rails g serializer モデル名

今回自分がこちらのgemで使用したのはUserモデルとProfileモデルですので、下記のようにコマンドを実行しました。コマンド実行後、app/serializers/配下にシリアライザ用のファイルが作成されます。

$ rails g serializer User
      create  app/serializers/user_serializer.rb
$ rails g serializer Profile
      create  app/serializers/profile_serializer.rb

デフォルトでは中身がattributes :idと記載されているファイルが作成されます。

class User < ActiveModel::Serializer
  attributes :id
end

attributesにはJSONで取得したい値を記述します。今回自分のUserモデルでは:nameカラム、:imageカラムを取得したいので追加しました。

class UserSerializer < ActiveModel::Serializer
  attributes :id, :name, :image
end

また、アソシエーション関係にあるJSONの値も取得したい場合、アソシエーションを表すhas_many, has_oneなどを記載すればアソシエーション関連の値も取得できます。

class UserSerializer < ActiveModel::Serializer
  attributes :id, :name, :image
    has_one :profile
end

Profileモデルの属性も同様に指定します。

class ProfileSerializer < ActiveModel::Serializer
  attributes :id, :birthday, :day_of_joinning, :height, :gender, :blood_type, :prefecture_id
  belongs_to :user
end

以上で準備完了です!

ほんとにお手軽です!

使用例

ではJSONを実際に取得してみます。

  • User情報一覧の取得の場合

http://localhost:3000/api/v1/users/

def index
  @users = User.all
  render json: ActiveModel::Serializer::CollectionSerializer.new(
    @users,
    serializer: UserSerializer
  ).to_json
  end

ActiveModel::Serializer::CollectionSerializerが気になるかと思いますが、リソースの集合をシリアライザで表現する時に使用するようです。だいたいリソースが複数となるindexアクションでしか使用しません。serializer: UserSerializerとすることで使用するシリアライザを指定します。

返却されるJSON

[{"id":8,"name":"大ちゃん","image":"~~~~省略~~~~-192.png","profile":{"id":5,"birthday":"2021-04-02T00:00:00.000Z","day_of_joinning":"2021-04-04T00:00:00.000Z","height":154,"gender":"male","blood_type":"B","prefecture_id":13}}]

現在UserのレコードとProfileのレコードが1つずつしかないので返却される値は少なく見えますが、ちゃんと値が返ってきています。

  • Profile一覧の取得の場合

http://localhost:3000/api/v1/profiles/

def index
   @profiles = Profile.all
  render json: ActiveModel::Serializer::CollectionSerializer.new(
    @profiles,
    serializer: ProfileSerializer
  ).to_json
end

返却されるJSON

[{"id":5,"birthday":"2021-04-02T00:00:00.000Z","day_of_joinning":"2021-04-04T00:00:00.000Z","height":154,"gender":"male","blood_type":"B","prefecture_id":13,"user":{"id":8,"name":"大ちゃん","image":"~~~~省略~~~~-192.png"}}]

リアライザーの値をメソッドで定義する

返却するJSONの値をシリアライザ側で加工して渡すこともできます。

例えば、先ほど取得したプロフィールカラムの性別を日本語に加工したいとしましょう。

"gender":"male""gender":"男性"

この場合enum_helpを使用していることが前提になります。

# config/locales/enums.ja.yml
ja:
  enums:
    profile:
      gender:
        male: "男性"
        female: "女性"

リアライザで変換したい場合、以下のようにメソッドを定義することで変換が可能です。

class ProfileSerializer < ActiveModel::Serializer
  attributes :id, :birthday, :day_of_joinning, :height, :gender, :blood_type, :prefecture_id
  belongs_to :user

  def gender
    object.gender_i18n
  end
end

objectはモデル自体です。gender_i18nとすることでenum_helpyamlファイルで定義した値に変換することができます。元の値をJSONファイルで取得する必要がない場合、attributes自体に変更は必要ありません。これで日本語に変換された値を取得できます。

プロフィールのprefecture_idも変換します。prefecture_idはActive Hashを使用して定義しています。

class Prefecture < ActiveHash::Base
  self.data = [
    { id: 1, name: '北海道' }, { id: 2, name: '青森県' }, { id: 3, name: '岩手県' },
    { id: 4, name: '宮城県' }, { id: 5, name: '秋田県' }, { id: 6, name: '山形県' },
    { id: 7, name: '福島県' }, { id: 8, name: '茨城県' }, { id: 9, name: '栃木県' },
    { id: 10, name: '群馬県' }, { id: 11, name: '埼玉県' }, { id: 12, name: '千葉県' },
    { id: 13, name: '東京都' }, { id: 14, name: '神奈川県' }, { id: 15, name: '新潟県' },
    { id: 16, name: '富山県' }, { id: 17, name: '石川県' }, { id: 18, name: '福井県' },
    { id: 19, name: '山梨県' }, { id: 20, name: '長野県' }, { id: 21, name: '岐阜県' },
    { id: 22, name: '静岡県' }, { id: 23, name: '愛知県' }, { id: 24, name: '三重県' },
    { id: 25, name: '滋賀県' }, { id: 26, name: '京都府' }, { id: 27, name: '大阪府' },
    { id: 28, name: '兵庫県' }, { id: 29, name: '奈良県' }, { id: 30, name: '和歌山県' },
    { id: 31, name: '鳥取県' }, { id: 32, name: '島根県' }, { id: 33, name: '岡山県' },
    { id: 34, name: '広島県' }, { id: 35, name: '山口県' }, { id: 36, name: '徳島県' },
    { id: 37, name: '香川県' }, { id: 38, name: '愛媛県' }, { id: 39, name: '高知県' },
    { id: 40, name: '福岡県' }, { id: 41, name: '佐賀県' }, { id: 42, name: '長崎県' },
    { id: 43, name: '熊本県' }, { id: 44, name: '大分県' }, { id: 45, name: '宮崎県' },
    { id: 46, name: '鹿児島県' }, { id: 47, name: '沖縄県' }
  ]
end

Active Hashへの値の変換方法は[object.prefecture.変換したい値]とすることで変換できます。今回の場合、object.prefecture.idでPrefectureモデルで指定した都道府県ID、object.prefecture.name都道府県名を取得できます。今回は都道府県名を取得します。

# app/serializers/profile_serializer.rb
class ProfileSerializer < ActiveModel::Serializer
  attributes :id, :birthday, :day_of_joinning, :height, :gender, :blood_type, :prefecture_id
  belongs_to :user

  def gender
    object.gender_i18n
  end

  def prefecture_id
    object.prefecture.name
  end
end

最後に日付のフォーマットも整形したいですね。不格好です。

"birthday":"2021-04-02T00:00:00.000Z"
"day_of_joinning":"2021-04-04T00:00:00.000Z"

strftimeメソッドを使えば簡単にフォーマットを整形できます。

# app/serializers/profile_serializer.rb
class ProfileSerializer < ActiveModel::Serializer
  attributes :id, :birthday, :day_of_joinning, :height, :gender, :blood_type, :prefecture_id
  belongs_to :user

  def gender
    object.gender_i18n
  end

  def prefecture_id
    object.prefecture.name
  end

  def birthday
    object.birthday.strftime("%F")
  end

  def day_of_joinning
    object.day_of_joinning.strftime("%F")
  end
end

参考文献

github.com

qiita.com

【Vue】【自戒】computedとmethodsの相違点

過ち

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

今回はタイトルに【自戒】とあるように同じ過ちを繰り返さないために書いている記事になります笑

何をやらかしたのかというと、Vueファイルに下記のようなコードを書いた事です。

<template>
・
・
    {{ translateGender(profile.gender) }}
・
・
</template>

<script>
・
・
    methods: {
    translateGender(gender) {
      return gender == "male" ? "男性" : "女性";
    },
    },
・
・
</script>

profile.genderはサーバー側でenumで定義していました。

# app/models/profile.rb
class Profile < ApplicationRecord
  enum gender: { male: 0, female: 1 }

それにより、プロフィールをAPIで取得する時、返却されるJSONファイルのgenderの値はmale、もしくはfemaleでした。しかし、これを男性女性のいずれかでフロント側では表示したかったので下記のようにgenderの値がmaleの時(gender == "male")は男性を返却し、それ以外の値は女性と返却するメソッドを三項演算子を用いて定義しました。

translateGender(gender) {
  return gender == "male" ? "男性" : "女性";
},

実際こちらのコードは正しく動作し、translateGender(profile.gender)の値は男性もしくは女性と変換された形式で表示されます。

しかし、今回は以下のように処理は算出プロパティで定義すべきです。

<template>
・
・
    {{ translateGender }}
・
・
</template>

<script>
・
・
    computed: {
    translateGender() {
      return this.profile.gender == "male" ? "男性" : "女性";
    },
    },
・
・
</script>

問題点

何が今回のコードで問題だったのでしょうか。

こちらのコードの問題点は算出プロパティで定義しなかった事です。

上述したように男性女性の値に返却するtranslateGenderメソッドはmethodsで囲ってあります。

methods: {
  translateGender(gender) {
    return gender == "male" ? "男性" : "女性";
  },
},

しかし、今回自分がこちらの実装で行いたかったことは、既存データを加工するという実装です。このような既存のデータを加工して取得する機能をVue.jsでは算出プロパティで定義することがセオリーとなっています。

これは自分が通勤電車でVue.jsのテキストを読んでいたときにそのような記載がありました。メソッドと算出プロパティの違いが記載されている該当箇所をいかに抜粋します。

算出プロパティの用途は、基本的に既存データの「加工を伴う取得」です。一方、メソッドはデータの取得に加え、操作や更新にも利用できます。・・・<省略>・・・算出プロパティでできることはメソッドでもできます。ただし、引数を伴わない単純な加工や演算なのであれば、算出プロパティを利用した方がコードの意図が明確になります。

https://wings.msn.to/index.php/-/A-03/978-4-8156-0182-9/

このように、単純なデータの加工のみでデータを更新したりしない場合は、算出プロパティで定義するべきということです。

今回の件については、それほど大きな過ちではないのではないかと思ったのですが、同じテキストで算出プロパティとメソッドの明確な違いについて述べている箇所を読んで今回は算出プロパティにするべきだったと思いました。

そこで使用されている例のコードをそのまま使用させていただきます。(テキストの例はシングルファイルコンポーネントの形式ではないですが、、、)

<div id="app">
  <form>
    <input type="button" value="クリック" v-on:click="onclick" />
  </form>
  <div>算出プロパティ:{{ randomc }}</div>
  <div>メソッド:{{ randomm() }}</div>
  <div>現在日時:{{ current }}</div>
</div>
new Vue({
  el: '#app',
  data: {
    current: new Date().toLocaleString()
  },
  computed: {
    randomc: function() {
      return Math.random();
    }
  },
  methods: {
    onclick: function() {
      this.current = new Date().toLocaleString();
    },
    randomm: function() {
      return Math.random();
    }
  }
});

randomcは算出プロパティ

computed: {
  randomc: function() {
    return Math.random();
  }
},

randomm()はメソッド

methods: {
  randomm: function() {
    return Math.random();
  }
}

どちらも同じMath.random()でランダムな乱数を返却するという処理をしています。

表示される画面は以下のようになります。上の乱数部分が算出プロパティで下の乱数部分がメソッドです。

https://i.gyazo.com/6917dfbe9e650be4de8088cd56f3e0f1.png

v-on:click="onclick"によってイベントが発生するので、算出プロパティ、メソッドの値も変わることが期待されます。しかし、クリックを押すとメソッドの乱数の値は変わりますが、算出プロパティの値は変わりません。

https://i.gyazo.com/10412796e5c29551080e5fbedb0bf3ab.png

これこそが算出プロパティとメソッドの最大の違いです。

なぜこのようなことが起きるのかというと、メソッドは実行されるたびに定義されている処理を実行するのですが、算出プロパティでは依存プロパティに変更がない場合、過去に表示した値がキャッシュされていてその値を使用します。

今回自分が修正したコードを使用して説明すると、算出プロパティで使用しているthis.profile.genderの値がdataオブジェクト側で変更されない限り、前の処理結果をそのまま返却するということです!

computed: {
  translateGender() {
    return this.profile.gender == "male" ? "男性" : "女性";
  },
},

一見算出プロパティでも表示している内容は変わりませんが、それが処理を挟まずにキャッシュから表示したものと毎回処理を評価されるものでは、明らかにコードの処理の重さが違います。

そのため、単純な既存データの返却に関しては必ず算出プロパティを使うことを肝に銘じておこうと思いました。

今回は以上です!

参考記事

https://wings.msn.to/index.php/-/A-03/978-4-8156-0182-9/

https://qiita.com/yukibe/items/f49fcb935363200916fe

【Gem】Banken

はじめに

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

2ヶ月ほど前にPunditの記事を書きましたが、今回はそれとよく似たgemのBankenについて書いていきたいと思います。

こちらのgemを作ってくださったのは日本人の方で英語のドキュメントだけでなく日本語のドキュメントも用意されています。

  • 日本語のドキュメント

github.com

日本人が作った日本語ドキュメントなので本当にわかりやすく導入することができました。

まずはインストール方法を先に紹介します。ただ、これは日本語ドキュメントがありますので、日本語ドキュメントを参照していただいた方が早いと思ってます。

インストール方法

上記ドキュメントに従ってインストールします。

Gemfileに追記しbundle install

# Gemfile
gem "banken"

# ターミナル
$ bundle
Fetching banken 1.0.3
  • アプリケーションコントローラでBankenをinclude
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include Banken
  • ターミナル上でコマンドを使用してインストール

application_loyalty.rbというファイルが作成されます。

$ rails g banken:install
      create  app/loyalties/application_loyalty.rb

作成されたファイル

# app/loyalties/application_loyalty.rb
class ApplicationLoyalty
  attr_reader :user, :record

  def initialize(user, record)
    @user = user
    @record = record
  end

  def index?
    false
  end

  def show?
    false
  end

  def create?
    false
  end

  def new?
    create?
  end

  def update?
    false
  end

  def edit?
    update?
  end

  def destroy?
    false
  end
end

ここまでで基本インストールはおしまいです。

要件

今回自分がPFに使用した例を紹介します。

自分のPFではログイン後ユーザーがプロフィールを作成していない場合、まずはプロフィール新規作成画面に遷移します。ここでユーザーはプロフィール作成が終わるまで、プロフィール新規作成画からはどこのページにも遷移できないように制御したいです。

また、新規作成後はプロフィール新規作成画面に戻る事ができない仕様にしたいです。ユーザーとプロフィールのアソシエーションはhas_oneの関係(1 : 1)なので、ユーザーが1つのプロフィールを既に作成していれば、新規作成画面に遷移する必要がないからです。

# app/models/profile.rb
class Profile < ApplicationRecord
  belongs_to :user

# app/models/user.rb
class User < ApplicationRecord
    has_one :profile, dependent: :destroy

以下2つの項目を制御できるようにBankenを使用してみます。

  • 基本プロフィール未作成時にプロフィール一覧に遷移できない
  • プロフィール作成後はプロフィール作成画面に遷移できない

使用方法

  • Loyaltyクラスの作成

制御したいアクションがあるコントローラの名前と同名のLoyaltyクラスを作成します。以下公式ドキュメントを引用。


公式ドキュメントの例

Bankenではcontrollerに一対一で紐づく同名のloyaltyクラスを作成していきます。 今回はPostsControllerに対して制御を行うのでPostsLoyaltyクラスを作成する必要があります。

> rails g banken:loyalty posts
      create  app/loyalties/posts_loyalty.rb

今回はProfilesControllerの新規作成画面での遷移(newアクション)と一覧画面への遷移(indexアクション)に対して認可制御を行います。

rails g banken:loyalty profiles
      create  app/loyalties/profiles_loyalty.rb

新しくProfilesLoyaltyクラスが作成されました。しかし、中身はクラスが定義されているだけでメソッド等のロジックは記載されていません。

# app/loyalties/profiles_loyalty.rb
class ProfilesLoyalty < ApplicationLoyalty
end

基本プロフィール未作成時にプロフィール一覧に遷移できない

まずは新規作成画面からどこの画面にも遷移できないように制御します。

  • プロフィール一覧画面を表示させるindexアクションに対してauthorize!メソッドを設置

※ 今回はAPI化を行っているため、特にこちらのコントローラに記載はしておりません。

# app/controllers/profiles_controller.rb
class ProfilesController < ApplicationController

  def index
    authorize!
  end

authorize!メソッドについてですが、実行されたアクション名に紐づいたLoyaltyクラス内のアクション名?メソッドを実行します。今回はProfilesControllerindexアクションの中でauthorize!メソッドが実行されているので、コントローラに紐づいているProfilesLoyaltyindex?メソッドが実行されます。

  • ProfilesLoyaltyindex?に対してuser.profile.present?を記述

index?メソッドがまだ定義されていませんので定義します。?とあるようにこのLoyaltyクラス内のメソッドは真偽値を返さないといけません。そこで今回は要件に従うようににcurrent_userがプロフィールを所持しているかどうかに対してboolean値を返すようにします。

# app/loyalties/profiles_loyalty.rb
class ProfilesLoyalty < ApplicationLoyalty
  def index?
    user.profile.present?
  end
end

このメソッドの中にあるuserはどこからくるの?と思うかもしれませんが、実はこのusercurrent_userと同じです。Bankenの便利なところで上記のauthorize!メソッドは下記のことと同じことを実行します。

raise "not authorized" unless ProfilesLoyalty.new(current_user).index?

勝手に引数にcurrent_userを渡してくれていますね。なのでauthorize!メソッドで引数を渡さずとも、current_userは勝手に渡されており、それがindex?アクションの中でuserとして扱われています。

※ 上記のようにcurrent_userを必ず使用するため、current_userが使用できないとエラーが発生します。よって、必ずcurrent_userを使えるようにしておく必要があります。自分の場合、deviseを使用してcurrent_userを使えるようにしております。

  • application_controllerに例外処理を記載

上記のuser.profile.present?でfalseが返ると例外が発生します。例外をハンドリングするためにApplicationControllerにエラーハンドリングのロジックを書きます。

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include Banken

  # エラーハンドリング =============
  rescue_from Banken::NotAuthorizedError, with: :user_not_authorized

  private

  def user_not_authorized(exception)
    redirect_to(request.referer || root_path)
  end

request.refererは遷移する前の元の画面のURLを取得します。プロフィール新規作成画面から遷移しようとしBankenで例外が生じると、プロフィール新規作成画面に戻るということになります。そして元の画面のURLがnilの場合はroot_pathに遷移するようにします。

ここで問題なのが、request.refererはボタンやリンクなどから元の画面のURLを取得するのですが、直接ブラウザにURLを打ち込まれて画面遷移した場合、request.refererの値はnilになってしまいます。よって、新規作成画面にいる状態でブラウザに/profilesを打ち込むと、root_pathにリダイレクトされていしまいます。しかし今回の実装の場合、プロフィール新規作成が終了するまでは他の画面に遷移する事はできないようにしたいので、root_pathにも遷移したくありません。

なのでroot_pathのアクションに対して直接ロジックを書きます。

def index
  if user_signed_in? && current_user.profile.nil?
    redirect_to new_profile_path
    end
end

ユーザーがuser_signed_in?していて尚且つプロフィールを持っていないのであれば、新規作成画面にリダイレクトします。少し冗長ですがこれで第一の要件を満たす事はできました。

プロフィール作成後はプロフィール作成画面に遷移できない

次の要件を満たすために先ほどと同じようにauthorize!メソッドを使用します。今回はnewアクションですね。

# app/controllers/profiles_controller.rb
class ProfilesController < ApplicationController

  def index
    authorize!
  end

 def new
    authorize!
 end

newアクションに対応するnew?メソッドをProfilesLoyaltyクラスに定義します。今回は新規作成済みの場合の制御となるのでuser.profile.present?とは逆のロジックを書けばいいと思います。

# app/loyalties/profiles_loyalty.rb
class ProfilesLoyalty < ApplicationLoyalty
  def index?
    user.profile.present?
  end

  def new?
      user.profile.nil?
  end
end

これで実装はできているのですが、先ほどと同じように直接ブラウザ上で新規作成画面のURLを打ち込むと、request.referernilになりますので、このままだとトップページにリダイレクトします。

それを制御するためには少々手荒な実装ですが、下記のように記述して解決しました。

def index
  if user_signed_in? && current_user.profile.nil?
    redirect_to new_profile_path
  elsif user_signed_in?
    redirect_to profiles_path
  end
end

user_signed_in?をさらに下に書く事で実装できました。これによりプロフィールは持っているけども、サインインしている状態のユーザーがプロフィール一覧画面にリダイレクトします。

user_signed_in? && current_user.profile.nil?の制御を必ず最初の条件に持っていかないとuser_signed_in?の制御が先に行われます。そのため、新規作成画面からトップページにリダイレクトした時にプロフィール一覧画面に再度また遷移し、そこでまた認可制御されるという無限ループになり画面が白くなります。

これで実装は完了です!

Punditとの違い

BankenはPunditの影響を強く受けているgemです。gemの英語ページでも下記のように説明されています。

Simple and lightweight authorization library for Rails inspired by Pundit

今回Bankenを使用し、また過去にPunditを使用したので2つのgemの違いがざっくりと見えましたので共有します。

コードの記述方法の違いを見ればわかりやすいかと思います。今回のロジックをPunditで行うと以下のようになります。以下はプロフィール一覧画面への制御です。

Punditの場合

def index
  authorize Profile
end

Bankenの場合

def index
  authorize!
end

違いとしては下記2点です。

Bankenと違う点

  1. authorize!ではなくauthorize(!マークがない)
  2. 引数がない場合クラスを引数にする(Profile)

1に関しては書き方の違いなので大した問題ではないのですが、2は大きな違いだと思います。Bankenの場合引数がなくても引数なしにauthorize!メソッドを使用できますが、Punditの場合、対応するモデルのクラス名を記述しないといけません。

こちらの理由ですが、これはPunditとBankenの紐づいている対象がモデルかコントローラかという違いになります。

Punditはコントローラ、モデル、そしてPolicyクラス(BankenのLoyalityクラスと同じ認可のメソッドが定義されるファイル)が1 : 1 : 1で結びついています。そして、authorizeメソッドは引数のインスタンス変数(@profileなど)からその変数のモデルクラスがあると推測し、そのモデルクラスに対応するPolicyクラスがあると判断します。ここがPunditとBankenの大きな違いです。

Punditの場合の認可のフローは下記のようなものです。まさにコントローラ、モデル、そしてPolicyクラスそれぞれが結びついていることがわかると思います。3つのファイルがあって初めて機能します。

def index # indexアクションを実行
  authorize @profile # 変数名からProfileクラスがあると推測
endclass Profile # Profileクラスを確認
endclass ProfilePolicy < ApplicationPolicy # モデルのクラスに対応するPolicyを確認
  def index? # コントローラで実行されたのはindexアクションなのでindex?メソッドを実行
    user.profile.present?
  end
end

そのため、authorizeメソッドで何も引数がない場合、Profileクラスがあると推測することができません。よって、モデルのクラス名を渡してあげることで、Punditにクラスがあると推測させる必要があるわけです。

def index # indexアクションを実行
  authorize Profile # クラス名からProfileクラスがあると推測
end

逆にBankenだとコントローラのみでしたね。モデルの有無は関係ありません。

class ProfilesController # コントローラ名からProfilesLoyaltyがあると推測
    def index # indexアクションを実行
      authorize! 
    endclass ProfilesLoyalty < ApplicationLoyalty # コントローラに対応するProfilesLoyaltyを確認
  def index? # コントローラで実行されたのはindexアクションなのでindex?メソッドを実行
    user.profile.present?
  end
end

もっと詳しく知りたいという方は下記のドキュメントを参照してください。

The difference between Banken and Pundit (Japanese)

github.com

本記事は以上になります。

参考記事

github.com

github.com

【Vue】moment-js

moment-jsとは

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

この記事では日付フォーマットを変換するプラグインmoment-jsについて紹介します。自分のPF作成で使用したため忘れぬうちにアウトプットさせていただきます。

Moment.js | Home

今回自分がこのプラグインを使用した理由は、railsAPIで渡されるdatetimeの値のフォーマットを変更したいためです。

通常created_atupdated_atなどのdatetimeカラムは保存されると以下のようなフォーマットになるかと思います。

"created_at":"2019-04-21T00:00:00.000Z"

自分のPFでは誕生日と入社日というカラムをdatetimeで追加していたため、これをrailsサーバーからJSON形式で値を受け取ると同様のフォーマットで受け取っていました。これをvue.js側で表示すると、表示される画面は以下のようになります。

[該当コード]

<div>{{ profile.birthday }}</div>

[表示画面] (画質が荒く申し訳ございません。)

https://i.gyazo.com/52d169cae2423134ced35cbd5ed56f61.png

これを解消する方法としては2つあるかなと思いました。

  • バックエンド側でJSONの値を加工してフォーマットを整形する
  • フロント側で受け取ったJSONを加工してフォーマットを整形する

上記2つの違いはバックエンドがフロントエンドかということだけです。JSONの値のフォーマットを変更するという点では共通なので、処理内容は同じになるかと思います。

まずバックエンド側で整形する方法ですが、これは個人的に思ったのですが好ましくないのかなと思いました。理由としては、本来のサーバー側でレンダリングしているフォーマットはサーバー側で保存しているフォーマットと一致しているのが一般的だと思います。(enumは話が別になりますが、、、)。ですのでサーバー側で表示する誕生日のJSONの値だけフォーマットを部分的に変更するという仕様は違和感がありました。また、フロント側で値を加工することが多いので値自体の加工はフロント側で統一した方がいいのかなとも思いました。

ではフロント側で受け取った値を加工するというのはどうでしょうか。これはvuexgettersのように受け取った値を加工するという仕様があるため問題なさそうです。

computed: {
  doneTodosCount () {
    return this.$store.state.todos.filter(todo => todo.done).length
  }
}

ゲッター | Vuex

しかし、javascriptに慣れていないためか処理が冗長になると感じました。上記の例とは違い、プロフィール群の全てのプロフィールの誕生日の値のフォーマットを変更するという処理はかなり文が長くなることが予想されます。もっと簡単に処理できる方法はないのかと悩んでいました。

そこで今回使用するプラグインmoment-jsを使用することにしました。導入するとmoment-jsYYYY年/mm月/dd日というフォーマットに変更することができました。

インストール方法

railsを使用しているのでwebpackerに任せてインストールします。個人的にmoment-jsの他の記事はそのままのvue(vue-clii)での開発のパターンが多くyarnでインストールしている記事は見当たりませんでしたが、問題なく実装できますのでご安心ください。

$ yarn add moment
yarn add v1.22.10
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
warning " > vue-loader@15.9.6" has unmet peer dependency "css-loader@*".
warning " > vue-loader@15.9.6" has unmet peer dependency "webpack@^3.0.0 || ^4.1.0 || ^5.0.0-0".
warning " > webpack-dev-server@3.11.2" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0".
warning "webpack-dev-server > webpack-dev-middleware@3.7.3" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0".
[4/4] 🔨  Building fresh packages...
success Saved lockfile.
success Saved 1 new dependency.
info Direct dependencies
└─ moment@2.29.1
info All dependencies
└─ moment@2.29.1
✨  Done in 4.07s.

次にコンポーネント自体にインポートします。誕生日をフォーマットを変更して表示するページが今のところ1ページのみですので、今回はページのコンポーネントに直接インストールしました。

<script>
import moment from "moment";

export default {
};
</script>

次にexport defaulltの中にfiltersでフォーマットを指定します。

<script>
import moment from "moment";

export default {
    filters: {
      moment: function(date) {
        return moment(date).format("YYYY年/MM月/DD日");
      },
    }
};
</script>

filtersに関しては全く知らなかったのですが、どうやらmustacheなどで値のフォーマットや大文字小文字を加工する時などに使われているようです。公式にも記載がありました。

フィルター - Vue.js

format("YYYY年/MM月/DD日")の部分で好きなフォーマットを指定します。format("YYYY/MM/DD")といった年などを削除しても可能です。

これで準備は完了です。あとは以下のように変換したい日付 | momentmustacheの中に記載します。

[変更前]

<div>誕生日: {{ profile.birthday }}</div>

[変更後]

<div>誕生日: {{ profile.birthday | moment }}</div>

これにより値のフォーマットが意図したものに加工されました。

https://i.gyazo.com/3aa40700275edf95722ca4a0191bd60a.png

問題点

今回はあまりフォーマットの実装自体に時間をかけたくなかったのでmoment-jsで実装しましたが、今後こちらの実装で予想される問題点もあります。

例えば、誕生日カラムだけでなく、記念日というカラムを追加しようとした時に、そのページには2箇所のmomentの記載が必要です。

<div>誕生日: {{ profile.birthday | moment }}</div>
<div>誕生日: {{ profile.memorial | moment }}</div>

さらに増えると3箇所というふうにそのページでmomentを記載しなければならない箇所がどんどん増えていきます。それはかなり冗長に思えますので、それだったらvuexのgettersで一度に加工してしまった方が処理が冗長でも管理が楽なのかもしれません。それについてわかったらこちらの記事に追記したいと思います。

以上、大ちゃんの駆け出し技術ブログでした!

【Vue】VeeValidate

はじめに

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

本日はveeValidateについてのアウトプットです。

これはフォームを入力時にフロント側でバリデーションしてくれる機能ですね。

これを使うと以下のようにリアルタイムでバリデーションのメッセージが表示されます。

https://i.gyazo.com/99e49e885344a47e0ea6641cc550bdbb.gif

今回はこれを紹介しますが、表示している画面が自分のPFの画面のため、コードを公開したくありません。そこで、サンプルコードを使用して説明します。

今回はveeValidateを導入するにあたって既に用意されているコンポーネントを2つ紹介します。基本は公式の通りに導入します。

Getting Started

インストール

まずインストール方法ですが、自分はrailsのアプリでwebpackerを使っていますので、yarnを使用してインストールします。

$ yarn add vee-validate

次にJSファイルを作成し、以下の3つをimportします。

  • ValidationProvider
  • ValidationObserver
  • extend
// app/javascript/plugins/vee-validate.js
import { ValidationProvider, ValidationObserver, extend } from "vee-validate";
import { required, max } from "vee-validate/dist/rules";

export default {
  components: {
    ValidationProvider,
    ValidationObserver,
  },
};

上述しました2つのコンポーネントというのはValidationProvider、ValidationObserverのことです。

※ extendに関しては後で記載します。

これらをメインで使用しているJSファイルでimportするためにexportをしています。

ではメインで使用しているJSファイルでimportします。

import veeValidate from "../plugins/vee-validate"
Vue.mixin(veeValidate)

Vue.mixinについては公式を参照したところ下記の記述があります。

グローバルにミックスインを適用することもできます。使用に注意してください!一度、グローバルにミックスインを適用すると、それはその後に作成する全ての Vue インスタンスに影響します。適切に使用されるとき、これはカスタムオプションに対して処理ロジックを注入するために使用することができます。

ミックスイン - Vue.js

グローバルミックスインという機能で、どこのVueインスタンスでも使用することができるようになります。よってveeValidateをどこのコンポーネントでも使用可能になりました。

(グローバルミックスイン自体にはVueインスタンス全体に影響があるので注意が必要ですが、今回のようなフォームへのバリデーションという複数のページで使用されると予想されるものは問題ないのかなと思っています。)

これでインストールの手順は完了です。

Validation Provider

Validation Provider | VeeValidate

Validation Providerは公式の例を使って説明します。

<template>
  <ValidationProvider rules="required" v-slot="{ errors }">
    <input v-model="value" type="text">
    <span id="error">{{ errors[0] }}</span>
  </ValidationProvider>
</template>

<script>
import { ValidationProvider } from 'vee-validate';

export default {
  components: {
    ValidationProvider
  }
};
</script>

ValidationProviderプラグインとして用意されているコンポーネントですので、コンポーネントとして登録してあります。これはグローバルミックスインを使用しない方法ですね。グローバルミックインをした場合、scriptタグの中にコンポーネントを登録する必要はありません。

[使用例]

<ValidationProvider rules="required" v-slot="{ errors }">
  <input v-model="value" type="text">
  <span id="error">{{ errors[0] }}</span>
</ValidationProvider>

ValidationProviderコンポーネントをフォームに対して挟むようにして設置します。コンポーネントにあるrules="required"ですが、これはrules="検証ルール"と記述することで検証ルールを指定しています。requiredは検証中のフィールドに空の値を入力できないというルールを表しています。

他にもこのルールに関してはたくさんありますが、今回は1番シンプルなrequiredのみを使用しています。他のルールについては以下のページで確認できます。

Available Rules | VeeValidate

そして検証ルールに沿わない、つまりフォームが未入力となっているとv-slot="{ errors }のerrorsの値が{{ errors[0] }}の部分に表示されるという仕組みです。slotに関してはこの間記事で説明したので割愛します。

これによりフォームが未入力となっていると検証メッセージが出ます。

extend

しかし、ValidationProviderデフォルトで用意されているメッセージ以外の独自メッセージも表示したいとします。例えば、自分が今回実装したメッセージのように「~してね」みたいなメッセージはデフォルトでは表示できません。

[再掲]

https://i.gyazo.com/99e49e885344a47e0ea6641cc550bdbb.gif

つまり上記のメッセージは、独自メッセージを作成したために表示できているということです。

そしてそれを可能にするのが先ほど挙げたextendになります。

import { ValidationProvider, ValidationObserver, extend } from "vee-validate";

extendをインポートすることで、カスタムメッセージを行えます。

その方法は同一ファイル内でカスタムメッセージをextendします。

import { ValidationProvider, ValidationObserver, extend } from "vee-validate";

extend("select_required", { // ルールの名前
  ...required, // 追加する検証ルールの種類
  message: "{_field_}を選択してね", // 表示するメッセージ
});

select_requiredはrulesの属性で指定する時に使用するルールの名前になります。rules="select_required"で定義するカスタムメッセージが表示できるということになります。

<ValidationProvider
  v-slot="{ errors }"
  rules="select_required"
>

2行目の...requiredですが、こちらはデフォルトの検証ルールの中でどのルールに該当するかということを示します。今回の場合は未入力時の検証ルールですので、requiredを指定します。例えばこれが「~文字以上入力してね」みたいな文字数の検証ルールの場合、requiredではなくminという検証ルールを指定します。

※ スプレッド構文(...)を使っている理由はわかりませんが、、、

最後に表示するメッセージを指定します。{_field_}の箇所はValidationProviderコンポーネントname属性で指定することで表示できます。例えば、以下のようにname属性に「性別」を指定した場合、「性別を選択してね」という文字がエラーメッセージとして表示されます。

<ValidationProvider
  v-slot="{ errors }"
  name="性別"
  rules="select_required"
>

ValidationObserver

最後にValidationObserverです。これの役割は検証エラーが出ている時にフォームの送信の実行ができないようにします。

ValidationObserverがないと、たとえエラーメッセージが表示されていてもフォームの送信ができてしまいます。

https://i.gyazo.com/9a985cb32ba7cf188020aaa82a17d1b6.gif

エラーメッセージが消えるまでこのボタンを押せないようにするためにValidationObserverが必要です

しかし実は実装は簡単で以下のようにフォームの間をValidationObserverコンポーネントで囲ってあげるだけです。

<ValidationObserver ref="observer" v-slot="{ invalid }">
    <ValidationProvider rules="required" v-slot="{ errors }">
      <input v-model="value" type="text">
      <span id="error">{{ errors[0] }}</span>
        <v-btn :disabled="invalid">入力完了!</v-btn>
    </ValidationProvider>
</ValidationObserver>

ref="observer" v-slot="{ invalid }"は必須です。invalidを挟み込むための箇所は、フォームの送信部分です。

v-btn :disabled="invalid">

これを行うことで、ValidationProviderで検証エラーが出ている場合、disabledが有効となり、ボタンが押せない状態になります。そして、エラーが消えたらdisabledが無効になります。

https://i.gyazo.com/df25859684d6249c511985171354ec37.gif

今回は以上です!

【Vue】axios - Base URLの指定方法

Base URLの必要性

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

本日のショートアウトプットはaxiosのbase URLの指定方法です。

axiosとはvue.jsでAPI通信によるJSON取得時に使われるHTTPクライアントです。

axios を利用した API の使用 - Vue.js

アプリケーション開発においてフロントとバックエンドを分けて開発する場合、フロントから自分のアプリケーションのAPIにアクセスします。axiosはそのサーバーへのAPI通信を実装してくれるツわけです。

例えば自分のアプリで以下のようにAPIを公開しているとします。

Api::V1::ProfilesController < ApiController
    def index
      profiles = Profile.select(:name, :email)
      render json: profiles
    end

サーバーのプロフィールテーブルからnameカラムとemailカラムを全て取り出し、それをJSONで公開しています。

これを取得するためにフロント側ではaxiosを使用します。

// app/javascript/pages/profile/index.vue
data() {
  return {
    profiles: {},
  };
},
created() {
  axios
    .get("http://localhost:3000/api/v1/profiles")
    .then((response) => (this.profiles = response.data))
    .catch((err) => console.log(err.status));
},

get("http://localhost:3000/api/v1/profiles")JSON形式でresponseを受け取り、それをdataで定義しているprofilesに格納しています。これを行うことでサーバー側で管理しているデータをフロント側から取得することができます。

しかし、この実装で問題となってくるのがサーバー側にアクセスしているURLの箇所

get("http://localhost:3000/api/v1/profiles")

開発環境であればlocalhost:3000の箇所は基本的には変わらないかと思います。しかし、開発が終わりProductionに移行する場合、開発環境だけでしか使用できない上記のURLではうまく動作しなくなってしまいます。

ProductionのURL: http://example.com/api/v1/profiles
開発環境のURL: http://localhost:3000/api/v1/profiles

そこでBase URLという名前の通りベースとなるURLを設定することでこの問題を解決することができます。

導入方法

導入方法としては公式のものを参照しています。

axios/axios

どこのディレクトリでもいいのですが、以下のようなファイルを作成します。

// app/javascript/plugins/axios.js
import axios from "axios";

const axiosInstance = axios.create({
  baseURL: "/api/v1",
});

export default axiosInstance;

baseURL: "/api/v1"の部分でベースとなるURLを指定します。Productionでも開発環境でもドメインの箇所は異なりますが/api/v1の箇所は同じです。そこをベースと指定することで、それ以前のドメインの箇所が違っていても問題なくサーバーにアクセスすることができます。

注意点として、かならず/api/v1のようにルート(/)から始めてください。例えば下記のように指定した場合うまくいきません。

const axiosInstance = axios.create({
  baseURL: "api/v1",
});

あとは上記のファイルを読み込んであげるだけです。

import Vue from "vue";
import App from "../app.vue";
import axios from "../plugins/axios";
import router from "../router/index.js";
import store from "../store/index.js";

Vue.config.productionTip = false
Vue.prototype.$axios = axios;

読み込むファイルは基本的にstorerouterを読み込んでいる箇所がいいかと思います。

ちょっと不明な箇所があるとすれば下記の部分です。

Vue.prototype.$axios = axios;

こちらは公式でも説明しています。

こちらの例を引用すると

多くのコンポーネントで使用したい値があるが、どこからでも参照できるグローバルスコープでの定義は避けたい。そのような場合は、プロトタイプに追加することで、Vueインスタンスの中でのみグローバルな値として参照できます。

Vue.prototype.$appName = 'My App'
new Vue({
  beforeCreate: function() {
    console.log(this.$appName) // "My App" 
  }
})

つまり今回の実装では、importされたBase URLを定義したaxiosがVueのインスタンスならどこからでも参照できる値として設定されたということです。

これでBase URLは定義できました。先ほどaxiosを使用していた箇所は以下のようになりました。

created() {
  this.$axios
    .get("/profiles")
    .then((response) => (this.user = response.data))
    .catch((err) => console.log(err.status));
},

変更点としてはまず目的であったURLが短くなっています。

.get("/profiles")

それよりも気になるところはaxiosの呼び出し方が違うというところだと思います。

this.$axios

しかしこれは先ほど例として使ったファイルでも同じ呼び出し方を取っています。

console.log(this.$appName)

プロトタイプに登録された値はthis.$と指定することで使用することができるのです。

今回は以上になります。

【Vue】slot

はじめに

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

今月は主なアウトプットとしてvueを題材にしたものを書きます

大体が短いアウトプットで終わると思うので、いつもみたいな長い記事ではないかなと思います。

基本的な用途

本記事ではslotについて紹介します。

slotはかなり特徴的な書き方をするのかなと個人的に思っております。

slotの機能ですが一言で言うと

「親コンポーネント側で指定した値を挟み込む」

となります!

まずは簡単なコードを見ていきましょう。

最初にコンポーネントの作成ファイルを見ていきます

slotを使用したコードサンプルが以下のようになります。

// slot/js/slot.js
Vue.component("slot-practice", {
  template: `<div>start:   [<slot></slot>]   :finish</div>`,
});

new Vue({
  el: "#app",
});

slot-practiceというコンポーネントを作成しています。

templateの中は極めてシンプルな作りでdivタグの中に今回の題材であるslotタグがあるだけです。「start: [」、「] :finish」はただの文字列です。この後説明する挟み込まれていることをわかりやすくするために書いています。

htmlコードは以下のようになります。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>slot-practice</title>
  </head>
  <body>
    <div id="app">
      <slot-practice></slot-practice> <!-- 注目 -->
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
    <script src="js/slot.js"></script>
  </body>
</html>

コンポーネントを配置しただけとい感じですね。

ここで実際に表示される画面をブラウザ上で確認します。

https://i.gyazo.com/c01adc7b1451e175c233b73e43bd88ae.png

特に何も起こっていませんね。

では冒頭で述べたslotの要約を思い出して実装します。

コンポーネント側で指定した値を挟み込む」

コンポーネントのタグの間で挟み込みたい文字を記述します。どんな文字でもOKです!

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>slot-practice</title>
  </head>
  <body>
    <div id="app">
      <slot-practice>挟まれた!</slot-practice>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
    <script src="js/slot.js"></script>
  </body>
</html>

もう一度ブラウザで確認しましょう。

https://i.gyazo.com/b54288152a36a12f64eff08abf3266f5.png

まさに親コンポーネントで指定した値が挟み込まれたといった感じです。

このように、コンポーネントのタグの間で記述した文字を子コンポーネントのslotタグの箇所に表示することがslotの機能です。

つまり、

<slot-practice>挟まれた!</slot-practice>

<div>start:   [<slot></slot>]   :finish</div><div>start:   [挟まれた!]   :finish</div>

このようにして<slot></slot>の箇所が置換されるのです!

名前付きスロット

少し発展して名前付きスロットという物も紹介しておきます。

挟み込みたい文字が複数あった場合とかに使用できます。

上述したコンポーネントのtemplateの中で複数箇所に挟み込みたい文字があったとします。

例えばheaderタグを用意してみます。

Vue.component("slot-practice", {
  template: `<div>
    <header>
      <slot></slot>
    </header>
    <div>start:   [<slot></slot>]   :finish</div>
  </div>`,
});

new Vue({
  el: "#app",
});

先ほどと同じhtmlファイルのままでブラウザを確認します。

https://i.gyazo.com/99c8f3abb7c1d7848d9f5779eb2f2827.png

2つのslotタグが両方とも同じ文字で挟み込まれました。

ここまでは先ほどのおさらいですね。

ここで、headerタグの中にあるslotと先ほどのdivタグの中のslotには違う文字を入れたいとします。

しかし、この時親コンポーネントからは複数の値を指定することができません。

その場合に使う機能が名前付きスロットです。

JSファイルに名前付きスロットを導入します。

Vue.component("slot-practice", {
  template: `<div>
    <header>
      <slot name="header"></slot>
    </header>
    <div>start:   [<slot name="middle"></slot>]   :finish</div>
  </div>`,
});

new Vue({
  el: "#app",
});

slotタグにname属性を付け加えているといった感じですね。

コンポーネント側では記述方法を少し変えます。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>slot-practice</title>
  </head>
  <body>
    <div id="app">
      <slot-practice>
        <template v-slot:header>
          ヘッダーを挟み込む
        </template>
        <template v-slot:middle>
          ミドルを挟み込む
        </template>
      </slot-practice>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
    <script src="js/slot.js"></script>
  </body>
</html>

slot-practiceコンポーネントの中にtemplateタグを2度使用しています。そして、タグにはv-slot:<コンポーネントのslotのname属性>を指定しています。

<slot-practice>
  <template v-slot:header>
    ヘッダーを挟み込む
  </template>
  <template v-slot:middle>
    ミドルを挟み込む
  </template>
</slot-practice>

これにより、v-slotで指定したname属性を持つslotの場所に挟み込みたい文字が表示されます。

表示される画面は以下のとおりです。

https://i.gyazo.com/37b5d03d7c3fe49f64a20550add09a69.png

以上のようにtemplateタグを同一コンポーネント内で分けて使用し、name属性を明示することで、表示させたいslotタグの箇所に意図した文字を挟み込むことが可能となります!

終わりに

slotはよく使われるとRUNTEQで教えられてたので上手に使えるようにしておきたいですね。

こんな感じで今月は短いアウトプットを多めにしようかなと思いますのでよろしくお願いいたします。

以上、大ちゃんの駆け出し技術ブログでした!