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,'&') .replace(/"/g,'"') .replace(/'/g,''') .replace(/</g,'<') .replace(/>/g,'>'); } // 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);" /> ' + ' <input type="submit" value="削除" class="btn btn-danger" onclick="handleDelete('+ index +', window.event);" /> ' + ' </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);" /> ' + ' <input type="submit" value="更新" class="btn btn-primary" /> ' + ' </div>' + ' <p></p>' + ' </form>'; } // レンダー function render(){ var html = ''; html = '<p></p>' + '<div class="fixed-bottom bg-dark text-white" style="opacity: 0.55">' + ' <span> </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プロジェクトの各種初期設定を参照して下さい。
関連記事
前の記事: | Rails + Vue.js + AjaxでCRUDのサンプルプロジェクト [Hello World] |
次の記事: | RailsでVue.jsの開発を行う [単一ファイルコンポーネント/Hello Wolrd] |