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

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

React + JQuery(+Vanilla JS) + AjaxによるCRUD(作成/読み込み/更新/削除)のサンプルプロジェクトです。 ※Vanilla JSは純粋のJavaScriptです。

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

DEMO

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

ソース一式

https://github.com/TakeshiOkamoto/mpp_jquery_crud

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

目次

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

1. プロジェクトの作成

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

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

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

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

// JQuery
yarn add jquery
// 日付、時刻の操作(日本語対応)
yarn add date-fns
// FormDataをIEに対応させる
yarn add formdata-polyfill

そして、JQueryの設定を行います。

[config/webpack/environment.js]

const { environment } = require('@rails/webpacker')

// エイリアスの設定をする
environment.toWebpackConfig().merge({
  resolve: {
      alias: {
               'jquery': 'jquery/src/jquery'
             }
           }
});

module.exports = environment

3. Bootstrapの導入

// Gemfileに以下を追加してbundleする
gem 'bootstrap', '4.6.1'
 
// 手動で削除
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 jquery_crud_data name:string comment:text
// バックアップ用
bin/rails g model jquery_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)

JqueryCrudDatum.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')  
JqueryCrudDatum.create(id:2, name: 'プチモンテ', comment: '青色申告決算書 & 仕訳帳システム',
                              created_at: '2020-02-27 20:00:12', updated_at: '2020-02-27 20:00:12')  
JqueryCrudDatum.create(id:3, name: 'プチモンテ', comment: 'Rails6プロジェクトの各種初期設定',
                              created_at: '2020-02-28 05:30:12', updated_at: '2020-02-28 05:30:12')
JqueryCrudDatum.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')                              
JqueryCrudDatum.create(id:5, name: 'プチモンテ', comment: 'ネットワークカメラを用いた顔認識及び人物特定システムの構築 [防犯カメラの自作]',
                              created_at: '2020-03-02 18:22:08', updated_at: '2020-03-02 18:22:08')   
JqueryCrudDatum.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')   
JqueryCrudDatum.create(id:7, name: 'プチモンテ', comment: '収縮と膨張によるノイズ除去のサンプルコード(2値画像用)',
                              created_at: '2020-03-03 23:30:11', updated_at: '2020-03-03 23:30:11')   
JqueryCrudDatum.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')  
                              
JqueryCrudDataBk.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')  
JqueryCrudDataBk.create(id:2, name: 'プチモンテ', comment: '青色申告決算書 & 仕訳帳システム',
                              created_at: '2020-02-27 20:00:12', updated_at: '2020-02-27 20:00:12')  
JqueryCrudDataBk.create(id:3, name: 'プチモンテ', comment: 'Rails6プロジェクトの各種初期設定',
                              created_at: '2020-02-28 05:30:12', updated_at: '2020-02-28 05:30:12')
JqueryCrudDataBk.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')                              
JqueryCrudDataBk.create(id:5, name: 'プチモンテ', comment: 'ネットワークカメラを用いた顔認識及び人物特定システムの構築 [防犯カメラの自作]',
                              created_at: '2020-03-02 18:22:08', updated_at: '2020-03-02 18:22:08')   
JqueryCrudDataBk.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')   
JqueryCrudDataBk.create(id:7, name: 'プチモンテ', comment: '収縮と膨張によるノイズ除去のサンプルコード(2値画像用)',
                              created_at: '2020-03-03 23:30:11', updated_at: '2020-03-03 23:30:11')   
JqueryCrudDataBk.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 jquery_crud_data index

10. config/routes.rb

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

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

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

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

11.1 ビュー

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

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <title><%= "Rails + JQuery + 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' %>
            
    <%# jquery_crud.jsの読み込み %>
    <%= javascript_pack_tag 'jquery_crud' %>   
  </head>
  <body>
    <nav class="navbar navbar-expand-md navbar-light bg-primary">
      <div class="navbar-brand text-white"><%= "Rails + JQuery + AjaxでCRUDのサンプルプロジェクト" %></div>
      <ul class="navbar-nav ml-auto">
        <li class="nav-item"><%= link_to '全て初期化',  new_jquery_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">
         JQuery "CRUD" Sample
      </div>  
      <div class="text-center text-white">       
        Takeshi Okamoto wrote the code.
      </div>  
      <p></p>      
    </nav>    
  </body>
</html>

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

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

11.2 コントローラー

[app/controllers/jquery_crud_data_controller.rb]

class JqueryCrudDataController < ApplicationController
  before_action :set_datum, only: [:update, :destroy]
  
  # 英語のdataは複数形、datumは単数形
  
  def index
    @data = JqueryCrudDatum.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 jquery_crud_data;")
      # 高速挿入
      ActiveRecord::Base.connection.execute("INSERT INTO jquery_crud_data SELECT * FROM jquery_crud_data_bks;")
    end  
  
    redirect_to root_path
  end
    
  def create
    @datum = JqueryCrudDatum.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 = JqueryCrudDatum.find(params[:id])
    end
      
    def datum_params
      # 送信側のJSONの形式を確認する事!
      params.require(:datum).permit(:name, :comment)
    end
end

11.3 モデル

[app/models/jquery_crud_datum.rb]

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

11.4 JS

[app/javascript/packs/jquery_crud.js]

import "jquery";

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

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

// ------------------------
//  状態管理
// ------------------------
var state ={
      items: [],    // アイテム
      mode: [],     // アイテムのモード(表示/編集)
      name: '',     // 投稿 - 名前
      comment: '',  // 投稿 - コメント       
      status: 'ここに「Ajax」に関するメッセージが表示されます。'     
};

// ------------------------
//  イベント
// ------------------------

// データの変更(name)
window.handleNameChange = function(event){
  state.name = event.target.value;
  event.preventDefault();   
}

// データの変更(comment)
window.handleCommentChange = function(event){
  state.comment = event.target.value;
  event.preventDefault();   
}

// 表示モード/編集モードの切り替え
window.handleModeChange = function(index, event){
  var html = '';
  
  state.mode[index] = !state.mode[index];  
  if (state.mode[index])
    html += htmlCardEdit(index);
  else
    html += htmlCardShow(index);
  
  // 一部のみ更新
  $("#card" + state.items[index].id).html(html);  
  
  event.preventDefault(); 
}

// データの登録
window.handleInsert = function(event){
  
  if (state.name && state.comment){
    
    // Ajax
    run_ajax("POST",
             "http://localhost:3000/jquery_crud_data/" ,
             {datum: {name: state.name, comment: state.comment}}
            );
                                              
    state.name = '';
    state.comment = '';
  }  
  event.preventDefault();   
}

// データの更新
window.handleUpdate = function(index, event){
  var form_data = new FormData(event.target);
  
  var txt_name = form_data.get('txt_name');
  var txt_comment = form_data.get('txt_comment');
      
  if (
      (txt_name && txt_comment) &&
      (!(state.items[index].name == txt_name && 
         state.items[index].comment == txt_comment))
     ){      
    
    // 値の設定
    state.items[index].name  = txt_name;
    state.items[index].comment  = txt_comment;
    state.items[index].updated_at  = new Date();
    
    // 表示モードに変更する
    state.mode[index] = !state.mode[index]; 
    
    // Ajax    
    run_ajax("PUT",
             "http://localhost:3000/jquery_crud_data/"  + state.items[index].id ,
             {datum: {name: txt_name, comment: txt_comment}}
            );
  }  
  
  // 一部のみ更新
  $("#card" + state.items[index].id).html(htmlCardShow(index));   
  event.preventDefault(); 
}

// データの削除
window.handleDelete = function(index, event){
  
  // Ajax
  run_ajax("DELETE",
           "http://localhost:3000/jquery_crud_data/"  + state.items[index].id ,
            {}
           );

  state.items.splice(index, 1);
  state.mode.splice(index, 1);
    
  // すべて再描画
  $("#root").html(render());
                     
  event.preventDefault(); 
}

// ------------------------
//  関数
// ------------------------

// サニタイズ
function htmlspecialchars(str){
  
 return (str + '').replace(/&/g,'&amp;')
                  .replace(/"/g,'&quot;')
                  .replace(/'/g,'&#039;')
                  .replace(/</g,'&lt;')
                  .replace(/>/g,'&gt;'); 
}

// Ajax
function run_ajax(method, url, data){
  
  $.ajax({
      url: url,
      method: method,
      data: JSON.stringify(data),
      headers:{
        // JSON
        'Content-Type': 'application/json',
        // CSRFトークン
        'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
     }
      
  }).done(function(data, status, xhr) {
      
      // 新規登録時のみIDなどが返却される
      if(data.id){
        
        // 失敗
        if(data.id == "error"){
          
          // エラー制御は行っていないので各自で。
          
        // 成功  
        }else{
          // 先頭にアイテムを追加する 
          state.items.unshift({id: data.id,
                              name: data.name,
                              comment: data.comment,
                              updated_at: data.updated_at}
                             );    
          state.mode.unshift(false);          
    
          // すべて再描画
          $("#root").html(render());                
        }
      // 更新/削除
      }else{
        // エラー制御は行っていないので各自で。
      }  
      
      $("#status").html(htmlspecialchars("サーバーからのメッセージ(" + 
                                         formatConversion(new Date())  + ") :" + data.registration
                                         ));      
      
  }).fail(function(xhr, status, error) {
       $("#status").html(htmlspecialchars(error))
  });  
}

// 日付操作
function formatConversion(updated_at) {
  
  return format(new Date(Date.parse(updated_at)), 'yyyy年MM月dd日(iiiii) HH:mm:ss', {locale: ja});
}
    
// 表示モード    
function htmlCardShow(index){

  return  '  <div class="card-header">' +
          '    '+ htmlspecialchars(state.items[index].name) +' <br />' + formatConversion(state.items[index].updated_at) +
          '  </div>' +
          '  <div class="card-body">' +
          '    ' + htmlspecialchars(state.items[index].comment) +
          '    <br>' +
          '    <br>' +
          '    <form>' +
          '      <div style="text-align:right;">' +
          '        <input type="submit" value="編集" class="btn btn-primary" onclick="handleModeChange('+ index +', window.event);" />&nbsp;' +
          '        <input type="submit" value="削除" class="btn btn-danger" onclick="handleDelete('+ index +', window.event);" />&nbsp;&nbsp;' +
          '      </div>' +
          '    </form>' +
          '  </div>';
}

// 編集モード
function htmlCardEdit(index){

  return  '  <form onsubmit="handleUpdate('+ index +', window.event);">' +
          '    <div class="card-header">' +
          '      <input type="text" value="'+ htmlspecialchars(state.items[index].name) +'" name="txt_name" class="form-control" />' +
          '    </div>' +
          '    <div class="card-body">' +
          '      <textarea name="txt_comment" class="form-control" rows="5" >' + htmlspecialchars(state.items[index].comment) +'</textarea>' +
          '    </div>' +
          '    <div style="text-align:right;">' +
          '      <input type="submit" value="キャンセル" class="btn btn-secondary" onclick="handleModeChange('+ index +', window.event);" />&nbsp;' +
          '      <input type="submit" value="更新" class="btn btn-primary" />&nbsp;&nbsp;' +
          '    </div>' +
          '    <p></p>' +
          '  </form>';        
}

// レンダー
function render(){
  
  var html = '';
  
  html =  '<p></p>' +
          '<div class="fixed-bottom bg-dark text-white" style="opacity: 0.55">' +
          '  <span>&nbsp;</span>' +
          '  <span id="status">'+ htmlspecialchars(state.status) +'</span>' +
          '</div>' +
          '<h3>投稿</h3>' +
          '<p></p>' +
          '<form onsubmit="handleInsert(window.event);">' +
          '  <input type="text" class="form-control" placeholder="名前" onchange="handleNameChange(window.event);" />' +
          '  <textarea class="form-control" rows="5" placeholder="コメントを入力します。" onchange="handleCommentChange(window.event);" />' +
          '  <input type="submit" value="登録" class="btn btn-primary" />' +
          '</form>' +        
          '<p></p>' +  
          '<h3>一覧</h3>' +
          '<p></p>' + 
    '<div class="card-columns">';
      
      // 各カード    
      for(var i=0; i<state.items.length;i++){
        
         html += '<div class="card" id="card'+ state.items[i].id +'">';
         
           if (state.mode[i])
             html += htmlCardEdit(i);
           else                       
             html += htmlCardShow(i);
             
         html += '</div>';   
      }  
  
  html += '</div>';
  
  return html; 
}

// ------------------------
//  メイン
// ------------------------
$(function() {
  
  // JSONデータの取得
  $.ajax({
      url: "http://localhost:3000/jquery_crud_data/index.json",
      method: "GET",
  }).done(function(data, status, xhr) {
      
      // リストデータ
      state.items = data;
      
      // モードの初期化(全て表示モード)
      for(var i=0;i<data.length;i++){
        state.mode[i] = false;
      }
            
      // レンダー  
      $("#root").html(render());
      
  }).fail(function(xhr, status, error) {
      alert(error)
  });
  
});

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





関連記事



公開日:2020年03月16日 最終更新日:2022年03月31日
記事NO:02824