ホーム > カテゴリ > Ruby・Ruby on Rails >

Rails + Vue.js + AjaxでCRUDのサンプルプロジェクト [Hello World]

Vue.js初心者が公式サイトで基礎を学んだ後に作るVue.js + AjaxによるCRUD(作成/読み込み/更新/削除)のサンプルプロジェクトです。

Twitterのように「つぶやき」を投稿可能で編集/削除もできます。

DEMO

https://www.petitmonte.com/rails-demo/vue_crud

ソース一式

https://github.com/TakeshiOkamoto/mpp_vue_crud

※学習用の為、ライセンスはパブリックドメイン

目次

1. プロジェクトの作成
2. 各パッケージをインストール
3. Bootstrapの導入
4. config/application.rb
5. データベース設定
6. モデルの作成
7. db/seeds.rb
8. マイグレーション
9. コントローラー/ビューの作成
10. config/routes.rb
11. 各コード(ビュー/コントローラー/モデル/JS/VUE)

1. プロジェクトの作成

これ以下はソース一式からではなく、一からプロジェクトを作成する方の為の解説です。※各コードの解説もあります。

新規プロジェクトに「vue」を追加してプロジェクトを作成する。

cd ~/
mkdir myapp
cd myapp
rails new . --webpack=vue --skip-turbolinks --skip-action-mailer --skip-action-mailbox --skip-active-storage --skip-test -d mysql

コマンドの実行後、app/javascript/app.vueのファイルが作成されるので「vue_curd_component.vue」の名称に変更する。app/javascript/packs/hello_vue.jsは「vue_crud.js」とします。

2. 各パッケージをインストール

次のyarnコマンドを実行します。

// IEでVue.jsを動作させる
yarn add babel-polyfill
// 日付、時刻の操作(日本語対応)
yarn add date-fns
// FetchをIEに対応させる
yarn add fetch-polyfill
// FormDataをIEに対応させる
yarn add formdata-polyfill

3. Bootstrapの導入

// Gemfileに以下を追加してbundleする
gem 'bootstrap'
 
// 手動で削除
app/assets/stylesheets/application.css
// 手動で作成
app/assets/stylesheets/application.scss
 
application.scssに次のコードをコピペする
/*
 * This is a manifest file that'll be compiled into application.css, which will include all the files
 * listed below.
 *
 * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
 * vendor/assets/stylesheets directory can be referenced here using a relative path.
 *
 * You're free to add application-wide styles to this file and they'll appear at the bottom of the
 * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
 * files in this directory. Styles in this file should be added after the last require_* statement.
 * It is generally better to create a new file per style scope.
 *
 *= require_tree .
 *= require_self
 */
 
@import "bootstrap"

4. config/application.rb

タイムゾーンの設定。

class Application < Rails::Application

  ・・・省略 ・・・
  
  config.time_zone = 'Asia/Tokyo'

  ・・・省略 ・・・

end

5. データベース設定

データベースの設定ユーザー/データベースの生成を参照。

6. モデルの作成

// メイン用
bin/rails g model vue_crud_data name:string comment:text
// バックアップ用
bin/rails g model vue_crud_data_bk name:string comment:text

7. db/seeds.rb

テーブルの初期データの設定です。

# This file should contain all the record creation needed to seed the database with its default values.
# The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup).
#
# Examples:
#
#   movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }])
#   Character.create(name: 'Luke', movie: movies.first)

VueCrudDatum.create(id:1, name: 'プチモンテ', comment: 'Ubuntu16.04にLazarusをインストールする [RADによる開発環境]',
                              created_at: '2020-02-26 13:37:12', updated_at: '2020-02-26 13:37:12')  
VueCrudDatum.create(id:2, name: 'プチモンテ', comment: '青色申告決算書 & 仕訳帳システム',
                              created_at: '2020-02-27 20:00:12', updated_at: '2020-02-27 20:00:12')  
VueCrudDatum.create(id:3, name: 'プチモンテ', comment: 'Rails6プロジェクトの各種初期設定',
                              created_at: '2020-02-28 05:30:12', updated_at: '2020-02-28 05:30:12')
VueCrudDatum.create(id:4, name: 'プチモンテ', comment: 'NginxにRoundcubeのWebメールシステムを導入する [CentOS]',
                              created_at: '2020-03-01 12:45:56', updated_at: '2020-03-01 12:45:56')                              
VueCrudDatum.create(id:5, name: 'プチモンテ', comment: 'ネットワークカメラを用いた顔認識及び人物特定システムの構築 [防犯カメラの自作]',
                              created_at: '2020-03-02 18:22:08', updated_at: '2020-03-02 18:22:08')   
VueCrudDatum.create(id:6, name: 'プチモンテ', comment: 'Android App Bundle(*.aab)でゲームをアップロード [Unity]',
                              created_at: '2020-03-03 22:28:00', updated_at: '2020-03-03 22:28:00')   
VueCrudDatum.create(id:7, name: 'プチモンテ', comment: '収縮と膨張によるノイズ除去のサンプルコード(2値画像用)',
                              created_at: '2020-03-03 23:30:11', updated_at: '2020-03-03 23:30:11')   
VueCrudDatum.create(id:8, name: 'プチモンテ', comment: 'JavaScriptでC/C++コードを実行してネイティブアプリのように高速にする [WebAssembly]',
                              created_at: '2020-03-03 23:47:52', updated_at: '2020-03-03 23:47:52')  
                              
VueCrudDataBk.create(id:1, name: 'プチモンテ', comment: 'Ubuntu16.04にLazarusをインストールする [RADによる開発環境]',
                              created_at: '2020-02-26 13:37:12', updated_at: '2020-02-26 13:37:12')  
VueCrudDataBk.create(id:2, name: 'プチモンテ', comment: '青色申告決算書 & 仕訳帳システム',
                              created_at: '2020-02-27 20:00:12', updated_at: '2020-02-27 20:00:12')  
VueCrudDataBk.create(id:3, name: 'プチモンテ', comment: 'Rails6プロジェクトの各種初期設定',
                              created_at: '2020-02-28 05:30:12', updated_at: '2020-02-28 05:30:12')
VueCrudDataBk.create(id:4, name: 'プチモンテ', comment: 'NginxにRoundcubeのWebメールシステムを導入する [CentOS]',
                              created_at: '2020-03-01 12:45:56', updated_at: '2020-03-01 12:45:56')                              
VueCrudDataBk.create(id:5, name: 'プチモンテ', comment: 'ネットワークカメラを用いた顔認識及び人物特定システムの構築 [防犯カメラの自作]',
                              created_at: '2020-03-02 18:22:08', updated_at: '2020-03-02 18:22:08')   
VueCrudDataBk.create(id:6, name: 'プチモンテ', comment: 'Android App Bundle(*.aab)でゲームをアップロード [Unity]',
                              created_at: '2020-03-03 22:28:00', updated_at: '2020-03-03 22:28:00')   
VueCrudDataBk.create(id:7, name: 'プチモンテ', comment: '収縮と膨張によるノイズ除去のサンプルコード(2値画像用)',
                              created_at: '2020-03-03 23:30:11', updated_at: '2020-03-03 23:30:11')   
VueCrudDataBk.create(id:8, name: 'プチモンテ', comment: 'JavaScriptでC/C++コードを実行してネイティブアプリのように高速にする [WebAssembly]',
                              created_at: '2020-03-03 23:47:52', updated_at: '2020-03-03 23:47:52')

8. マイグレーション

// 各テーブルの作成
bin/rails db:migrate
// 各テーブルの初期データの作成
bin/rails db:seed

9. コントローラー/ビューの作成

SPA(Single Page Application)なので1つだけです。

bin/rails g controller vue_crud_data index

10. config/routes.rb

この設定はAjaxから呼び出すAPIのURLともなります。

Rails.application.routes.draw do
  
  # ルート
  root to: 'vue_crud_data#index'

  get    'vue_crud_data/index'  
  get    'vue_crud_data/new',  to: 'vue_crud_data#new',     as: 'new_vue_crud_data'
  post   'vue_crud_data',      to: 'vue_crud_data#create'
  put    'vue_crud_data/:id',  to: 'vue_crud_data#update'
  delete 'vue_crud_data/:id',  to: 'vue_crud_data#destroy'
end

11. 各コード(ビュー/コントローラー/モデル/JS/VUE)

11.1 ビュー

[app/views/layouts/application.html.erb]

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <title><%= "Rails + Vue.js + AjaxでCRUDのサンプルプロジェクト" %></title>
    <meta name="robots" content="noindex, nofollow" />
    <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1" />    
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= stylesheet_link_tag 'application', media: 'all' %>
    <%= javascript_pack_tag 'application' %>
            
    <%# vue_crud.jsの読み込み %>
    <%= javascript_pack_tag 'vue_crud' %>   
  </head>
  <body>
    <nav class="navbar navbar-expand-md navbar-light bg-primary">
      <div class="navbar-brand text-white"><%= "Rails + Vue.js + AjaxでCRUDのサンプルプロジェクト" %></div>
      <ul class="navbar-nav ml-auto">
        <li class="nav-item"><%= link_to '全て初期化',  new_vue_crud_data_path, class: 'nav-link',style: "color:#fff;" %></li> 
      </ul>       
    </nav>   
    <div class="container">
       <%= yield %>       
    </div>    
    <br>       
    <nav class="container bg-primary p-2 text-center">
      <div class="text-center text-white">
         Vue "CRUD" Sample
      </div>  
      <div class="text-center text-white">       
        Takeshi Okamoto wrote the code.
      </div>  
      <p></p>      
    </nav>    
  </body>
</html>

[app/views/vue_crud_data/index.html.erb]

<div id="root"></div>

11.2 コントローラー

[app/controllers/vue_crud_data_controller.rb]

class VueCrudDataController < ApplicationController
  before_action :set_datum, only: [:update, :destroy]
  
  # 英語のdataは複数形、datumは単数形
  
  def index
    @data = VueCrudDatum.all.order(updated_at: "DESC") 
    
    respond_to do |format|

        # HTML用
        format.html
        
        # JSON用
        format.json { render json: @data}
        
        # JSONは次のような形式となる
        #
        #  [
        #    {"id":1,"name":"プチモンテ"}
        #    {"id":2,"name":"プチラボ"}
        #    {"id":3,"name":"@ゲーム"}        
        #  ]        
    end
  end
  
  # 全て初期化
  def new
    
    ActiveRecord::Base.transaction do 
      # 高速削除
      ActiveRecord::Base.connection.execute("TRUNCATE TABLE vue_crud_data;")
      # 高速挿入
      ActiveRecord::Base.connection.execute("INSERT INTO vue_crud_data SELECT * FROM vue_crud_data_bks;")
    end  
  
    redirect_to root_path
  end
    
  def create
    @datum = VueCrudDatum.new(datum_params)

    respond_to do |format|
      if @datum.save
        format.json { render json: {registration: "Ajaxによるデータの登録が成功しました。", 
                                    id:  @datum.id, name:  @datum.name, comment:  @datum.comment, updated_at:  @datum.updated_at} }
      else
        format.json { render json: {registration: "Ajaxによるデータの登録が失敗しました。", 
                                    id:  "error"} }
      end
    end
  end
  
  def update
    respond_to do |format|
      if @datum.update(datum_params)
        format.json { render json: {registration: "Ajaxによるデータの更新が成功しました。"} }
      else
        format.json { render json: {registration: "Ajaxによるデータの更新が失敗しました。"} }
      end
    end
  end  
  
  def destroy
    respond_to do |format|
      if @datum.destroy
        format.json { render json: {registration: "Ajaxによるデータの削除が成功しました。"} }
      else
        format.json { render json: {registration: "Ajaxによるデータの削除が失敗しました。"} }
      end
    end
  end
    
  private
  
    def set_datum
      @datum = VueCrudDatum.find(params[:id])
    end
      
    def datum_params
      # 送信側のJSONの形式を確認する事!
      params.require(:datum).permit(:name, :comment)
    end
end

11.3 モデル

[app/models/vue_crud_datum.rb]

class VueCrudDatum < ApplicationRecord
  validates :name, length: { maximum: 20 }, presence: true
  validates :comment, length: { maximum: 140 },  presence: true
end

11.4 JS

[app/javascript/packs/vue_crud.js]

// IEでVue.jsを動作させる
import "babel-polyfill";
// Vue.js
import Vue from 'vue'
// Vueコンポーネント
import VueCurdComponent from '../vue_curd_component.vue'

document.addEventListener('DOMContentLoaded', () => {
  const app = new Vue({
    render: h => h(VueCurdComponent)
  }).$mount()
  document.getElementById('root').appendChild(app.$el)
})

11.5 VUE

[app/javascript/vue_curd_component.vue]

RailsのCSRFトークンにも対応しています。未対応だとAjaxからRailsのAPIを呼び出すと「422 (Unprocessable Entity)」のエラーが表示されて動作しません。

<template>
<div>
  <p />
  <div class="fixed-bottom bg-dark text-white" v-bind:style="{opacity: 0.55}">
    <span>&nbsp;</span>
    <span>{{status}}</span>
  </div>
  <h3>投稿</h3>
  <p />
  <form v-on:submit.prevent="handleInsert">
    <input type="text" class="form-control" placeholder="名前"  v-model="name" />
    <textarea class="form-control" rows="5" placeholder="コメントを入力します。" v-model="comment" />
    <input type="submit" value="登録" class="btn btn-primary" />
  </form>        
  <p />
  <h3>一覧</h3>
  <p />       
  <div class="card-columns">
    <div v-for="(item, index) in items">    
    
      <!-- 表示モード -->
      <template v-if="!mode[index]">
        <div v-bind:key="item.id" class="card"> 
          <div class="card-header">
            {{item.name}} <br />{{formatConversion(item.updated_at)}}
          </div>
          <div class="card-body">
            {{item.comment}}
            <br />
            <br />
            <form>
              <div v-bind:style="{textAlign: 'right'}"> 
                <input type="submit" value="編集" class="btn btn-primary" v-on:click.prevent="$root.$set(mode,index,!mode[index])" />&nbsp;
                <input type="submit" value="削除" class="btn btn-danger" v-on:click.prevent="handleDelete(index, item.id, $event)" />&nbsp;&nbsp;
              </div> 
            </form>
          </div>    
        </div>
      </template>  
      
      <!-- 編集モード -->
      <template v-else>
        <div v-bind:key="item.id" class="card">  
          <form v-on:submit.prevent="handleUpdate(index, item.id, $event)">
            <div class="card-header">
              <input type="text" v-bind:value="item.name" name="txt_name" class="form-control" />
            </div>
            <div class="card-body">
              <textarea v-bind:value="item.comment" name="txt_comment" class="form-control" rows="5" />                      
            </div>
            <div v-bind:style="{textAlign: 'right'}">
              <input type="submit" value="キャンセル" class="btn btn-secondary" v-on:click.prevent="$root.$set(mode,index,!mode[index])" />&nbsp;
              <input type="submit" value="更新" class="btn btn-primary" />&nbsp;&nbsp;
            </div>
            <p />
          </form>
        </div> 
      </template>   
        
    </div>  
  </div>
</div>
</template>

<script>

// 日時操作
import {format} from 'date-fns'
import ja from 'date-fns/locale/ja'

// IEのFormData対策用
import 'formdata-polyfill'

// IEのfetch対策用
import "fetch-polyfill";

export default {
  
  // ---------------------
  //  データ定義
  // ---------------------    
  data: function () {
    return {
      items: [],    // アイテム
      mode: [],     // アイテムのモード(表示/編集)
      name: '',     // 投稿 - 名前
      comment: '',  // 投稿 - コメント       
      status: 'ここに「Ajax」に関するメッセージが表示されます。' 
    }
  },
  
  // ---------------------
  //  マウント
  // ---------------------  
  mounted: function(){
    
    // JSONデータの取得
    fetch("http://localhost:3000/vue_crud_data/index.json")
      .then(res => res.json()) 
      .then(
        (result) => {             
          // リストデータ                          
          this.items = result;
          // モードの初期化(全て表示モード)
          this.mode = Array(result.length).fill(false);             
        },
        (error) => {
          this.status = error.message;
        }
      )
  },
  
  methods: {
    
    // ---------------------
    //  Ajax通信(送信用)
    // ---------------------  
    run_ajax: function(method, url, data) {
      
      fetch(url, 
           {
             method: method,
             body: JSON.stringify(data),
             headers:{
              // JSON
              'Content-Type': 'application/json',
              // CSRFトークン
              'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content') 
             }
           })
        .then(res => res.json())
        .then(
          (result) => {
            
            this.status = "サーバーからのメッセージ(" + 
                          this.formatConversion(new Date())  + ") :" + result.registration;
                          
            // 新規登録時のみIDなどが返却される
            if(result.id){
              
              // 失敗
              if(result.id == "error"){
                
                // エラー制御は行っていないので各自で。
                
              // 成功  
              }else{
                // 先頭にアイテムを追加する 
                this.items.unshift({id: result.id,
                                    name: result.name,
                                    comment: result.comment,
                                    updated_at: result.updated_at}
                                   );    
                this.mode.unshift(false);
              }
            // 更新/削除
            }else{
              // エラー制御は行っていないので各自で。
            }  
              
          },
          (error) => {
            this.status = error.message;   
          }
        )
        .catch((error) => {       
             this.status = error.message;
          }
        );   
    },      
    
    // ---------------------      
    //  日付操作 
    // ---------------------      
    formatConversion: function(updated_at) {
      return format(new Date(Date.parse(updated_at)), 'yyyy年MM月dd日(iiiii) HH:mm:ss', {locale: ja});
    },
    
    // ---------------------       
    //  データの登録
    // ---------------------   
    handleInsert: function(event) {    
      
      if (this.name && this.comment){
        
        // Ajax
        this.run_ajax("POST",
                      "http://localhost:3000/vue_crud_data/" ,
                      {datum: {name: this.name, comment: this.comment}}
                     );
                                                  
        this.name = '';
        this.comment = '';
      }   
    },
      
    // ---------------------       
    //  データの更新
    // ---------------------       
    handleUpdate: function (index, id, event) {
      const form_data = new FormData(event.target);
      
      const txt_name = form_data.get('txt_name');
      const txt_comment = form_data.get('txt_comment');
      
      if (
          (txt_name && txt_comment) &&
          (!(this.items[index].name == txt_name && 
             this.items[index].comment == txt_comment))
         ){      
        
        this.$root.$set(this.items[index], "name", txt_name);
        this.$root.$set(this.items[index], "comment", txt_comment);
        this.$root.$set(this.items[index], "updated_at", new Date());
        
        // Ajax
        this.run_ajax("PUT",
                      "http://localhost:3000/vue_crud_data/"  + id ,
                      {datum: {name: txt_name, comment: txt_comment}}
                     );
      }
      
      // 表示モードに変更する
      this.$root.$set(this.mode, index, !this.mode[index])
    },   
    
    // ---------------------       
    //  データの削除
    // ---------------------       
    handleDelete: function (index, id, event) {
      
      this.items.splice(index, 1);
      this.mode.splice(index, 1);
      
      // Ajax
      this.run_ajax("DELETE",
                    "http://localhost:3000/vue_crud_data/"  + id ,
                    {}
                   );
    }     
  },    
  
  computed: {  
    // none
  }
}
</script>

<style>
/* none */
</style>

これで、プロジェクトが完成です。動作しない場合はソース一式Rails6プロジェクトの各種初期設定を参照して下さい。






関連記事



公開日:2020年03月14日 最終更新日:2020年03月17日
記事NO:02823