#8 リソースマスタ画面を作る(3)

投稿者: | 2021年5月5日

さて今回は、前回まででリソースマスタ画面の登録機能は作ったので、一覧表示部分を作ります。

まずは振り返りです。現在はまだDB上のデータを取得しておらず、ダミーデータが出ているのみです。

今回のゴールは、下記の2点です。

  • 一覧部分に出ているダミーデータを、DB上から取得した値にすること
  • 1ページ20件として、データベース上の件数が21件以上であればページネーションを表示すること

ページネーション化は後回しにして、まずは、データを取得できるようにしましょう。

画面の初期表示とデータの取得

ユーザ目線では、この画面の初期表示時にデータを表示しているようにふるまいます。しかし、アプリのバックエンドとしては初期表示とデータの取得は分けて作成します。

これは後々ページネーション化する際に、共通化できるためです。

初期表示時、前へクリック時、次へクリック時のいずれも、同じ検索処理を実施します。そして、レスポンスとして帰ってきたデータを一覧に表示し、次ページや前ページの有無によってボタンを活性化したり、非活性にします。

検索はAjaxとして実行し、結果はバックエンドが返します。結果を設定する処理は、Ajaxでレスポンスを受け取ったときのコールバックとして書きましょう。

クライアント側の共通ファンクション

ウェイティングリストアプリではAjaxでリクエストを送信し、レスポンスを画面にマッピング…ということを頻繁に行います。この部分を共通のファンクションとして定義してしまいましょう。新しいファイルをassetsの下に作ります。ファイルを作成後は、念のためrails sでサーバは再起動しておきましょう。

app/assets/javascripts/util/table.js
(function($) {
});
'user strict';

/**
 * submitボタンにバインドすることを想定したファンクション
 * @param {*ajax先のURL} action 
 * @param {*POSTやDELETE,PUTなどのメソッド} method 
 * @param {*送信するデータを返すファンクション UTF-8であること.} getDataFunc 
 * @param {*submit前の事前チェックファンクション.文字列null以外をエラーメッセージとみなし、表示してsubmitを中断する.} beforeFunc 
 * @param {*ajaxのdoneに渡すcallback} successFunc 
 * @returns 
 */
function submitFunction( action, method, getDataFunc, beforeFunc = undefined, successFunc = undefined ){
    if (beforeFunc === undefined) {} else {
        //エラーメッセージがnull以外ならばpostせずに終わる.
        var errmsg = beforeFunc()
        if (errmsg != null) {
            errorMessageFunction( errmsg );
            return;
        }
    }
    doAjax(action, method, JSON.stringify(getDataFunc()), successFunc);
}
/**
 * ajax通信を行うファンクション
 * @param {*AJAX先のURL} action 
 * @param {*POSTやDELETE,PUTなどのメソッド} method 
 * @param {*送信するデータ UTF-8であること.} data 
 * @param {*ajaxのdoneに渡すcallback} callBack 
 */
function doAjax(action, method, data, callBack) {
    $.ajax({
        url: action,
        type: method,
        data: data,
        dataType: 'json',
        contentType: 'application/json; charset=utf-8',
        async: true,
        processData: true,
        cache: false
    }).fail(function(xhr, status, error) {
        var dialog = $('<div></div>').text(xhr.responseJSON.message);
        dialog.dialog({
            modal: true,
            title: "エラーがあります.",
            buttons: [{
                text: 'OK',
                click: function() { $(this).dialog('close'); }
            }]
        });
    }).done(function(d) {
        if (callBack === undefined) {} else {
            callBack(d);
        }
    });
}
function errorMessageFunction( errmsg ){
    var dialog = $('<div></div>').text(errmsg);
    dialog.dialog({
        modal: true,
        title: "エラーがあります.",
        buttons: [{
            text: 'OK',
            click: function() { $(this).dialog('close'); }
        }]
    });
}

次に、画面側の実装を行います。今までダミーデータを表示していたところを消します。

app/views/w_resource/new.html.erb
         let data = [];
-        for(var i = 0; i < 10; i++){ data.push( { name: 'dummy_name' + i , ref: 'dummy_name' + i } ); }
         hot.updateSettings({

先ほど作った共通ファンクションを使って、バックエンドの検索処理をたたきます。(searchResources)

処理が完了するとレスポンスを引数に持つファンクションが実行されるようにしてあります。

app/views/w_resource/new.html.erb
+    let viewSearchResult = function( result ){
+        let data = result.data;
+        let settings = hot.getSettings();
+        settings.data = data;
+        hot.updateSettings(settings);
+    };
+    let searchResources = function(){
+        const searchAction = '<%= search_w_resource_path %>'
+        let getDataFunc = function(){ return {}; };
+        submitFunction( searchAction, 'POST', getDataFunc, beforeFunc = undefined, viewSearchResult );
+    };
     resources();
+    searchResources();

バックエンドの実装

ページネーションの情報をマップに詰めて画面に返せるようにします。基本的にはどのコントローラでも使うのでApplicationControllerに実装してしまいます。

app/controllers/application_controller.rb
+  def kaminari_object( kaminari )
+    {
+      current_page: kaminari.current_page,
+      total_pages: kaminari.total_pages,
+      has_next: kaminari.total_pages > kaminari.current_page,
+      has_prev: kaminari.current_page > 1
+    }
+  end
app/controllers/w_resource_controller.rb
    def search
        test request
        begin
            page = request[:page] || 1
            data = WaitingResource.page(page).per(20).order(created_at: :desc)
            render status:200, json:{
                message: "success", status: :OK, data: data, kaminari: kaminari_object(data)
            }
        rescue => e
            obj = errorHandling(e)
            render status: obj[:status], json: obj.to_json
        end
    end

実行してみよう

リソース名と備考を記入し、追加を押します。

登録処理の中では、登録しましたというメッセージと再度このページにリダイレクトがかかります。

flash[:success] = "登録しました."
redirect_to w_resource_path

すると初期表示が行われ、まずはHandsontableの初期化が実行されます。そしてすぐに、バックエンドの検索処理が実行されます。

let searchResources = function(){
    const searchAction = '<%= search_w_resource_path %>'
    let getDataFunc = function(){ return {}; };
    submitFunction( searchAction, 'POST', getDataFunc, beforeFunc = undefined, viewSearchResult );
};
resources();
searchResources();

バックエンドの処理は、WaitingResourceのcreated_atという項目の新しい順に20件を取得し、「data」という項目に詰めて返します。

返すデータのサンプルは下記の通りです。

{"message":"success","status":"OK"
 ,"data":[
  {"id":28,"name":"新規の登録","ref":"新規の備考","created_at":"2021-05-05T05:12:21.357Z","updated_at":"2021-05-05T05:12:21.357Z"},
  {"id":27,"name":"テーブル席U","ref":"TEST","created_at":"2021-05-04T16:45:42.238Z","updated_at":"2021-05-04T16:45:42.238Z"},
  (以下略)
 ]
 ,"kaminari":{"current_page":1,"total_pages":2,"has_next":true,"has_prev":false}
}

dataというデータHandsontableのdataに指定することで、データを画面に表示します。

    let viewSearchResult = function( result ){
        let data = result.data;
        let settings = hot.getSettings();
        settings.data = data;
        hot.updateSettings(settings);
    };

コールバック

画面側の共通ファンクションである「submitFunction」や「doAjax」は単純に「送信」と「受信したデータを処理するファンクションを実行する」ということしかしません。「具体的に受信データを処理するファンクション」は、画面ごとに異なってきますので、そちらで実装します。

コールバックという仕組みは、プログラミングの歴史の中では古くからあるテクニックです。一言でいうと、「関数(ファンクション)を引数に渡して、渡された側で実行する」というテクニックです。今回だとJqueryのajaxファンクションのdone時にもらったファンクション(ここでは「viewSearchResult」)を実行しています。

    }).done(function(d) {
        if (callBack === undefined) {} else {
            callBack(d);
        }
    });

練習問題

ChromeやEdgeなどのブラウザを起動してF12を押し、Consoleを出してください。

下記のソースを張り付けてください。

let admin  = function( id, name ){ return "id:" + id + "は" + name + "さん." };
let normal = function( id, name ){ return name + "さん." };
function disp( viewFunc ){
  let members = [];
  members.push( { id: 1, name: "鈴木" } );
  members.push( { id: 2, name: "佐藤" } );
  for(let i = 0; i < members.length; i++){ console.log( viewFunc( members[i].id, members[i].name ) ); }
}

disp(admin)と叩いて実行してみます。

disp(normal)と実行するとこのようになります。

このようにすることで、表示するメッセージの編集は、「文字列を返すファンクション」さえ作れば、何通りでも好きに作ることができます。下記のように表示するファンクションを作り、dispに渡して実行してください。

disp(test);
鈴木さん,あなたのidは1です.
佐藤さん,あなたのidは2です.

次回

今回は画面にデータの表示するだけで内容が膨らんでしまったので、次回こそは「ページネーション」をやっていきます。