Docker+Rails+Vue.jsを使ったDB操作可能なWEBアプリケーションの作成手順を実例で解説

docker-rails-vue-js-prograshi(プロぐらし)-kv Rails
記事内に広告が含まれていることがあります。
[PR]

Railsはwebpackerを使うことで簡単にVue.jsが使えるようにできます。

便利な使い方として、Vue.jsをただのページとして表示させるだけでなく、Vue.jsを使ってRailsを操作しDBからデータを取得したり、データを追加・変更・削除することもできます。

ここでは、RailsをDB操作のためのバックエンドとして、Vue.jsをWEBブラウザに表示するフロントエンドと使う方法について、実例を用いて解説しています。

  1. Docker上にRails+Vue.jsの環境を構築する方法
  2. テーブルとモデルの作成
    1. マイグレーションファイルとモデル作成
    2. マイグレーションファイルの編集
    3. マイグレーションの実行
    4. モデルにバリデーションを追加する
  3. ActiveAdminの導入
    1. Gemfileの編集とインストール
    2. セットアップとマイグレーション
    3. 既存モデルとActiveAdminを連携する
  4. APIの作成
    1. ApiControllerの作成
    2. ClientsControllerの作成
    3. ルーティングの設定
    4. APIの動作確認(ブラウザに表示)
  5. Vue.jsの導入
    1. WebpackerでVueをインストールする
    2. vue-loaderのバージョンを下げる(コンパイルエラー対応)
    3. Babelの設定を変更する(大量の警告を非表示にする)
    4. hello_vue.jsをmain.jsに変更する
    5. ビューファイルを編集してVue.jsの内容を表示する
  6. Vue.jsにAPIの内容を表示する
    1. axiosのインストール
    2. axiosでAPIのデータを取得する
  7. ルーティングの設定
    1. テンプレートのディレクトリ構造
    2. vue-routerのインストール
    3. ルーティングの設定
  8. トップページの作成
  9. 一覧画面の作成
    1. Clients.vueテンプレートの編集
    2. コントローラの編集
  10. 詳細画面の作成
    1. axiosの設定内容
    2. ブラウザの表示
  11. 一覧ページに詳細ページへのリンクを設置
  12. 新規登録ページの作成
    1. 新規登録ページ作成の流れ
    2. Railsのresourcesにcreateのルーティングを追加
    3. Railsのコントローラにcreateアクションを追加
    4. 新規登録用のVueテンプレートを作成
    5. Vueに新規登録用のルーティングを追加
    6. ブラウザで確認
  13. 編集ページの作成
    1. 編集ページ作成の流れ
    2. 新規登録用のVueテンプレートからform部分を別テンプレートに切り出す
    3. 新規登録用のVueテンプレートの編集
    4. Railsのresourcesにupdateのルーティングを追加
    5. Railsのコントローラにupdateアクションを追加
    6. 編集用のVueテンプレートを作成
    7. Vueに編集用のルーティングを追加
    8. ブラウザで確認
  14. 削除機能の実装
    1. 削除機能実装の流れ
    2. Railsのresourcesにdestroyルーティングを追加
    3. Railsのコントローラにdestroyアクションを追加
    4. Vue.jsで削除確認用のモーダルの作成
    5. Vue.jsの一覧ページにモーダルを追加する
    6. ブラウザで確認
  15. 参考リンク

Docker上にRails+Vue.jsの環境を構築する方法

Docker上にRails+Vue.jsの環境を構築する方法については、下記を参考にしてください。

(参考)Docker上にRails6を作成する手順をわかりやすく解説


なお、コンパイルを早めるために、webpack-dev-serverの利用と、dev-serverの設定変更をしておくことをお勧めします。


テーブルとモデルの作成

マイグレーションファイルとモデル作成

DB上にテーブルを作成するために、テーブルの元となるマイグレーションファイルと、Railsを使ってテーブルを操作するためのモデルを作成します。

# rails g model <モデル名> カラム名1:型 カラム名2:型,,,,,
  • gはgenerateの省略形。rails generate model ~ でも同じ処理になります。
  • モデル名は単数形で指定します。テーブル名は自動的にこの複数形になります。
  • モデル名、カラム名ともに複数の単語を繋げる場合はスネークケースを使います。(アンダースコアで繋げる)
Railsで使える主なカラムの型
データ型内容詳細用途例
integer整数4bite, ±2,147,483,647
bigint長い整数8bite, ±9,223,372,036,854,775,808多くの会員ID、金額
decimal正確な少数(固定長整数型)実数を記録する。10進数。小数点を含む計算
float少数(浮動小数点)丸め込みやわずかな誤差が発生する。2進数。計算が不要な少数
string文字列1 ~ 255文字名前、住所、パスワード
text長い文字列1 ~ 4,294,967,296文字コメント、投稿文
date日付1000-01-01 〜 9999-12-31
datetime日付と時刻1000-01-01 00:00:00.000000 〜 9999-12-31 23:59:59.999999
time時刻-838:59:59 〜 838:59:59
timestampタイムスタンプ‘1970-01-01 00:00:01’ UTC ~ ‘2038-01-19 03:14:07’ UTC
binaryバイナリ文字列2進数(0と1)
boolean真偽値true, false
primary_keyプライマリーキー(主キー)
  • Railsのデフォルトの型はinteger
  • floatの数値計算で誤差が発生する理由:2進数は少数を正確に表現できないため。
  • MySQLのtinyint, smalintなどの指定は、オプションに limit: バイト数 をつける。
  • RailsのPostgreSQLでのみ使える型もあります。

実例

Clientモデルと、clientsテーブルを作成します。

# rails g model client pj_name:string client_name:string status:integer order_date:date price:bigint memo:text
Running via Spring preloader in process 303
      invoke  active_record
      create    db/migrate/20210719082013_create_clients.rb
      create    app/models/client.rb
      invoke    test_unit
      create      test/models/client_test.rb
      create      test/fixtures/clients.yml


これにより、マイグレーションファイル db/migrate/20210719082013_create_clients.rbが生成されました。

マイグレーションファイルの中身は次のようになっています。

class CreateClients < ActiveRecord::Migration[6.1]
  def change
    create_table :clients do |t|
      t.string :pj_name
      t.string :client_name
      t.integer :status
      t.date :order_date
      t.bigint :price
      t.text :memo

      t.timestamps
    end
  end
end


この状態でマイグレーションを実行すると、clientsテーブルを作成し、その中に、(1)pj_name, (2)client_name, (3)status, (4)order_date, (5)price, (6)memo のカラムが生成されます。

マイグレーションファイルの編集

Railsのマイグレーションファイルでは、カラム毎に、デフォルトの値や、nullを許容するかといった設定ができます。(修飾子やオプションと呼びます)

,オプション名: 値 で記述します。複数設定する場合はカンマでつなげます。

オプション内容
defaultデフォルトの値を設定する
nullNULL値を許容するか。デフォルトはtrue(許可)
limit最大サイズをバイトで指定。integer, string, text, binaryに使う
precisiondecimalの全体の桁数(※整数の桁数ではない)
scaledecimalの小数点以下の桁数を指定。デフォルトは0
polymorphicbelongs_toの関連付けで使うtypeカラムを追加
commentカラムにコメントつける

(参考)Rails公式 Active Record マイグレーションのカラム修飾子

実例

class CreateClients < ActiveRecord::Migration[6.1]
  def change
    create_table :clients do |t|
      t.string :pj_name, null: false, defualt: ""
      t.string :client_name, null:false, default: ""
      t.integer :status, null: false, default: 0
      t.date :order_date, null: false
      t.bigint :price, null:false, defualt:0
      t.text :memo, null: true

      t.timestamps
    end
  end
end

マイグレーションの実行

マイグレーションファイルが完成したら、マイグレーションを実行します。

# rails db:migrate

実例

# rails db:migrate
== 20210715105849 CreateClients: migrating ====================================
-- create_table(:clients)
   -> 0.1232s
== 20210715105849 CreateClients: migrated (0.1235s) ===========================


(補足)DBを生成していない場合は、db:create後に行います。

# rails db:create db:migrate


モデルにバリデーションを追加する

Railsでテーブルにデータを格納する際のバリデーション(規則)を設定します。

バリデーションを設定すると、create, save, updateメソッドを実行するときにバリデーションの内容で確認します。

validates :<カラム名>, <ルール名>: <内容>

複数のバリデーションを指定する

カンマでつなげば、複数のバリデーションをまとめて設定することもできます。

validates :<カラム名>, <ルール名1>: <内容1>, <ルール名2>: <内容2>, <ルール名3>: <内容3>,,,

複数のカラムを同時に指定する

同じバリデーションを複数のカラムにまとめて設定することもできます。(カラム名の前の「:」を忘れずに)

validates :<カラム名1>, :<カラム名2>, :<カラム名3>, <ルール名>: <内容>


主なバリデーションルールと使い方

規則実例備考
入力必須presence: true
真偽値inclusion: { in: [true, false] }inclusionは含むことを指定するヘルパです
nilを許容しないexclusion: { in: [nil] }exclusionは除くことを指定するヘルパです
nilと空白を許容しないexclusion: { in: [nil, “”] }
重複を許可しない(一意)uniqueness: true
指定したカラムの中で一意uniqueness: { scope: :year }yearカラムの中で一意
数値のみnumericality: true
整数のみnumericality: { only_integer: true }
最少値length: { minimum: 2 }
最大値length: { maximum: 500 }
最少値〜最大値の範囲length: { in: 6..20 }
長さを指定length: { is: 6 }
アルファベットのみformat: { with: /\A[a-zA-Z]+\z/}正規表現で指定
メールアドレスformat: { with: /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i}

バリデーションのルールのことをバリデーションヘルパーと呼んだりもします。

(参考)Rails公式 Active Record バリデーション

実例

デフォルトではモデルには何も設定されていません。

class Client < ApplicationRecord
end


バリデーションを追加します。

class Client < ApplicationRecord

  enum status: { "契約前": 0, "進行中": 1, "完了": 2 }

  validates :pj_name, :client_name, exclusion: { in: [nil, ""] }, length: { in: 3..80 },  exclusion: { in: [nil, ""] }
  validates :client_name, :client_name, exclusion: { in: [nil, ""] }, length: { in: 3..80 },  exclusion: { in: [nil, ""] }
  validates :status, inclusion: { in: ["契約前", "進行中", "完了"] }, exclusion: { in: [nil, ""] }
  validates :price, numericality: { only_integer: true },  exclusion: { in: [nil, ""] }

end


enum

enumとは、enumerationの略で列挙という意味です。

enum カラム名: { 選択肢 } のように記述すると、指定した選択肢の中からプルダウンで選択できるようになります。

外部から直接データを追加する場合も想定してvalidates :statusで選択肢も限定しておくと安心です。

tips

enumで指定した選択肢を、validateで制限する場合は、数値ではなく指定した値を記載します。

数値を記載すると、テーブルにデータを登録できなくなります。

OK: inclusion: { in: [“契約前”, “進行中”, “完了”] }
NG: inclusion: { in: [0, 1, 2] }

ActiveAdminの導入

Railsでデータの作成、更新、削除などのDB操作を行う管理画面をとても簡単に作成する方法に、ActiveAdminというgemがあります。

デザインがいけてる訳ではありませんが、操作を行うにはとても便利なライブラリです。(何より導入が簡単です)

ユーザーのログイン管理を行うdeviseというgemと一緒に導入することが多いです。ユーザー管理が必要ない場合は、ActiveAdminのみのインストールで問題ありません。

ここでは、ActiveAdminをインストールします(deviseは後からでも追加できます)

Gemfileの編集とインストール

GemfileでActiveAdminを使うことを宣言します。

gem 'activeadmin'
# bundle install


注意

activeadminのgemをbundle install せずに次のステップに進むとエラーが出ます。

Could not find gem ‘activeadmin’ in any of the gem sources listed in your Gemfile.
Run bundle install to install missing gems.

セットアップとマイグレーション

Webpackerを使う場合はオプションに--use_webpackerをつけます。

rails g active_admin:install --skip-users --use_webpacker

必要なファイル一式を自動で生成します。

# rails g active_admin:install --skip-users --use_webpacker
Running via Spring preloader in process 491
      create  config/initializers/active_admin.rb
      create  app/admin
      create  app/admin/dashboard.rb
       route  ActiveAdmin.routes(self)
    generate  active_admin:webpacker
       rails  generate active_admin:webpacker
Running via Spring preloader in process 513
      create  app/javascript/packs/active_admin.js
      create  app/javascript/stylesheets/active_admin.scss
      create  app/javascript/packs/active_admin/print.scss
      create  config/webpack/plugins/jquery.js
      insert  config/webpack/environment.js
      insert  config/webpack/environment.js
         run  yarn add @activeadmin/activeadmin from "."
yarn add v1.22.5
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@2.3.2: The platform "linux" is incompatible with this module.
info "fsevents@2.3.2" is an optional dependency and failed compatibility check. Excluding it from installation.
info fsevents@1.2.13: The platform "linux" is incompatible with this module.
info "fsevents@1.2.13" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
warning " > vue-loader@15.9.2" has unmet peer dependency "css-loader@*".
[4/4] Building fresh packages...
success Saved lockfile.
success Saved 4 new dependencies.
info Direct dependencies
└─ @activeadmin/activeadmin@2.9.0
info All dependencies
├─ @activeadmin/activeadmin@2.9.0
├─ jquery-ui@1.12.1
├─ jquery-ujs@1.2.3
└─ jquery@3.6.0
Done in 10.77s.
      create  db/migrate/20210716065845_create_active_admin_comments.rb

マイグレーションの実行

マイグレーションファイルも生成されるので、マイグレーションを行いテーブルを作成します。

rails db:migraterails db:migrate


既存モデルとActiveAdminを連携する

ActiveAdminにモデルを追加します。

rails generate active_admin:resource <モデル名>


追加したモデルの中でActiveAdminで操作を許可する項目を設定します。
app/admin/モデル名.rb のファイルを開いて、permit_paramsのコメントアウトを外し、許可したいカラム名を記述します。

permit_params :カラム名1, ;カラム名2,,,,

実例

# rails generate active_admin:resource Client
Running via Spring preloader in process 566
      create  app/admin/clients.rb

次に、許可するカラムを指定します。(idなど自動で設定されるカラム以外の許可したいものを記述します)

ActiveAdmin.register Client do

  permit_params :pj_name, :client_name, :status, :order_date, :price, :memo

end


アプリケーションの再起動

この状態で、http://localhost:3000/admin にアクセスした時に、ActiveAdmin.routes(self) が認識されずにエラーが発生したり、No route matches [GET] “/admin” が表示される場合は、Railsを再起動すれば読み込めるようになります。

$ docker stop <Railsコンテナ名> <DBコンテナ名>
$ docker-compose up -d && docker-compose exec web bin/webpack-dev-server

以上でActiveAdminの導入と既存のモデルとの連携は完了です。

http://localhost:3000/admin にアクセスすればActiveAdminの管理画面が表示されます。
先ほど追加したClientsモデルも表示されています。

※CSSが適用されず、レイアウト崩れが発生することがありますが、今後の処理に問題はありません。

▼CSSが適用されない場合

ActiveAdminにスタイルを適用させたい場合は、splitChunksをオフにすれば適用されます。

(参考)【Rails】ActiveAdminにスタイルが適用されないときの対処法|SplitChunksを使うときの注意点


APIの作成

次にAPIを作成して、Clientモデルのデータを取得できるようにします。

後々、APIに追加処理をすることも考え、ここでは、ActionController::APIを継承した、ApiControllerを作成し、これをベースにデータにアクセスするAPIを作成します。

ApiControllerの作成

touch app/controllers/api_controller.rb

RailsでAPIの機能を使う時は、ActionController::APIを継承します。

ActionController::APIを継承した、ApiControllerを作成します。

class ApiController < ActionController::API
end


ClientsControllerの作成

clientsテーブルの情報を取得する、ClientsControllerを作成します。

複数プロジェクトを管理することを想定したディレクトリ構造にします。

apiディレクトリの下に、プロジェクト毎のディレクトリを配置し、その中に関連するモデルを操作するコントローラを作ります。

ここでは、ディレクトリをpj1とし、その中にclients_controller.rbを作成します。

  • ファイルパス: app/controllers/api/pj1/clients_controller.rb

▼ファイルの作成

ビューファイルやscssファイルは不要なので、rails g controller ではなく、直接ファイルを作成します。

mkdir -p app/controllers/api/pj1
touch app/controllers/api/pj1/clients_controller.rb

▼ファイルの中身

class Api::Pj1::ClientsController < ApiController
  before_action :set_client, only: [:show]

  #例外処理
  rescue_from Exception, with: :render_status_500
  rescue_from ActiveRecord::RecordNotFound, with: :render_status_404

  #一覧
  def index
    @clients = Client.all
    render json: @clients
  end

  #詳細
  def show
    render json: @client
  end

  private

    def set_client
      @client = Client.find(params[:id])
    end

    def render_status_404(exception)
      render json: { errors: [exception] }, status: 404
    end

    def render_status_500(exception)
      render json: { errors: [exception] }, status: 500
    end
end

before_action

before_actionとは、アクションを実行する前に行うメソッドを指定できる機能です。

before_action :メソッド名

ここでは、メソッドとして、個々のクライアントのデータを取得するset_clientメソッドを指定しています。

onlyオプションを指定すると、メソッドを実行するアクションを絞りこむことができます。

only: [:show]なので、showアクションを実行する前のみset_clientメソッドを実行します。

あわせて読みたい

before_actonは複数のアクションで共通の処理がある場合にひとまとめにするために使われることがあります。

例えば、show, edit, update, destroyは対象のユーザーをセットする処理を共通して行います。このようなときにbefore_actionを使えば、一つ一つのアクションに処理を記述するせず、処理をまとめることができます。

before_actionの実例や使い方については下記をご参考ください。

【Rails】超便利コマンドscaffoldの使い方を完全理解|before_actionとは?


Api::pj1::ClientsController < ApiController

Api::pj1::ClientsControllerは、Apiという名前空間の中の、pj1という名前空間の中のClientsControllerクラスという意味です。

一般的に名前空間はディレクトリ構造に合わせます。

クラス名 < ApiController の <は継承を意味します。指定したクラスが、ApiControllerクラスの内容を引き継ぎます。


rescue_from

rescue_fromは例外処理を行うRailsの機能です。例外とはエラーのことで、例外処理とはエラーが発生したときに実行する処理のことです。

rescue_from 例外名, with: :メソッド名

キャッチする例外と、例外をキャッチしたときに実行するメソッドを指定します。メソッドは同じコントローラファイルの中にprivateメソッドとして記述するのが一般的です。

rescue_fromは下から順に適用されるので、上位クラスの例外処理は上に記述する必要があります。

ここでは、DB上の存在しない行(レコード)のデータを取得しようとしたときに発生するエラーActiveRecord::RecordNotFoundと、例外クラスの祖先(すべての例外)であるExceptionを指定しています。

実行する処理はprivateメソッドに記述しています。

  private

  (省略)

    def render_status_404(exception)
      render json: { errors: [exception] }, status: 404
    end

    def render_status_500(exception)
      render json: { errors: [exception] }, status: 500
    end

エラーメッセージをexceptionという変数に代入し、ステータスコードを指定(404と500)して、エラーメッセージをJSON形式で出力します。

ステータスコードを指定せずにステータス200の状態でエラーページを表示させるとソフト404というSEO的にNGな処理になってしまいます。

あわせて読みたい

rescue_fromで具体的に何が行われているか?や実際の使い方とブラウザに表示される内容については、下記をご参考ください。

【Rails】rescue_fromとは?例外処理の便利な使い方


render json: @clients

renderはブラウザに返すレスポンスを指定します。jsonオプションを使うと指定した内容をJSON形式でブラウザに表示します。

@clientsはClient.allなので、clientsテーブルのすべてのデータを表示する処理になります。

あわせて読みたい

モデルクラスに対して指定のメソッドを実行すると、対象のテーブルからデータを取得することができます。

メソッドは、all以外にもwhereやfindなど様々用意されています。詳細と実例については下記をご参考ください。

【Rails】モデルを使ってテーブルの一覧表示やデータ追加・更新・変更・削除する方法


ルーティングの設定

Clientsコントローラが作成できたので、対応するルーティングを作成します。

コントローラの名前空間はApi::pj1::なので、ルーティングも同じ構造になるようにnamespaceを指定します。

Rails.application.routes.draw do
  
  ActiveAdmin.routes(self)
  (省略)
  # APIコントローラへのルーティング
  namespace :api, {format: 'json'} do
    namespace :pj1 do
      resources :clients, only: [:index, :show]
    end
  end
end

この処理で、次のルーティングが追加されます。

# rails routes | grep api
                         api_pj1_clients GET    /api/pj1/clients(.:format)
                                       api/pj1/clients#index {:format=>/json/}
                          api_pj1_client GET    /api/pj1/clients/:id(.:format)
                                       api/pj1/clients#show {:format=>/json/}


resourcesとは?

resourcesとは、DB操作に必要なルーティング一式をまとめて生成してくれる便利な指定です。

reousercesの後にコントローラー名を指定します。

resources :コントローラ名

上記のように指定すると、次の8つのルーティングが生成されます。

アクションHTTP動詞パス内容
indexGET/コントローラ名一覧を表示
createPOST/コントローラ名データ追加処理
newGET /コントローラ名/newデータ追加用の画面
editGET/コントローラ名/:id/edit指定したデータを変更する画面
showGET/コントローラ名/:id指定したデータの詳細画面
updatePATCH/コントローラ名/:id指定したデータの更新処理(変更箇所のみ)
updatePUT/コントローラ名/:id指定したデータの更新処理(全体を更新)
destroyDELETE/コントローラ名/:id指定したデータの削除処理

onlyオプションを使ってアクションを指定すれば、指定したアクションへのルーティングのみを生成します。

あわせて読みたい

resourcesはRailsでDB操作をするときには必須の機能といっても過言ではありません。resourcesの詳細や実際の使い方やについては以下をご参考ください。

【Rails】ルーティングのresourcesとは?意味と使い方|resourceとの違いやネスト、名前空間の指定などをわかりやすく解説


namespaceとは?

namespaceとは、Railsのルーティングで名前空間を定義してグループ化するための処理です。基本的にresourcesとセットで使います。

namespace :<名前空間> do
  resources :<コントローラ名>
end
   


APIの動作確認(ブラウザに表示)

APIの動作確認のため、clientsテーブルのすべてのデータがブラウザ上に正しく表示されるか確認します。

まずは、ActiveAdminの管理画面経由でclientsテーブルにデータを追加します。(追加するデータはお好みで)

http://localhost:3000/admin/clients の中の、NewClientから追加処理を行います。

直接アクセスする場合は、http://localhost:3000/admin/clients/new です。


▼データ追加後の例

例として2つデータを追加しました。


追加したデータをAPIコントローラ経由でブラウザ上に表示します。

先ほどのルーティングで追加したURIにアクセスします。

clients一覧ページへのアクセス例

http://localhost:3000/api/pj1/clients

client詳細ページへのアクセス例

http://localhost:3000/api/pj1/clients/7

どちらも正しくJSON形式でデータが表示されていることを確認できました。以上でAPIの設定はOKです。


Vue.jsの導入

次にフロントとしてブラウザにWEBページを表示するためのVue.jsをRailsアプリケーションに追加します。

WebpackerでVueをインストールする

RailsでVue.jsを読み込むために、Webpackerを使ってRailsにVueを追加します。これを追加することで、Vueファイルのコンパイルが可能になります。

また、デフォルトで、hello_vue.jsという「Hello Vue!」をブラウザに表示するテスト用のファイルも生成してくれます。

rails webpacker:install:vue

実例

Dockerコンテナの外から実行する場合は、サービスを指定して、引数でコマンドを渡します。

$ docker-compose run web rails webpacker:install:vue
省略
Webpacker now supports Vue.js 🎉


webpack関連のファイルが更新され、app.vueやhello_vue.jsといったファイルが生成されます。

        modified:   config/webpack/environment.js
        modified:   config/webpacker.yml
        modified:   package.json
        modified:   yarn.lock

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        app/javascript/app.vue
        app/javascript/packs/hello_vue.js
        config/webpack/loaders/


vue-loaderのバージョンを下げる(コンパイルエラー対応)

rails webpacker:install:vueでVueをインストールした状態では、コンパイルエラーが発生します。

これは、vue-loaderのバージョンが新しすぎて、必要なライブラリが入っていないためです。このため、vue-loaderのバージョンを15系まで落とします。

package.jsonファイルの修正

Webpackerはgemではなくyarn(node.js)で管理されているので、package.jsonに記載されている vue-loaderのバージョンを修正します。

"dependencies": {
  (省略)
    "vue-loader": "15.9.2",
  (省略)
  },


インストール

yarnを使って、package.jsonの内容を読み込み必要なライブラリをインストールします。

# yarn install

▼Dockerコンテナの外から実行する場合

docker-compose.ymlファイルのあるディレクトリで以下を実行します。

# docker-compose run <Railsのサービス名> yarn install


実例

$ docker exec -it rails-vue-web bash
root@59229383d91c:/rails-vue# yarn install
yarn install v1.22.5
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@2.3.2: The platform "linux" is incompatible with this module.
info "fsevents@2.3.2" is an optional dependency and failed compatibility check. Excluding it from installation.
info fsevents@1.2.13: The platform "linux" is incompatible with this module.
info "fsevents@1.2.13" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
warning " > vue-loader@15.9.2" has unmet peer dependency "css-loader@*".
[4/4] Building fresh packages...
success Saved lockfile.
Done in 21.29s.

vue-loader@15.9.2″のインストールが完了しました。

エラー内容の詳細についてはこちらをご参考。


Babelの設定を変更する(大量の警告を非表示にする)

デフォルトの状態では、コンパイル時にBabel関連の警告が大量に表示されるため、この表示をオフにします。

(エラーではなく警告なのでコンパイルは実行できるのですが、コンパイルのたびにターミナル(黒画面)が警告で埋まります)

["@babel/plugin-proposal-private-methods", { "loose": true }]をルート直下にあるbabel.config.jsファイルに追記します。

    plugins: [
     (省略),
     ["@babel/plugin-proposal-private-methods", { "loose": true }]
    ].filter(Boolean)

保存すれば完了です。

次回のコンパイル以降は警告が表示されなくなります。

実際の記述例

module.exports = function(api) {
  var validEnv = ['development', 'test', 'production']
  var currentEnv = api.env()
  var isDevelopmentEnv = api.env('development')
  var isProductionEnv = api.env('production')
  var isTestEnv = api.env('test')

  if (!validEnv.includes(currentEnv)) {
    throw new Error(
      'Please specify a valid `NODE_ENV` or ' +
        '`BABEL_ENV` environment variables. Valid values are "development", ' +
        '"test", and "production". Instead, received: ' +
        JSON.stringify(currentEnv) +
        '.'
    )
  }

  return {
    presets: [
      isTestEnv && [
        '@babel/preset-env',
        {
          targets: {
            node: 'current'
          }
        }
      ],
      (isProductionEnv || isDevelopmentEnv) && [
        '@babel/preset-env',
        {
          forceAllTransforms: true,
          useBuiltIns: 'entry',
          corejs: 3,
          modules: false,
          exclude: ['transform-typeof-symbol']
        }
      ]
    ].filter(Boolean),
    plugins: [
      'babel-plugin-macros',
      '@babel/plugin-syntax-dynamic-import',
      isTestEnv && 'babel-plugin-dynamic-import-node',
      '@babel/plugin-transform-destructuring',
      [
        '@babel/plugin-proposal-class-properties',
        {
          loose: true
        }
      ],
      [
        '@babel/plugin-proposal-object-rest-spread',
        {
          useBuiltIns: true
        }
      ],
      [
        '@babel/plugin-transform-runtime',
        {
          helpers: false
        }
      ],
      [
        '@babel/plugin-transform-regenerator',
        {
          async: false
        }
      ],
      ["@babel/plugin-proposal-private-methods", { "loose": true }]
    ].filter(Boolean)
  }
}


エラーの詳細やコンパイル結果についてはこちらをご参照ください。


hello_vue.jsをmain.jsに変更する

Webpackのコンパイル対象となる大元のファイル名をhello_vue.jsからmain.jsに変更します。(変更しなくても使えますが、変更するとより本格的に見えるようになります)

ファイル名の変更

hello_vue.js を main.js に変更します。

$ mv app/javascript/packs/hello_vue.js app/javascript/packs/main.js


home/index.html.erbの修正

app/views/home/index.html.erb にWebpackerを使ってコンパイル後の hello_vue.vue を読み込んでいる記述を変更します。

<%= javascript_pack_tag 'main' %>
<%= stylesheet_pack_tag 'main' %>

manifest.jsonの修正

manifest.jsonはコンパイル時にWebpackerが自動で生成するファイルです。元のファイルとコンパイル後のファイルの対応表になっています。

hello_vue.jsの状態で既にコンパイルされているため、この記述を変更します。

hello_vue を main に一括置換します。

{
  "application.js": "/packs/js/application-e421b4aa3f716bebdab1.js",
  "application.js.map": "/packs/js/application-e421b4aa3f716bebdab1.js.map",
  "entrypoints": {
    "application": {
      "js": [
        "/packs/js/application-e421b4aa3f716bebdab1.js"
      ],
      "js.map": [
        "/packs/js/application-e421b4aa3f716bebdab1.js.map"
      ]
    },
    "main": {
      "js": [
        "/packs/js/main-f10dedba5d1ccdc85afe.js"
      ],
      "js.map": [
        "/packs/js/main-f10dedba5d1ccdc85afe.js.map"
      ]
    }
  },
  "main.js": "/packs/js/main-f10dedba5d1ccdc85afe.js",
  "main.js.map": "/packs/js/main-f10dedba5d1ccdc85afe.js.map",
}

(参考)修正前のファイル

{
  "application.js": "/packs/js/application-e421b4aa3f716bebdab1.js",
  "application.js.map": "/packs/js/application-e421b4aa3f716bebdab1.js.map",
  "entrypoints": {
    "application": {
      "js": [
        "/packs/js/application-e421b4aa3f716bebdab1.js"
      ],
      "js.map": [
        "/packs/js/application-e421b4aa3f716bebdab1.js.map"
      ]
    },
    "hello_vue": {
      "js": [
        "/packs/js/hello_vue-f10dedba5d1ccdc85afe.js"
      ],
      "js.map": [
        "/packs/js/hello_vue-f10dedba5d1ccdc85afe.js.map"
      ]
    }
  },
  "hello_vue.js": "/packs/js/hello_vue-f10dedba5d1ccdc85afe.js",
  "hello_vue.js.map": "/packs/js/hello_vue-f10dedba5d1ccdc85afe.js.map",
}



以上でmain.jsへの置き換えは完了です。保存して、ブラウザをリロードしページが表示されればOKです。


ビューファイルを編集してVue.jsの内容を表示する

ここまでで、Vue.jsファイルをコンパイルする準備が整ったので、実際に、ブラウザにVue.jsの内容を出力します。

出力するファイルは、rails webpacker:install:vueで自動生成されたテスト用の、app.vueを使います。

表示するルーティングには作成済みのHomeコントローラーのindexアクションを使います。app/views/home/index.html.erb を以下のように書き換えます。

<%= javascript_pack_tag 'hello_vue' %>
<%= stylesheet_pack_tag 'hello_vue' %>

保存して、Railsを起動するとコンパイルが走ります。

http://localhost:3000/home/index にアクセスして「Hello Vue!」と表示されればvue.jsファイルの表示に成功です。

tips

Webpackerのデフォルト状態のコンパイルはとても時間がかかります。

一度、ctrl + c で起動中のコンテナを終了し、次のコマンドで再起動すれば、劇的に速くなります。

$ docker-compose up -d && docker-compose exec web bin/webpack-dev-server


詳細は【簡単】Dcoker上のRailsでWebpackerのコンパイルが遅すぎるを解決する方法 をご参考ください。

<%= javascript_pack_tag ‘hello_vue’ %>とは?

javascript_pack_tagはWebpackerでコンパイルしたjsファイルを読み込むためのヘルパーメソッドです。

Webpackerは app/javascript/packs 配下にあるJavaScriptファイルを、コンパイルして public/packs/js 配下に出力します。そのコンパイルしたファイルを読み込んでいます。

javascript_pack_tag '元のファイル名' として使います。元のファイル名は .js を省略して指定します。

<%= stylesheet_pack_tag ‘hello_vue’ %>とは?

stylesheet_pack_tagはWebpackerでコンパイルしたcssファイルを読み込むためのヘルパーメソッドです。

Webpackerは app/assets/stylesheets 配下にあるscssやcssファイルを、コンパイルして public/assets 配下に出力します。そのコンパイルしたファイルを読み込んでいます。

stylesheet_pack_tag '元のファイル名' として使います。元のファイル名は拡張子を省略して指定します。

<%= %>とは?

<%= 処理 %>は、erbの中で使うHTMLタグと一緒に使う記述で、記載した処理結果を出力(print)するためのものです。

<% 処理 %>とした場合は、処理は行いますが、その結果を出力しません。

なお、<%== 処理 %> のようにイコールを2つにすると、中に記述したタグなどをエスケープせずそのまま表示します。<%# %>はコメントアウトに使います。

あわせて読みたい

RailsがWebpakを使ってVueファイルの内容を表示するまでには、ルーティング、コントローラ、ビューファイルなど様々なファイルが関わってきます。

Railsでビューファイルを表示するまでの処理の流れについては下記をご参考ください。


【Rails】Webpackでvueファイルを表示するまでの処理の流れ。どのファイルを辿っているか?


Vue.jsにAPIの内容を表示する

モデルからデータを取得するAPIと、フロント側にページを表示するVue.jsの用意が完了したので、Vue.js経由でAPIにアクセスして、テーブルのデータを表示させます。

axiosのインストール

APIからデータを取得するために、axiosというモジュールを使います。

axiosとは、Node.jsで作成されたモジュールで、非同期にHTTP通信を行うときに使います。インストールはnpm、yarn、CDNリンクのいずれでも可能です。

#npmの場合
npm install axios

#yarnの場合
yarn add axios

#CDNの場合(headerにリンクを追加)
<script src="https://unpkg.com/axios/dist/axios.min.js"><script>

実例

ここでは、yarnを使ってインストールします。

#yarn add axios

yarn add v1.22.5
[1/4] Resolving packages...
  ・
  ・
  ・
info All dependencies
└─ axios@0.21.1
Done in 177.29s.

Doneが表示されれば完了です。


axiosでAPIのデータを取得する

いきなり表示部を成型すると何が行われているかわかりにくいので、まずはaxiosを使ってAPI経由で取得したデータをブラウザに表示します。

app.vueの内容を次のように変更します。

<template>
  <div id="app">
    {{ clients }}
  </div>
</template>

<script>
import axios from 'axios';

export default {
  data(){
    return{
      clients: []
    }
  },
  mounted(){
    axios
      .get('/api/pj1/clients')
      .then(response => (this.clients = response.data))
  }
}
</script>

<style scoped>
</style>

templateタグ

.vueファイルにおけるtemplateタグの中身はブラウザ上に表示するHTML(vueの記述が使えます)になります。

<template>
  <div id="app">
    {{ clients }}
  </div>
</template>

{{ }}はマスタッシュ構文といい、この中で変数名やメソッド名を記述すると、その内容を呼び出すことができます。

{{ clients }}とすることで、dataで定義したclients変数の中身を表示します。

import axios from ‘axios’;

スクリプトタグの直下で、axiosモジュールをaxiosという名前で読み込んでいます。

▼import fromの基本構文

import エクスポート名 from モジュール

(参考)MDN JacaScript import


export default

exportとは、JavaScriptをモジュール化して外から呼び出せるようにする記述です。

exportには名前付きエクスポートと、デフォルト(default)エクスポートがあります。

通常のexportは変数や関数毎に名前をつけて呼び出します(名前付きエクスポート)。これに対して、export defaultとすると、その処理の中身全体を1つのモジュールとして出力することができます。

vue単一コンポーネントの場合、このexport defaultにmountedやmethod、dataなどvueの処理を記述します。

(参考)MDN JavaScript export


data

dataオプションでは、変数を定義します。

  data(){
    return{
      clients: []
    }
  },

clientsという変数を、デフォルトが空の配列として定義しています。

mounted

mountedはビューインスタンスを生成するときに実行されます。ブラウザでビュー全体がレンダリングされる前に読み込まれます。

(参考)Vue.js API mounted

mountedの実行タイミングはmain.js(デフォルトではhello_vue.js)に記述があります。

  const app = new Vue({
    vuetify, //追加
    render: h => h(App)
  }).$mount()
  document.body.appendChild(app.$el)

Vueで各処理が実行されるタイミングについてはVueのライフサイクルをご参考ください。


axios

mountedの中のaxiosで、importしたaxiosを使っています。この処理がAPIからデータを取得する処理です。

    axios
      .メソッド('HTTPアクセスするパス')
      .then(変数名 => (処理))

上記は改行していますが、メソッドチェンでつながった処理になります。

axios.メソッドで指定したURLにHTTP動詞でアクセスして、ページの内容を取得します。.thenで受け取ったデータを変数に格納して、処理の中で使えるようにしています。

    axios
      .get('/api/pj1/clients')
      .then(response => (this.clients = response.data))

ここでは、GETメソッドで相対パス(/api/pj1/clients)を指定しています。

そして取得したページの内容をresponseという変数に代入して、responseの中のdataプロパティの値を、Vue.jsで定義した変数clietnsに代入しています。

あわせて読みたい

axiosにはget以外のHTTPメソッドが使えたり、then以外にもcatchやfinallyといったメソッドが用意されています。

axiosの詳細と実例については下記をご参考ください。


axiosとは何か?Vue.jsでAPIのデータを取得する方法|取得したデータの中身や処理の指定方法(変数名の変更, then, catch, finally)


ブラウザ上での表示

app.vueを開く指定をしているパスにアクセスして、APIの処理結果がVueファイルで正しく表示できるか確認します。

ActiveAdmin経由で登録したデータをVueファイル上に表示することができました。


ルーティングの設定

Vue.jsを使ってSPAでページを遷移するためにvue-routerというモジュールを使用します。

vue-routerを使って、/clientsにアクセスした場合に一覧ページを開き、/clients/:idにアクセスしたときに詳細ページを開くようにします。

テンプレートのディレクトリ構造

一覧ページと詳細ページのテンプレートは、componentsの中にclientsディレクトリを作成し、Clietns.vueとClient.vueを作成します。

また、トップページとして、Top.vueを作成します。

ディレクトリ構造は以下のようになります。

|- javascript
|     |
|     |- packs
|     |    |- main.js
|     |
|     |- app.vue
|     |
|     |- components
|          |- Top.vue
|          |
|          |- clients
|                |- Clients.vue
|                |- Client.vue

以下の手順でルーティングを設定したときにファイルが存在しないとコンパイルエラーになるので、先にファイルだけ生成しておきます(中身は後ほど作成します)

vue-routerのインストール

まずは、vue-routerをインストールします。npmかyarnを使ってインストールします。

▼yarn

yarn add vue-router

▼npm

npm install vue-router
あせて読みたい

vue-routerのインストールにはCDNを使った方法やVue CLIを使う方法もあります。

また、ルーティングの設定方法も、Vueインスタンスを生成するファイルに記述したり、JavaScriptやVueファイルに切り出すこともできます。

詳細については下記をご参考ください。

(参考)Vue.jsのvue-routerを使いこなす方法|vue-routerのインストールと設定


ルーティングの設定

vue-routerを使ったルーティングの設定は、app.vueファイルに記述します。

ここでは大きく、vue-routerの設定と、ルーティングの設定の2つを行っています。

<template>
  <div>
    <router-view></router-view>
  </div>
</template>

<script>
import Vue from 'vue'
import VueRouter from 'vue-router'

import Top from './components/Top'
import Client from './components/clients/Client'
import Clients from './components/clients/Clients'

Vue.use(VueRouter)

const router = new VueRouter({
  routes: [
    {
      path: '/',
      component: Top,
      name: 'top',
    },
    {
      path: '/clients',
      component: Clients,
      name: 'clients',
    },
    {
      path: '/clients/:id(\\d+)',
      component: Client,
      name: 'client',
    }
  ]
})

export default {
  router
}
</script>

<style lang="scss" scoped>
</style>

templateタグの中に記述した<router-view></router-view>で、ルーティングで設定したVueテンプレートを読み込みます。

ルーティングの設定はVueRouterインスタンスの中の、routesオプションの記述になります。

const router = new VueRouter({
  routes: [
    {
      path: '/',
      component: Top,
      name: 'top',
    },
    {
      path: '/clients',
      component: Clients,
      name: 'clients',
    },
    {
      path: '/clients/:id(\\d+)',
      component: Client,
      name: 'client',
    }
  ]
})

pathでURIを指定し、router-viewタグの中で呼び出すテンプレートをcomponentで指定します。nameを使えば、このルーティングに名前をつけることができます。

注意点

プロパティの値を指定するときは、文字列なのか変数なのかを意識する必要があります。存在しない変数を指定すると、エラーが発生しVueが描画されません。

例えば、component: Clientsの値はシングルクオテーションで囲んでいないのに対して、name: 'clients'の値はシングルクオテーションで囲んでいます。

これは、Clientsはimportで変数名を指定してテンプレートを読み込んでいる(文字列でない)のに対し、clientsの方は文字列として指定しているためです。

tips

pathの指定には正規表現を使うことができます。例えば、path: '/clients/:id(\\d+)' としたときの、 (\\d+) は数値のみであることを指定しています。

正規表現を使ったpath指定の例については下記をご参考ください。

(参考)Vue.jsのvue-routerを使いこなす!正規表現でパスやパラメータを指定する方法


トップページの作成

トップページのテンプレートになる、Top.vueは以下のようにします。

<template>
  <div>
      <ul>
          <router-link to="/clients" tag="li">クライアント一覧</router-link>
      </ul>
  </div>
</template>

<script>
export default {

}
</script>

<style lang="scss" scoped>

</style>

router-linkタグはvue-routerの機能の一つで、指定したルーティングへのリンクを生成します。

通常はコンパイル後にaタグに変換されますが、tagオプションを使うと指定したタグとしてコンパイルすることができます(もちろんリンクも機能します)


▼ブラウザの表示

ブラウザに表示される内容は次のようになります。

表示されている、「クライアント一覧」をクリックすると/clientsページに飛びます。

vue-routerを使ったSPAでは、ルートディレクトリに/#/が入ります(#をハッシュと呼びます)。モードをhistoryに変更すれば、このハッシュを外すこともできます

あせて読みたい

router-linkタグでルーティングを指定する方法は、パスやルート名など様々です。指定時に変数を使ったり、変数と文字列を合わせることもできます。

詳細については下記をご参考ください。

(参考)Vue.jsのvue-routerを使いこなす方法|router-linkタグの使い方


一覧画面の作成

/clientsのルーティングで表示する一覧画面のテンプレートClietns.vueを作成します。

すべてのデータを表示するのは詳細画面の役割として、一覧画面では、client_name, pj_name, statusの3つのみを表示するようにします。

Clients.vueテンプレートの編集

Clients.vueの中身は以下のようにします。

<template>
  <div>
    <table>
      <tbody>
        <tr>
          <th>Project Name</th>
          <th>Client Nmae</th>
          <th>Status</th>
        </tr>
        <tr v-for="(client, index) in clients" :key="index">
          <td>{{ client.client_name }}</td>
          <td>{{ client.pj_name }}</td>
          <td>{{ client.status }}</td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  data(){
    return{
      clients: []
    }
  },
  mounted(){
    axios
      .get('/api/pj1/clients')
      .then(response => (this.clients = response.data))
  }
}
</script>

<style scoped>
</style>

Vue.jsではv-forを使えばループさせることができます。

v-forの基本構文は次のようになります。

<開始タグ v-for="(変数名, インデックス用変数名) in ループさせるデータ" :key="固有となる変数">

インデックス番号も抜き出したいときは、第2引数でインデックス番号を格納する変数名を指定してます。

また、同じタグの中で:keyでループごとに固有となる値を指定します。これが無くても使うことはできるのですが、エディタやconsoleにエラーが出ます。

:keyの指定はインデックス番号以外にも、ループさせるデータの中の固有のidなども使えます。

<開始タグ v-for="変数名 in ループさせるデータ" :key="変数名.id">


▼ブラウザの表示

ブラウザに表示される内容は次のようになります。


コントローラの編集

一覧画面で使うデータはclient_name, pj_name, statusの3つのみなので、DBからデータを取得してくるclientsコントローラのindexアクションで取得してくる情報もこの3つに絞ります。

モデルに対してselectメソッドを使用します。

  def index
    @clients = Client.select(:client_name, :pj_name, :status, :id)
    render json: @clients
  end
selectメソッドとは?

selectメソッドは対象のオブジェクトから、指定した要素を取得するものです。

モデルに対して使用すると、指定したカラム名のみのデータを取得することができます。

モデル名.select(:カラム名1, :カラム名2,,,,)


詳細画面の作成

続いて詳細画面を作成します。

大枠の構成は一覧ページ(Clients.vue)と同じですが、変更点は3か所です。

  1. templateの中身(ブラウザに表示する内容)
  2. dataの変数名(単数形にする)
  3. axiosのアクセス先URL(:idが入ったものになる)
<template>
  <div>
      <dl>
          <dt>client_id</dt>
          <dd>{{ client.id }}</dd>
          <dt>client_name</dt>
          <dd>{{ client.client_name }}</dd>
          <dt>pj_name</dt>
          <dd>{{ client.pj_name }}</dd>
          <dt>status</dt>
          <dd>{{ client.status }}</dd>
          <dt>price</dt>
          <dd>{{ client.price }}</dd>
          <dt>order_date</dt>
          <dd>{{ client.order_date }}</dd>
          <dt>memo</dt>
          <dd>{{ client.memo }}</dd>
      </dl>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  data(){
    return{
      client: []
    }
  },
  mounted(){
    axios
      .get(`/api/pj1/clients/${this.$route.params.id}.json`)
      .then(response => (this.client = response.data))
  }
}
</script>



<style lang="scss" scoped>
dl {
  display: flex;
  flex-wrap: wrap;
  padding: 10px;
  dt {
    width: 30%;
    border: 1px solid gray;
    padding: 10px;
  }
  dd {
    width: 50%;
    border: 1px solid gray;
    padding: 10px;
  }
}
</style>

axiosの設定内容

axiosの設定が以下のようになっています。

    axios
      .get(`/api/pj1/clients/${this.$route.params.id}.json`)
      .then(response => (this.client = response.data))

this.$route.params.id

$routeというのはvue-routerにおいて現在のルートの情報が入ったオブジェクトです。この中のparamsプロパティには、URIで指定したパラメータが格納されています。

ここでは、/:id という情報が欲しいので、 $route.params.id として情報を取得しています。(export defaultの中なので、this.をつけるのを忘れずに)

あわせて読みたい

$routeには、パラメータの情報以外にも、現在のパスやクエリ、リダイレクト元など様々な情報が入っています。

プロパティ名を指定すれば欲しい情報を抜き出すことができます。

(参考)便利な$routeの使い方


バッククオートと${}

axiosのURLの指定を、`(バッククオート)で囲んで、中で変数を${変数名}として使っています。

これはテンプレートリテラルというJavaScriptの書式で、文字列と変数を結合することができます。


ブラウザの表示

ブラウザでclients/:id にアクセスしたときの表示は以下のようになります。


一覧ページに詳細ページへのリンクを設置

一覧ページの各データ(tableタグ)の右端に詳細ページへのリンクを設置します。

router-linkタグを使って、詳細ページをルート名前で指定し、パラメータを渡します。

<router-link :to="{ name: 'client', params: { id: client.id }}">
  {{ client.id }}
</router-link>

ルート名はapp.vueのルーティングのnameオプションで指定した値になります。(ここでは、clientです)

パラメータを渡すには、paramsというプロパティの中で、キーと値を指定します。

あせて読みたい

router-linkタグでルーティングを指定する方法は、パスやルート名など様々です。指定時に変数を使ったり、変数と文字列を合わせることもできます。

詳細については下記をご参考ください。

(参考)Vue.jsのvue-routerを使いこなす方法|router-linkタグの使い方


Clients.vue全体のコードを以下のようにします。(ついでにスタイルも調整)

<template>
  <div>
    <table>
      <tbody>
        <tr>
          <th>Project Name</th>
          <th>Client Nmae</th>
          <th>Status</th>
          <th>Details Link</th>
        </tr>
        <tr v-for="(client, index) in clients" :key="index">
          <td>{{ client.client_name }}</td>
          <td>{{ client.pj_name }}</td>
          <td>{{ client.status }}</td>
          <td><router-link :to="{ name: 'client', params: { id: client.id }}">{{ client.id }}</router-link></td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  data(){
    return{
      clients: []
    }
  },
  mounted(){
    axios
      .get('/api/pj1/clients')
      .then(response => (this.clients = response.data))
  }
}
</script>

<style lang="scss" scoped>
table{
  text-align: center;
  th{
    padding: 10px;
    color: #FF9800;
    background: #fff5e5;
  }
  td{
    padding: 10px;
    border: 1px solid lightgray;
  }
}
</style>

ブラウザで/clientsにアクセスすると、表示は次のようになります。

右端のリンクをクリックすると、詳細ページに移動することができます。


新規登録ページの作成

ここまでで、Railsのモデル経由でDBから取得したデータをVue.jsで表示することができました。

続いて、Vue.js経由でDBに新たにデータを登録できるようにします。

新規登録ページ作成の流れ

Rails側にテーブルへの新規登録機能を追加し、Vue.js経由でPOSTメソッドでそのURLにアクセスするようにします。

  1. Railsのresourcesにcreateのルーティングを追加
  2. Railsのコントローラにcreateアクションを追加
  3. 新規登録用のVueテンプレートを作成
  4. Vueに新規登録用のルーティングを追加


Railsのresourcesにcreateのルーティングを追加

まずは、バックエンドとなるRailsにcreateアクション用のルーティングを追加します。

routes.rbのresourcesのonlyオプションに:createを追記します。

Rails.application.routes.draw do
  
  ActiveAdmin.routes(self)
  (省略)
  # APIコントローラへのルーティング
  namespace :api, {format: 'json'} do
    namespace :pj1 do
      resources :clients, only: [:index, :show, :create]
    end
  end
end

以下のようなルーティングが追加されます。

#rails routes
Prefix          Verb    URI                         Pattern Controller#Action
api_pj1_clients POST   /api/pj1/clients(.:format)  api/pj1/clients#create {:format=>/json/}


Railsのコントローラにcreateアクションを追加

createアクションとメソッドの追記

clientsコントローラにcreateアクションを追加します

  #新規登録
  def create
    @client = Client.new(client_params)
    if @client.save
      render json: @client, status: :created
    else
      render json: { errors: @client.errors.full_messages }, status: :unprocessable_entity
    end
  end

clientモデルの.newメソッドでインスタンスを生成します。引数に、client_paramsを指定しています。

ストロングパラメータの設定

これは、セキュリティ強化のためストロングパラメータ用のメソッドです。コントローラの下部にclient_paramsの処理も追記します。

  private

   (省略)    

    #ストロングパラメータの設定
    def client_params
      params.fetch(:client, {}).permit(:client_name, :pj_name, :status, :order_date, :price, :memo)
    end
params.fetch

通常ストロングパラメータの設定には、params.requireを使いますが、ここでは、params.fetchを使っています。

requireメソッドは指定したパラメータが存在しない場合に例外(ActionController::ParameterMissing)を出します。

fetchメソッドを使って、fertch(:プロパティ名, {})とすれば、指定したプロパティが存在しない場合は{}となり、例外が発生しません。

注意点

RailsではPOSTやPATCH、PUTメソッドを実行するときに、ストロングパラメータを使ったパラメータの検証は必須です。

使わないと、ActiveModel::ForbiddenAttributesErrorというエラーが発生します。

status

renderで指定した変数をJSON形式で表示します。その時にステータスをシンボルで指定しています。

シンボルステータスコード内容
:created201リクエストが成功し、リソースの作成が完了
:unprocessable_entity422リクエストは正しいが、中の処理が不適切

clientsコントローラの全貌

clientsコントローラのコードは以下のようになります。

class Api::Pj1::ClientsController < ApiController
  before_action :set_client, only: [:show]

  #例外処理
  rescue_from Exception, with: :render_status_500
  rescue_from ActiveRecord::RecordNotFound, with: :render_status_404


  #一覧
  def index
    # @clients = Client.all
    @clients = Client.select(:client_name, :pj_name, :status, :id)
    render json: @clients
  end

  #詳細
  def show
    render json: @client
  end

  #新規登録
  def create
    @client = Client.new(client_params)
    if @client.save
      render json: @client, status: :created
    else
      render json: { errors: @client.errors.full_messages }, status: :unprocessable_entity
    end
  end

  private

    def set_client
      @client = Client.find(params[:id])
    end

    #ストロングパラメータの設定
    def client_params
      params.fetch(:client, {}).permit(:client_name, :pj_name, :status, :order_date, :price, :memo)
    end

    def render_status_404(exception)
      render json: { errors: [exception] }, status: 404
    end

    def render_status_500(exception)
      render json: { errors: [exception] }, status: 500
    end
end


新規登録用のVueテンプレートを作成

フロントエンド(Vue.js)側で、新規登録用のVueテンプレートを作成します。

componentsのclients配下にClientNew.vueを作成します。

|- javascript
|     |
|     |- packs
|     |    |- main.js
|     |
|     |- app.vue
|     |
|     |- components
|          |- Top.vue
|          |
|          |- clients
|                |- Clients.vue
|                |- Client.vue
|                |- ClientNew.vue

ClientNew.vueの作成

新規登録用のテンプレートとなるClientNew.vueの中身は以下のようにします。

<template>
  <form @submit.prevent="createClient">
    <div class="error-wrapper" v-if="errors.length != 0">
      <ul v-for="error in errors" :key="error">
        <li><font color="red">{{ error }}</font></li>
      </ul>
    </div>
    <div>
      <label>client_name</label>
      <input v-model="client.client_name" type="text">
    </div>
    <div>
      <label>pj_name</label>
      <input v-model="client.pj_name" type="text">
    </div>
    <div>
      <label>status</label>
      <select v-model="client.status">
        <option>契約前</option>
        <option>進行中</option>
        <option>完了</option>
      </select>
    </div>
    <div>
      <label>order_date</label>
      <input v-model="client.order_date" type="date">
    </div>
    <div>
      <label>price</label>
      <input v-model="client.price" type="number" min="0">
    </div>
    <div>
      <label>memo</label>
      <textarea v-model="client.memo"></textarea>
    </div>
    <button type="submit">送信</button>
  </form>
</template>

<script>
import axios from 'axios';

export default {
  data(){
    return {
      client: {
        client_name: '',
        pj_name: '',
        status: '',
        order_date: '',
        price: '',
        memo: '',
      },
      errors: ''
    }
  },
  methods: {
    createClient(){
      console.log(this.client)
      axios
        .post('/api/pj1/clients', this.client)
        .then(response => {
          let res = response.data;
          this.$router.push({ name: 'client', params: { id: res.id } });
        })
        .catch(error => {
          console.error(error);
          if (error.response.data && error.response.data.errors) {
            this.errors = error.response.data.errors;
          }
        });
    }
  }
}
</script>

<style lang="scss" scoped>
div:not(.error-wrapper){
  display: flex;
}
label{
  width: 15%;
}
input, select, textarea{
  margin: 3px;
  width: 30%;
  border: 1px solid gray;
}
button{
  margin: 10px;
  display: inline-block;
  padding: 0.5em 1em;
  text-decoration: none;
  background: #668ad8;
  color: #FFF;
  border-bottom: solid 4px #627295;
  border-radius: 3px;
  &:active{
    -webkit-transform: translateY(4px);
    transform: translateY(4px);
    border-bottom: none;
  }
}
</style>

@submit.prevent=”createClient”

@submit.preventとは、送信ボタンをクリックしたときに、actionで指定したページへの遷移を行わない(prevent)する指定です。

@submit.prevent="メソッド名"とすることで、送信ボタンがクリックされたときに指定したメソッドを実行することができます。

ここでは、送信ボタンをクリックした後に、createClientメソッドを実行します。その中で、axiosを使ってPOSTメソッドでAPI側の新規登録用のURLにアクセスします。


エラー処理

冒頭のulタグとliタグではエラーが存在する場合に、エラーを表示する処理になります。

    <div class="error-wrapper" v-if="errors.length != 0">
      <ul v-for="error in errors" :key="error">
        <li><font color="red">{{ error }}</font></li>
      </ul>
    </div>

v-if="errors.length != 0でエラーが存在する場合に、v-forでエラーを一つづつ表示します。

エラーの情報が入った変数errorsは、export defaultのdataオプションで定義しています。

エラー情報は、axiosのcatchの処理の中で定義しています。

      axios
        .post('/api/pj1/clients', this.client)
        (省略)
        .catch(error => {
          console.error(error);
          if (error.response.data && error.response.data.errors) {
            this.errors = error.response.data.errors;
          }

catchはaxiosでエラーが発生したときに行う処理です。引数で指定したerrorに取得した情報が入ります(名前はなんでもいいです)

エラー情報が存在する場合に、this.errors = error.response.data.errorsで、errors変数に取得したエラーメッセージを代入しています。


フォームの項目(v-model)

フォームの各項目の基本形は次のようになっています。

    <div>
      <label>表示するラベル名</label>
      <input v-model="変数" type="タイプ">
    </div>

v-modelを使うことで、dataプロパティで定義している変数と連動させることができます。(双方向バインディングといいます)

tips

formタグの中でv-modelを使う場合、value, checked, selected属性は不要です。(あっても無視されます。)

v-modelで指定した値が、value, checked, selectedになります。

(参考)Vue.js フォーム入力バインディング


axiosのPOSTメソッド

送信ボタンをクリックすると、createClientイベントを実行し、axiosを使ってメソッドでAPIにアクセスします。

    createClient(){
      console.log(this.client)
      axios
        .post('/api/pj1/clients', this.client)
        .then(response => {
          let res = response.data;
          this.$router.push({ name: 'client', params: { id: res.id } });
        })

axios.post('URL', オブジェクト)とすることで、指定したURLにメソッドでアクセスし、第2引数のオブジェクトを渡します。

渡すオブジェクトは、dataで指定しているclient変数です。デフォルトの中身は空ですが、v-modelでinputタグに入力した値が入ります。

      client: {
        client_name: '',
        pj_name: '',
        status: '',
        order_date: '',
        price: '',
        memo: '',
      },


this.$router.push

axiosのthen(通信成功時)の処理の一番最後に、this.$router.pushがあります。

これは、指定したVueのルーティングにリンクする処理です。(router-linkタグで実行される処理と同じです)

あわせて読みたい

$routerとは、vue-routerのインスタンスです。pushメソッドを使うと、指定したルーティングにリンクすることができます。

使い方や実例については下記をご参考下さい。

(参考)Vue.jsのvue-routerを使いこなす!プログラム的に遷移する方法($router.push)


Vueに新規登録用のルーティングを追加

/clients/newにアクセスしたときに、ClientNew.vueのテンプレートを開くようにapp.vueにルーティングを追加します。

追加するルーティングは以下になります。テンプレートのimportを忘れないようにしてください。

    {
      path: '/clients/new',
      component: ClientNew,
      name: 'clientNew'
    }

app.vue

app.vueの全体像は以下のようになります。

<template>
  <div>
    <router-view></router-view>
  </div>
</template>

<script>
import Vue from 'vue'
import VueRouter from 'vue-router'

import Top from './components/Top'
import Client from './components/clients/Client'
import Clients from './components/clients/Clients'
import ClientNew from './components/clients/ClientNew'

Vue.use(VueRouter)

const router = new VueRouter({
  routes: [
    {
      path: '/',
      component: Top,
      name: 'top',
    },
    {
      path: '/clients',
      component: Clients,
      name: 'clients',
    },
    {
      path: '/clients/:id(\\d+)',
      component: Client,
      name: 'client',
    },
    {
      path: '/clients/new',
      component: ClientNew,
      name: 'clientNew'
    }
  ]
})

export default {
  router
}
</script>

<style lang="scss" scoped>
</style>

以上で新規登録のための準備は完了です。


ブラウザで確認

実際に、ブラウザで確認します。

エラー処理の確認

まずは、エラーが正しく表示されるか確認するため、何も入力していない状態で送信ボタンをクリックします。

エラーメッセージが表示されれば、処理が正しく行われています。

新規登録

続いて新規登録を行います。

↓ 送信ボタンをクリックすると、新規作成したクライアントのページにリンクします。

↓ 一覧ページに移動すると、データが新たに追加されていることが確認できます。


編集ページの作成

既に登録してあるデータを編集できるようにします。

編集ページ作成の流れ

Rails側にテーブルへの編集機能を追加し、Vue.js経由でPATCHメソッドでそのURLにアクセスするようにします。

  1. 新規登録用のVueテンプレートからform部分を別テンプレートに切り出す
  2. 新規登録用のVueテンプレートの編集
  3. Railsのresourcesにupdateのルーティングを追加
  4. Railsのコントローラにupdateアクションを追加
  5. 編集用のVueテンプレートを作成
  6. Vueに編集用のルーティングを追加


新規登録用のVueテンプレートからform部分を別テンプレートに切り出す

編集用画面に表示するフォーム部分は、新規登録用のフォームと共通になります。

このため、新規登録用のフォーム部分をパーシャルとして別のテンプレートに切り出します。

|- javascript
|     |
|     |- packs
|     |    |- main.js
|     |
|     |- app.vue
|     |
|     |- components
|          |- Top.vue
|          |
|          |- clients
|                |- Clients.vue
|                |- Client.vue
|                |- ClientNew.vue
|                |- ClientForm.vue

パーシャルの作成(ClientForm.vue)

新規登録用のClientNew.vueのformタグの部分を、 ClientForm.vueに移行します。

その際、次の4か所を変更する必要があります。

  • @submit.preventの値を、"$emit('submit')"に変更。
  • dataをpropsに変更
<template>
  <form @submit.prevent="$emit('submit')">
    <div class="error-wrapper" v-if="errors.length != 0">
      <ul v-for="error in errors" :key="error">
        <li><font color="red">{{ error }}</font></li>
      </ul>
    </div>
    <div>
      <label>client_name</label>
      <input v-model="client.client_name" type="text">
    </div>
    <div>
      <label>pj_name</label>
      <input v-model="client.pj_name" type="text">
    </div>
    <div>
      <label>status</label>
      <select v-model="client.status">
        <option>契約前</option>
        <option>進行中</option>
        <option>完了</option>
      </select>
    </div>
    <div>
      <label>order_date</label>
      <input v-model="client.order_date" type="date">
    </div>
    <div>
      <label>price</label>
      <input v-model="client.price" type="number" min="0">
    </div>
    <div>
      <label>memo</label>
      <textarea v-model="client.memo"></textarea>
    </div>
    <button type="submit">送信</button>
  </form>
</template>

<script>
export default {
  props:{
    client: {},
    errors: "",
  }

}
</script>

<style lang="scss" scoped>
div:not(.error-wrapper){
  display: flex;
}
label{
  width: 15%;
}
input, select, textarea{
  margin: 3px;
  width: 30%;
  border: 1px solid gray;
}
button{
  margin: 10px;
  display: inline-block;
  padding: 0.5em 1em;
  text-decoration: none;
  background: #668ad8;
  color: #FFF;
  border-bottom: solid 4px #627295;
  border-radius: 3px;
  &:active{
    -webkit-transform: translateY(4px);
    transform: translateY(4px);
    border-bottom: none;
  }
}
</style>

$emitとは?

@submit.preventの値を、メソッド名から、$emit('submit')に変更しています。

$emitは子テンプレートから親のテンプレートにイベントを渡すときに使います。(子→親)

$emit('イベント名', 引数,,,)

親側のテンプレートでは、v-on:イベント名で、子テンプレートのイベント発火を検知します。値にメソッド名を指定して、メソッドを実行することが多いです。

v-on:イベント名="メソッド名"

or

@イベント名="メソッド名"


propsとは?

propsとは親のテンプレートから受け取る変数です。ここでは、clientとerrorsを受け取っています。

親から子にデータを渡すときは、呼び出しているテンプレートタグの中で、v-bindを使います。

<テンプレート名 :渡すデータ名1='変数名1' :渡すデータ名2='変数名2',,,  />

「渡すデータ名」と子テンプレートのpropsで定義した変数名が連動します。

tips

Vue.jsで親子間のテンプレートでデータやイベントの受け渡しをするときは、方法が決まっています。

データ受け渡しの方向内容内容
親→子propsv-bindした親のdataが子のpropsに渡る。
子→親$emit子の$emit('イベント名')で親の@イベント名が発火する
  • 子のテンプレートの中に、propsがあれば、親から渡されるデータであることがわかります。
  • 子のテンプレートの中に、$emitがあれば、親とイベントを同期していることがわかります。
  • 親のテンプレートの中に、見たことのないv-on(@)イベント名があれば、子からイベントを受け取っていることがわかります。


新規登録用のVueテンプレートの編集

ClientForm.vueでformタグの部分を切り出したので、それに合わせてClientNew.vueを編集します。

必要な編集内容は次の3つです。

  1. ClientForm.vueをコンポーネントとして読み込む
  2. data(errorsとclient)を子テンプレートのpropsに渡す
  3. 子テンプレートの$emitから受け取るイベントをセットする
<template>
  <ClientForm :errors='errors' :client="client" @submit="createClient" />
</template>

<script>
import ClientForm from './ClientForm.vue'
import axios from 'axios'

export default {
  components:{
    ClientForm: ClientForm,
  },
  data(){
    return {
      client: {
        client_name: '',
        pj_name: '',
        status: '',
        order_date: '',
        price: '',
        memo: '',
      },
      errors: ''
    }
  },
  methods: {
    createClient(){
      console.log(this.client)
      axios
        .post('/api/pj1/clients', this.client)
        .then(response => {
          let res = response.data;
          this.$router.push({ name: 'client', params: { id: res.id } });
        })
        .catch(error => {
          console.error(error);
          if (error.response.data && error.response.data.errors) {
            this.errors = error.response.data.errors;
          }
        });
    }
  }
}
</script>

<style lang="scss" scoped>
</style>

テンプレートをコンポーネントとして読み込む

ClientForm.vueをコンポーネントとして読み込む手順は次の3つです。

  1. ClientForm.vueをimportする。
  2. export defaultの中で、componentsオプションで、コンポーネント名をつける
  3. templateタグ内で、読み込んだコンポーネントを呼び出す
<script>
import ClientForm from './ClientForm.vue'

export default {
  components:{
    ClientForm: ClientForm,
  },
}

この処理が、(1)ClientForm.vueをimport、(2) export defaultの中で、componentsオプションで、コンポーネント名をつけるになります。

components: {タグ名: テンプレート名}とすることで、読み込んだテンプレートをタグで呼び出すことができます。

 <ClientForm />
tips

components: {タグ名: テンプレート名} でコンポーネントを登録することができます。

このときに、タグ名とテンプレート名が同じになる場合は、タグ名:が省略できます。

以下の2つは同じです。

components:{ ClientForm: ClientForm }

components:{ ClientForm }

(参考)Vue.js コンポーネントのローカル登録


data(errorsとclient)を子テンプレートのpropsに渡す

続いて、呼び出した子テンプレートのpropsにデータを渡します。タグの中でv-bindを使います(省略形は:

<ClientForm :errors='errors' :client="client" />


子テンプレートの$emitから受け取るイベントをセットする

子テンプレートの中で$emitで渡されているイベントを受け取れるようにします。イベントを受け取ったら、createClientメソッドが発火するようにします。

<ClientForm :errors='errors' :client="client" @submit="createClient" />

以上で、新規登録用のテンプレートからformタグの切り出しが完了です。

ブラウザで確認

正しく読み込みができるか、/clients/newにアクセスして確かめます。

先ほど作成した内容と同じになれば、切り出しは完了です。


Railsのresourcesにupdateのルーティングを追加

編集処理を可能にするために、バックエンドとなるRailsにupdateアクション用のルーティングを追加します。

routes.rbのresourcesのonlyオプションに:updateを追記します。

Rails.application.routes.draw do
  
  ActiveAdmin.routes(self)
  (省略)
  # APIコントローラへのルーティング
  namespace :api, {format: 'json'} do
    namespace :pj1 do
      resources :clients, only: [:index, :show, :create, :update]
    end
  end
end

以下のようなルーティングが追加されます。

#rails routes
Prefix          Verb    URI                            Pattern Controller#Action
api_pj1_clients PATCH  /api/pj1/clients/:id(.:format)  api/pj1/clients#update {:format=>/json/}
                PUT    /api/pj1/clients/:id(.:format)  api/pj1/clients#update {:format=>/json/}


Railsのコントローラにupdateアクションを追加

before_actionに:updateを追加

updateメソッドをclientモデルのインスタンスに対して実行します。clientモデルのインスタンス生成は、before_actionでset_clientメソッドを指定しているため、onlyオプションにupdateアクションを追加します。

before_action :set_client, only: [:show, :update]

updateアクションの追記

clientsコントローラにupdateアクションを追加します

  #編集
  def update
    if @client.updat(client_params)
      head :no_content
    else
      render json: { errors: @client.errors.full_messages }, status: :unprocessable_entity
    end
  end

client_paramsメソッドの実行結果を引数としてupdateメソッドを実行します。

head :no_content

head: no_contentとは、ブラウザにステータスコード204を返す処理です。

headとは、ヘッダー(header)のみで本文(body)のないレスポンスをブラウザに送信するためのメソッドです。

:no_contentとは、シンボル形式で、ステータスコード204を表すものです。

ステータスコード204とは、リクエストが成功しかつ、現在のページから遷移する必要がないときに使います。

(参考)

clientsコントローラの全貌

clientsコントローラのコードは以下のようになります。

class Api::Pj1::ClientsController < ApiController
  before_action :set_client, only: [:show, :update]

  #例外処理
  rescue_from Exception, with: :render_status_500
  rescue_from ActiveRecord::RecordNotFound, with: :render_status_404


  #一覧
  def index
    # @clients = Client.all
    @clients = Client.select(:client_name, :pj_name, :status, :id)
    render json: @clients
  end

  #詳細
  def show
    render json: @client
  end

  #新規登録
  def create
    @client = Client.new(client_params)
    if @client.save
      render json: @client, status: :created
    else
      render json: { errors: @client.errors.full_messages }, status: :unprocessable_entity
    end
  end

  #編集
  def update
    if @client.update(client_params)
      head :no_content
    else
      render json: { errors: @client.errors.full_messages }, status: :unprocessable_entity
    end
  end

  private

    def set_client
      @client = Client.find(params[:id])
    end

    #ストロングパラメータの設定
    def client_params
      params.fetch(:client, {}).permit(:client_name, :pj_name, :status, :order_date, :price, :memo)
    end

    def render_status_404(exception)
      render json: { errors: [exception] }, status: 404
    end

    def render_status_500(exception)
      render json: { errors: [exception] }, status: 500
    end
end


編集用のVueテンプレートを作成

フロントエンド(Vue.js)側で、編集用のVueテンプレートを作成します。

componentsのclients配下にClientEdit.vueを作成します。

|- javascript
|     |
|     |- packs
|     |    |- main.js
|     |
|     |- app.vue
|     |
|     |- components
|          |- Top.vue
|          |
|          |- clients
|                |- Clients.vue
|                |- Client.vue
|                |- ClientNew.vue
|                |- ClientForm.vue
|                |- ClientEdit.vue

ClientEdit.vueの作成

編集用のテンプレートとなるClienEdit.vueの中身を以下のようにします。form部分はClientForm.vueを読み込みます。

<template>
<div>
  {{client}}
  <ClientForm :errors="errors" :client="client" @submit="updateClient" />
</div>
</template>

<script>
import ClientForm from './ClientForm.vue';
import axios from 'axios';

export default {
  components: {
    ClientForm
  },
  data(){
    return {
      client: {},
      errors: ''
    }
  },
  mounted(){
    axios
      .get(`/api/pj1/clients/${this.$route.params.id}.json`)
      .then(response => (this.client = response.data))
  },
  methods: {
    updateClient(){
      axios
        .patch(`/api/pj1/clients/${this.client.id}`, {client: this.client})
        .then(response => {
          this.$router.push({ name: 'clients' });
        })
        .catch(error => {
          console.error(error);
          if (error.response.data && error.response.data.errors) {
            this.errors = error.response.data.errors;
          }
        });
    }
  }
}
</script>

<style lang="scss" scoped>
</style>

@submit=”updateClient”

子テンプレート(ClientForm.vue)の$emit('submit')で発火したイベントをv-on:submitでキャッチする処理です。(@はv-on:の省略形)

updateClientメソッドを実行します。

mounted

mountedオプションは、Vueテンプレートの描画タイミングで実行するメソッドを記述します。このページが読み込まれるときに、axiosの処理を実行します。

    axios
      .get(`/api/pj1/clients/${this.$route.params.id}.json`)
      .then(response => (this.client = response.data))

これにより、APIのクライアント詳細ページから対象のデータを取得してきます。

取得してきたデータはtheメソッドで、変数clientに代入されます。

そして、読み込んだテンプレート(ClientForm.vue)に渡され、ブラウザ上にデフォルトとして表示されます。


axios.patch

axios.patchは、指定したURLにPATCHメソッドでアクセスする処理です。

axios.patch( 'URL', オブジェクト )

第2引数で指定したオブジェクトを渡します。

      axios
        .patch(`/api/pj1/clients/${this.client.id}`, {client: this.client})
        .then(response => {
          this.$router.push({ name: 'clients' });

PATCHメソッドの処理が成功したら、this.$router.pushで一覧ページに飛ぶようにしています。

Vueに編集用のルーティングを追加

/clients/edit/:idにアクセスしたときに、ClientEdit.vueのテンプレートを開くようにapp.vueにルーティングを追加します。

追加するルーティングは以下になります。テンプレートのimportを忘れないようにしてください。

    {
      path: '/clients/edit/:id(\\d+)',
      component: ClientEdit,
      name: 'clientEdit',
    }

パラメータの後ろにある、(\\d+)は数値のみであることを指定する正規表現です。

app.vue

app.vueの全体像は以下のようになります。

<template>
  <div>
    <router-view></router-view>
  </div>
</template>

<script>
import Vue from 'vue'
import VueRouter from 'vue-router'

import Top from './components/Top'
import Client from './components/clients/Client'
import Clients from './components/clients/Clients'
import ClientNew from './components/clients/ClientNew'
import ClientEdit from './components/clients/ClientEdit'

Vue.use(VueRouter)

const router = new VueRouter({
  routes: [
    {
      path: '/',
      component: Top,
      name: 'top',
    },
    {
      path: '/clients',
      component: Clients,
      name: 'clients',
    },
    {
      path: '/clients/:id(\\d+)',
      component: Client,
      name: 'client',
    },
    {
      path: '/clients/new',
      component: ClientNew,
      name: 'clientNew'
    },
    {
      path: '/clients/edit/:id(\\d+)',
      component: ClientEdit,
      name: 'clientEdit',
    },
  ]
})

export default {
  router
}
</script>

<style lang="scss" scoped>
</style>

以上で新規登録のための準備は完了です。


ブラウザで確認

実際に、ブラウザで確認します。

/clients/edit/:id にアクセスして、情報が正しく読み込まれるか確認します。

 ↓ データを変更します。

 ↓ 送信ボタンをクリックすると、一覧ページに飛びます。

更新した内容が反映されていることがわかります。

以上で更新処理は完了です。



削除機能の実装

最後に削除機能を実装します。一覧ページに削除ボタンを設置し、クリックすると確認用のモーダルが開くようにします。モーダル内の削除をクリックしたときに削除が実行されるようにします。

削除機能実装の流れ

Rails側にテーブルのレコードの削除機能を追加し、Vue.js経由でDELETEメソッドでそのURLにアクセスするようにします。

  1. Railsのresourcesにdestroyのルーティングを追加
  2. Railsのコントローラにdestroyアクションを追加
  3. Vue.jsで削除確認用のモーダルの作成
  4. Vue.jsの一覧ページにモーダルを追加する

Railsのresourcesにdestroyルーティングを追加

削除処理を可能にするために、バックエンドとなるRailsにdestroyアクション用のルーティングを追加します。

routes.rbのresourcesのonlyオプションに:destroyを追記します。

Rails.application.routes.draw do
  
  ActiveAdmin.routes(self)
  (省略)
  # APIコントローラへのルーティング
  namespace :api, {format: 'json'} do
    namespace :pj1 do
      resources :clients, only: [:index, :show, :create, :update, :destroy]
    end
  end
end

以下のようなルーティングが追加されます。

#rails routes
Prefix          Verb    URI                            Pattern Controller#Action
api_pj1_clients DELETE /api/pj1/clients/:id(.:format)  api/pj1/clients#destroy {:format=>/json/}


Railsのコントローラにdestroyアクションを追加

before_actionに:destroyを追加

destroyメソッドをclientモデルのインスタンスに対して実行します。clientモデルのインスタンス生成は、before_actionでset_clientメソッドを指定しているため、onlyオプションにdestroyアクションを追加します。

before_action :set_client, only: [:show, :update, :destroy]

destroyアクションの追記

clientsコントローラにupdateアクションを追加します

  #削除
  def destroy
    @client.destroy
    head :no_content
  end

生成したClientモデルのインスタンスをdestroyメソッドで削除します。

head :no_content

head: no_contentとは、ブラウザにステータスコード204を返す処理です。

headとは、ヘッダー(header)のみで本文(body)のないレスポンスをブラウザに送信するためのメソッドです。

:no_contentとは、シンボル形式で、ステータスコード204を表すものです。

ステータスコード204とは、リクエストが成功しかつ、現在のページから遷移する必要がないときに使います。

(参考)

clientsコントローラの全貌

clientsコントローラのコードは以下のようになります。

class Api::Pj1::ClientsController < ApiController
  before_action :set_client, only: [:show, :update, :destroy]

  #例外処理
  rescue_from Exception, with: :render_status_500
  rescue_from ActiveRecord::RecordNotFound, with: :render_status_404


  #一覧
  def index
    # @clients = Client.all
    @clients = Client.select(:client_name, :pj_name, :status, :id)
    render json: @clients
  end

  #詳細
  def show
    render json: @client
  end

  #新規登録
  def create
    @client = Client.new(client_params)
    if @client.save
      render json: @client, status: :created
    else
      render json: { errors: @client.errors.full_messages }, status: :unprocessable_entity
    end
  end

  #編集
  def update
    if @client.update(client_params)
      head :no_content
    else
      render json: { errors: @client.errors.full_messages }, status: :unprocessable_entity
    end
  end

  #削除
  def destroy
    @client.destroy
    head :no_content
  end

  private

    def set_client
      @client = Client.find(params[:id])
    end

    #ストロングパラメータの設定
    def client_params
      params.fetch(:client, {}).permit(:client_name, :pj_name, :status, :order_date, :price, :memo)
    end

    def render_status_404(exception)
      render json: { errors: [exception] }, status: 404
    end

    def render_status_500(exception)
      render json: { errors: [exception] }, status: 500
    end
end


Vue.jsで削除確認用のモーダルの作成

フロントエンド(Vue.js)側で、削除確認用のモーダルの作成のVueテンプレートを作成します。

components配下にModal.vueを作成します。

|- javascript
|     |
|     |- packs
|     |    |- main.js
|     |
|     |- app.vue
|     |
|     |- components
|          |- Top.vue
|          |- Modal.vue
|          |
|          |- clients
|                |- Clients.vue
|                |- Client.vue
|                |- ClientNew.vue
|                |- ClientForm.vue
|                |- ClientEdit.vue

Modal.vueの作成

Modal.vueの中身を以下のようにします。

<template>
  <transition name="modal">
    <div class="modal-mask">
      <div class="modal-wrapper">
        <div class="modal-container">

          <div class="modal-body">
            <slot name="body">
            </slot>
          </div>

          <div class="modal-footer">
            <slot name="footer">
              <button class="modal-default-button" @click="$emit('ok')">
                削除
              </button>
              <button class="modal-default-button" @click="$emit('cancel')">
                キャンセル
              </button>
            </slot>
          </div>
        </div>
      </div>
    </div>
  </transition>
</template>

<script>
export default {
}
</script>


<style lang="scss" scoped>
.modal-mask {
  position: fixed;
  z-index: 9998;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, .5);
  display: table;
  transition: opacity .3s ease;
}

.modal-wrapper {
  display: table-cell;
  vertical-align: middle;
}

.modal-container {
  width: 300px;
  margin: 0px auto;
  padding: 20px 30px;
  background-color: #fff;
  border-radius: 2px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, .33);
  transition: all .3s ease;
  font-family: Helvetica, Arial, sans-serif;
}

.modal-header h3 {
  margin-top: 0;
  color: #42b983;
}

.modal-body {
  margin: 20px 0;
}

.modal-footer{
  text-align: right;
}

.modal-default-button{
  margin-left: 5px;
  border: 1px solid gray;
  padding: 2px 10px;
  border-radius: 5px;
}

.modal-enter {
  opacity: 0;
}

.modal-leave-active {
  opacity: 0;
}

.modal-enter .modal-container,
.modal-leave-active .modal-container {
  -webkit-transform: scale(1.1);
  transform: scale(1.1);
}
</style>

transitionタグ

transitionタグとはVue.jsのテンプレートの中で使うことで特別な意味をもつようになるタグです。トランジションやアニメーションをつけるときに使います。

v-ifやv-showが付与されているタグとあわせて使います。

v-ifやv-showにより要素が消える/表示する(遷移する)状況に合わせて以下の6つのクラスが付与されます。

クラス状態
v-enterenterの開始状態。要素が挿入される前に適用され、要素が挿入された 1 フレーム後に削除。
v-enter-activeenterの活性状態。遷移中に適用。
v-enter-toenterの終了状態。要素が挿入された 1 フレーム後に追加され、遷移が終了したら削除。v-enterと入れ替わりで付与される。
v-leaveleaveの開始状態。
v-leave-activeleaveの活性状態。
v-leave-toleaveの終了状態。

leaveはenterと逆向きの遷移中に適用されるクラスです。

https://jp.vuejs.org/v2/guide/transitions.html

transitionタグにname属性をつけると、付与されるクラス名のv-がname属性の値になります。

 <transition name="modal">

上記のように、name属性の値にmodalを指定すると以下のようになります。

クラス状態
modal-enterenterの開始状態
modal-enter-activeenterの活性状態
modal-enter-toenterの終了状態
modal-leaveleaveの開始状態
modal-leave-activeleaveの活性状態
modal-leave-toleaveの終了状態

slotタグ

通常、テンプレートは使い回します。そのときに、テンプレートの中の要素を呼び出す親テンプレートに内容を変更したいときがあります。

その時は、slotタグとslot属性を使うことで、親で指定した要素を、子テンプレートに渡すことができます。

子テンプレートにslotタグを設置

親からのデータを受け取る場所として、slotタグを設置します。slotタグは複数設置することもあるため、name属性でslotタグに名前を付けます。

<slot name="スロット名">デフォルトのテキスト</slot>

親からデータが渡されない場合は、ここで指定している内容が表示されます。

親テンプレートから子のslotにデータを渡す

子のslotタグにデータを渡すには、渡したいタグの属性にslot="スロット名"をつけるだけです。

slot属性をつけたタグが丸ごと子テンプレートのslotタグと置き換わります。

<タグ slot="スロット名">内容</タグ>
あわせて読みたい

Vue.jsでモーダルを作成する方法や、処理の中で使われている機能の詳細については下記をご参考ください。

Vue.jsでモーダルを作成する方法をわかりやすく解説


Vue.jsの一覧ページにモーダルを追加する

作成したModal.vueを一覧ページ(Clients.vue)の中でコンポーネントとして登録して呼び出します。

  1. componentsにModal.vueを登録
  2. テーブルのthにDeleteを追加
  3. テーブルの要素にモーダルを開くボタンを追加
  4. モーダルの削除ボタンがクリックされたときの削除処理を追加
<template>
  <div>
    <table>
      <tbody>
        <tr>
          <th>Project Name</th>
          <th>Client Nmae</th>
          <th>Status</th>
          <th>Details Link</th>
          <th>Delete</th>
        </tr>
        <tr v-for="(client, index) in clients" :key="index">
          <td>{{ client.client_name }}</td>
          <td>{{ client.pj_name }}</td>
          <td>{{ client.status }}</td>
          <td><router-link :to="{ name: 'client', params: { id: client.id }}">{{ client.id }}</router-link></td>
          <td>
            <button @click="deleteTarget = client.id; showModal = true">Delete</button>
          </td>
        </tr>
      </tbody>
    </table>
    <Modal v-if="showModal" @cancel="showModal = false" @ok="deleteClient(); showModal = false;">
      <div slot="body">本当に削除しますか?</div>
    </Modal>
  </div>
</template>

<script>
import Modal from '../Modal.vue'
import axios from 'axios';

export default {
  components:{
    Modal
  },
  data(){
    return{
      clients: [],
      showModal: false,
      deleteTarget: -1,
      errors: '',
    }
  },
  mounted(){
    this.getClients();
  },
  methods:{
    getClients(){
      axios
      .get('/api/pj1/clients')
      .then(response => (this.clients = response.data))
    },
    deleteClient(){
      if (this.deleteTarget <= 0) {
        console.warn('deleteTarget should be grater than zero.');
        return;
      }

      axios
        .delete(`/api/pj1/clients/${this.deleteTarget}`)
        .then(response => {
          this.deleteTarget = -1;
          this.getClients();
        })
        .catch(error => {
          console.error(error);
          if (error.response.data && error.response.data.errors) {
            this.errors = error.response.data.errors;
          }
        });
    }
  }
}
</script>

<style lang="scss" scoped>
table{
  text-align: center;
  th{
    padding: 10px;
    color: #FF9800;
    background: #fff5e5;
  }
  td{
    padding: 10px;
    border: 1px solid lightgray;
  }
}
</style>

componentsにModal.vueを登録

Vue.jsテンプレートをcomponentsとして登録します。

import Modal from '../Modal.vue'

export default {
  components:{
    Modal
  },
  (省略)
}

components:{ Modal }components:{ Modal:Modal }の省略形です。これで、templateタグの中で、ModalタグでModal.vueを呼び出すことができます。

    <Modal v-if="showModal" @cancel="showModal = false" @ok="deleteClient(); showModal = false;">
      <div slot="body">本当に削除しますか?</div>
    </Modal>


テーブルのthにDeleteを追加

削除ボタンを設置するためのカラムをtableに追加します。

<th>Delete</th>

(省略)

<td></td>

テーブルの要素にモーダルを開くボタンを追加

上記のtdタグの中に、モーダルを開くためのボタンを追加します。

 <button @click="deleteTarget = client.id; showModal = true">Delete</button>

@clickはクリックイベントです。値には、メソッドか式をいれることができます。

ここでは2つの式を指定しています。deleteTarget = client.id; showModal = true

変数deleteTargetは選択したデータのid番号を格納し、axiosのDELETEメソッドで使用します。

変数showModalの値がtrueになると、Modalタグの中のv-if="showModal"と連動して、Modalが表示されます。


モーダルの削除ボタンがクリックされたときの削除処理を追加

export defaultのmethodsオプションの中に、モーダルの削除ボタンがクリックされたときの、削除処理を追加します。

メソッド名はdeleteClientです。

    deleteClient(){
      if (this.deleteTarget <= 0) {
        console.warn('deleteTarget should be grater than zero.');
        return;
      }

      axios
        .delete(`/api/pj1/clients/${this.deleteTarget}`)
        .then(response => {
          this.deleteTarget = -1;
          this.getClients();
        })
        .catch(error => {
          console.error(error);
          if (error.response.data && error.response.data.errors) {
            this.errors = error.response.data.errors;
          }
        });
    }

最初にif文で誤作動防止処理を加えています。dataオプションで定義したdeleteTargetのデフォルト値は-1です。これが、マイナスの状態のままのときにconsoleにエラーを表示します。

console.warnを使うことで表示する内容の冒頭に警告マークをつけることができます。

deleteTargetで対象の数値が渡されたときは、axiosで経由でRailsのページにDELETEメソッドでアクセスします。

削除に成功したら、deleteTargetを再度-1に戻しておきます。最後に、削除結果を現在のブラウザに反映するために、getClientsメソッドを実行します。

以上で、削除処理は完了です。

あわせて読みたい

コンソールにログを出力する方法は、console.log以外にも様々あります。使いこなせるとよりデバッグが便利になります。詳細は下記をご参考ください。

console.log以外の便利メソッド一覧|ログレベルとは何か?


ブラウザで確認

実際に、ブラウザで確認します。

/clients にアクセスして、モーダルが正しく表示されるか確認します。

まずはキャンセル操作です。Deleteをクリックするとモーダルが表示され、キャンセルボタンをクリックするとフワっと閉じます。


続いて削除処理です。

モーダルを開いて「削除」ボタンをクリックすることで、対象のデータを削除することができました。


以上で、Railsをバックエンド、Vue.jsをフロントエンドとしたDB操作可能なアプリケーションの作成は完成です。

参考リンク

この記事の内容は、QiitaのRuby on Rails, Vue.js で始めるモダン WEB アプリケーション入門の内容を参考に、補足と実例加えながら解説をしたものです。

とても価値の高い情報を公開してくれたtatsurou313さんに感謝します。


Rails + Vue.jsの環境で、オシャレなレイアウトがサクサク組めるVuetifyを使う場合は下記をご参考ください。

Docker上に構築したRails6でVue.jsを表示する方法(エラー対処法&Vuetifyの使い方)

タイトルとURLをコピーしました