jqGrid使用メモ  

jqGridを使った際のメモを残しておきます。

ページャーのAddボタンの動作  

2011-08-22:jqGrid 4.1.1で確認

$("#datagrid").jqGrid.("navGrid", "#pager", opt)でページャーを追加するときのoptで以下のようなオプションを追加できる模様。

名前説明参照初期値
addfuncAddボタンを押されたときの動作。通常のフォームエディットの代わりに、実行したいFunctionを設定可能。引数なしgrid.formedit.js:1654null

Firefox4で$().jqGrid is not a functionエラー  

2011-06-09:jqGrid 3.8.2および4.0.0+jQuery 1.5.1+Firefox 4.0.1で確認

2011-07-05追記 -- jqGrid 4.1.1で解決されてました [smile](プラグインが一つのファイルにまとめられているので)

いろいろ調べてみたのですが、解決策を書いたページが見つからなかったのでメモ。
Firefox 3.6までは問題なく使えていたのに、Firefox 4にしたところ、以下のようなエラーが出ました。

$("#datagrid").jqGrid is not a function

これはjQueryの$()関数中でjqGridプラグインにアクセスした際に発生することがあるようです。
なかなか核心を突くサイトが見つからなかったのですが、Firefox 4のHTMLパーサに関して以下のような資料がありました。

実はSolutions部分ぐらいしか読んでないのですが [huh]、Firefox4では<script>タグは並列して解析される、と(たぶん)。そのため、HTMLファイルのインラインJavaScript中、$()部分(jQueryのonloadですね)がjqGridの外部ファイルを読み込む前に実行されてしまっていると推測されます(どうやらHTML5の仕様らしいです)。

jqGridのjquery.jqGrid.jsの動作  

jqGridがくせ者なのはjquery.jqGrid.jsの存在であり、これを<script>タグで読み込んだ場合、解釈され、必要なファイルが読み込まれますが、Firefox4ではこれが問題になるようです。つまり、丁寧に<script>タグでjs/grid.*.jsを読み込んでいる場合は問題が起こらない・・・はず(試してません)。たぶん以下の感じで解析されていくのかな?

  • Firefox 3.x
    1. jquery.jqGrid.jsが読み込まれる
      • js/grid.*.jsが読み込まれる(同期処理)
    2. jquery.jqGrid.jsのloadedイベント発生
    3. インラインの<script>の$()が実行される
  • Firefox 4
    1. jquery.jqGrid.jsが読み込まれる
    2. jquery.jqGrid.jsのloadedイベント発生
    3. インラインの<script>の$()が実行される
      • js/grid.*.jsが読み込まれる(非同期処理のためタイミング不定)

Firefox4では、js/grid.*.jsを読み込む処理がブロックされないため、実際に読み込みが完了していないのにloadedイベントが発生してしまう状態になると推測されます。そのため、環境によってはエラーが再現しないという状況になるようです。

解決方法  

これを解消するためには上のサイトのSolutionsの1つめの方法、document.write()を使います。document.write()を使うと以下のタイミングになるようです。HTML5 Script Execution Changes in Firefox 4によると以下のように解説されています。

If you want to add an external script from a script during the parse and you want the script to block subsequent scripts appearing in the network stream…

  • Use document.write("\u003Cscript src='foo.js'>\u003C/script>");. This solution is cross-browser-compatible without sniffing. If you write multiple scripts at once, Firefox 4 downloads the scripts in parallel, so concern about parallel downloads is no longer a reason to avoid document.write in Firefox.
  • Alternatively, you could use plain <script> tags in the HTML source transferred over HTTP. This way Firefox (and other browsers) that implement speculative script loading will start fetching the scripts even earlier and will load the scripts in parallel.

ここに示されているdocument.write()による方法を使うと以下のように処理されると考えられます。なお、ここでは<の代わりに\u003Cを使っていますが、私が確認したところ通常の<でも問題ないようでした。

  1. jquery.jqGrid.jsが読み込まれる
    • document.write()でjs/grid.*.jsのタグ書き込み、解釈(同期処理になる)
  2. jquery.jqGrid.jsのloadedイベント発生
  3. インラインの$()が実行される

具体的にはjquery.jqGrid.js中の以下の部分を修正します(これは3.8.2ですが4.0.0でも同様の箇所があるはず)。

34  if (jQuery.browser.msie) {
35      document.write('<script charset="utf-8" type="text/javascript" src="'+filename+'"></script>');
36  } else {
37      IncludeJavaScript(filename);
38  }

この部分によるとFirefoxではfunction IncludeJavaScript()でDOMを使って外部ファイルを読み込むようにしていますが、IEではdocument.write()を使っています。そこで、Firefox4でもdocument.write()を使うように修正します。
34行目のif文を以下のように修正します。

34  if (jQuery.browser.msie || $.browser.mozilla && $.browser.version >= "2") {

Firefox 4.0.1では$.browserは{mozilla=true,version="2.0.1"}でした。そのため、この条件によりFirefox4でもdocument.write()が使われ、問題が解消しました。

わかりにくい説明ですが、試してみてください [smile]

jqGrid-3.8.2メモ  

jqGridのAJAXコール  

grid.base.jsの該当部分  

  1584 case "json":
  1585 case "jsonp":
  1586 case "xml":
  1587 case "script":
  1588     $.ajax($.extend({
  1589         url:ts.p.url,
  1590         type:ts.p.mtype,
  1591         dataType: dt ,
  1592         data: $.isFunction(ts.p.serializeGridData)? ts.p.serializeGridData.call(ts,ts.p.postData) : ts.p.postData,
  1593         success:function(data,st) {
  1594             if(dt === "xml") { addXmlData(data,ts.grid.bDiv,rcnt,npage>1,adjust); }
  1595             else { addJSONData(data,ts.grid.bDiv,rcnt,npage>1,adjust); }
  1596             if(lc) { lc.call(ts,data); }
  1597             if (pvis) { ts.grid.populateVisible(); }
  1598             if( ts.p.loadonce || ts.p.treeGrid) {ts.p.datatype = "local";}
  1599             data=null;
  1600             endReq();
  1601         },
  1602         error:function(xhr,st,err){
  1603             if($.isFunction(ts.p.loadError)) { ts.p.loadError.call(ts,xhr,st,err); }
  1604             endReq();
  1605             xhr=null;
  1606         },
  1607         beforeSend: function(xhr){
  1608             beginReq();
  1609             if($.isFunction(ts.p.loadBeforeSend)) { ts.p.loadBeforeSend.call(ts,xhr); }
  1610         }
  1611     },$.jgrid.ajaxOptions, ts.p.ajaxGridOptions));
  1612 break;

これを見るとjqGridのオプションとしてはurl, mtype, datatype, postData, serializeGridData, loadErrorが使われる。また$.extendでajaxOptions, ajaxGridOptionsが結合されるため、ajaxGridOptionで設定すれば任意のオプションを上書きすることができる(おそらく高度な使い方だろうが)。

処理  

  1. リクエストが送信される前にbeforeSendハンドラがコールされ、jqGrid#loadBeforeSendが設定されていればbeforeSendハンドラ内でjqGrid#loadBeforeSend(xhr)がコールされる。
  2. 送られるデータはjqGrid#serializeGridDataが定義されていればjqGrid#serializeGridData(postData)の戻り値が送信される。定義されていなければjqGrid#postDataの内容が送信される。
      1592 data: $.isFunction(ts.p.serializeGridData)? ts.p.serializeGridData.call(ts,ts.p.postData) : ts.p.postData,
  3. 成功時は通常のjqGridのsuccessハンドラ(1593行目)、エラー時はloadErrorが定義されていればloadError(xhr,textStatus,errorThrown)が、されていなければerrorハンドラ(1602行目)がそれぞれコールされる。

jqGridのデータ取得でJSON-RPC  

ここでは、jqGridを使う際にJSON-RPCを用いる方法を模索していきます。JSON-RPC 2.0提案(proposal)に則ったメッセージパッシングを試してみます。

jqGridのデータ取得リクエスト  

データ取得リクエストは最後にjqGrid#serializeGridData()がコールされるので、ここで与えられるデータをJSON-RPCリクエストのJSON文字列に変換します。

serializeGridData: function(postData) {
    var obj = GLOBAL.json.wrapInJSONRPCRequest("objects.select", ["User", {gid:1}]);
    return GLOBAL.json.objToJson(obj);
},

ここでのGLOBAL.json.wrapInJSONRPCRequestは

wrapInJSONRPCRequest(string Method name, mixed Arguments)

となっていて、

{
    jsonrpc: "2.0",
    method : "Method name",
    params : Arguments,
    id     : #ID
}

というオブジェクトを返します。#IDはGLOBAL.jsonで保持しているランダムなID番号。
返されたオブジェクトをJSON文字列に変換すればそれがjqGridにより送信されます。

{"jsonrpc":"2.0","method":"Method name","params":JSON of Arguments,id:#ID}

がURLエンコードされた文字列で送られます

応答時のエラー処理  

フックする部分が見つからず、送信よりも応答時の処理の方が難しく感じました。ここで言うエラーとはInternal Server Error 500などのHTTPエラーではなく、あくまでJSON-RPCのエラーです。従って、HTTP通信は正常に完了しているというところに留意してください。そのため、AJAX処理内ではsuccessハンドラがコールされます。
JSON-RPCエラーオブジェクトの処理はjqGridのsuccessハンドラ内(1593-1601行目)で行うことがスマートだと思いますが、successハンドラ内ではフックされるメソッドがありません。そのため、jqGrid#ajaxGridOptionsでいっそのことsuccessハンドラを上書きするかjqGrid#jsonReaderを使った少々トリッキーな方法をとることにします。
jsonReaderはデータ構造を定義するパラメータで、関数としてコールされるのでその中に処理を書くことでJSON-RPCエラー処理を行うことができます。jsonReader.root()がrecordsの配列データを返さなければ(空配列やundefined、null)、データは追加されないことを利用します。例えば以下のようにします。

jsonReader: {
    repeatitems: false,
    id:          "id",
    root:        function(obj){   // obj["result"]がなければundefinedでデータは追加されない
        if(obj.result === undefined && obj.error){
            alert(obj.error.message + "\n" + obj.error.data);
            return [];
        }
        return obj["result"];
    },
    page:        function(obj){ return 1; },
    total:       function(obj){ return 1; },
    records:     function(obj) {
        if(obj.result === undefined && obj.error){ return 0; }
        return obj["result"].length;
    }
}

rootの前にpage, total, recordsが処理されるのでこの段階でobject#resultを使うとundefinedエラーになるのでその辺りは適宜処理します。この例ではrecords()で0を返し、root()で空配列[]を返すようにしています。エラーの表示等が必要ならrecords, rootどちらで行っても問題ないようです。

インライン編集でのJSON-RPC  

インライン編集の場合もグリッドデータ取得とほぼ同じような手順で行います。
インライン編集を行う際はjqGrid APIのeditRowを使用します。

editRow(rowId, keys, onEditFunc, successFunc, url, extraParam, afterSaveFunc, errorFunc, afterRestoreFunc)

レコードを選択したときに編集開始  

$("#datagrid").jqGrid({
    ...
    onSelectRow: function(id) {
        if(id && id !== GLOBAL.lastSel){
            $("#datagrid").restoreRow(GLOBAL.lastSel);
            GLOBAL.lastSel = od;
        }
        $("#datagrid").jqGrid("editRow", id, true, false, rpcOnSuccess, "JSONRPCService");
    },
    ...
});

ここのポイントはeditRowの第4引数のrpcOnSuccessです。JSON-RPCメッセージで応答が返ってくるので、後述するようにレスポンスを処理しなければなりません。そのためにrpcOnSuccessメソッドで処理します。また、RPCリクエストは後述のjqGrid#serializeRowDataプロパティで処理を行います。リクエスト送信先は第5引数urlで指定しますが、省略はできないようです。
なお、ここでは第2引数のkeysがtrueになっているのでEnterを押すことでリクエストが発行されます。

リクエスト  

リクエスト時の処理は、jqGrid#serializeRowDataをセットします。例えば以下のようにコードすると送信前にrow_dataをシリアライズすることが可能になります。serializeRowDataは送信文字列を戻すようにします。

serializeRowData: function(row_data) {
    var obj = JSONRPCRequest("objects.setuser", [row_data.id, row_data]);
    return obj.toJson();
},

レスポンスの処理  

応答時の処理で問題となるのはグリッドデータと同様JSON-RPCのエラーオブジェクトの扱いです。HTTP成功後の処理をフックするときはeditRowの第4引数successFuncに関数で行うことができます。この関数で真を返せば編集処理成功、偽なら失敗と判定することが可能です。
たとえば以下のようにします。

function rpcOnSuccess(xhr) {
    obj = jsonToObj(xhr.responseText);
    if(obj.error){ alert("Error!!"); return false; }
    return true;
}
$("#datagrid").jqGrid("editRow", id, true, false, rpcOnSuccess, "JSONRPCService");

これでリクエストが成功すればonSuccess(XMLHttpRequest)がコールされます。真を返せば編集がデータグリッドに反映され、偽なら編集処理が失敗とみなされ、データグリッド内の対象データが元のデータに戻ります。ここで反映されるというのはデータグリッドに表示されているデータが書き換えられるということを示します。

ダイアログ編集でのJSON-RPC  

フォームエディットでのJSON-RPCの方法を解説します。フォームエディットではjqGrid#editGridRow()で行います。

editGridRow(rowId, properties)

ここのpropertiesで値をセットすることで挙動を制御します。

レコードをダブルクリックした時に編集開始  

$("#datagrid").jqGrid({
    ...
    ondblClickRow: function(id, iRow, iCol, e) {
        if(id && id!== GLOBAL.lastSel){
            $("#datagrid").restoreRow(GLOBAL.lastSel);
            GLOBAL.lastSel = id;
        };
        $("#datagrid").jqGrid("editGridRow", id, {
            url: "JSONRPCService",
            serializeEditData: function(data): { ... },
            afterSubmit: function(xhr, postData): { ... },
            closeAfterEdit: true
        };
    },
    ...
});

インライン編集とは書式が異なっているので注意。編集するレコードのIDと対するプロパティを指定することで編集します。後述のようにリクエストを発行する際にはserializeEditDataプロパティによる処理、レスポンスはafterSubmitプロパティで処理します。
JSON-RPCで処理する場合は上記のurl, serializeEditData, afterSubmitは必須です。
closeAfterEditプロパティを真にすると編集終了後にダイアログを閉じます。

フォームからのリクエスト  

propertiesserializeEditDataに送信前のデータを処理する関数を定義します。戻り値はJSON文字列になるようにします。例えば以下の通り。

serializeEditData: function(data) {
    delete data.id;
    var obj = GLOBAL.json.wrapInJSONRPCRequest("usermgr.add_user", [data]);
    return GLOBAL.json.objToJson(obj);
},

戻り値の処理  

レスポンスもインライン編集と同じように処理しますが、ここではpropertiesafterSubmitに受信後の処理をセットします。引数はXMLHttpRequestとpostDataです。

afterSubmit: function(xhr, postdata) {
    var res = GLOBAL.json.jsonToObj(xhr.responseText);
    var msg = "Unexpected exception.";
    if(res.error && res.error.code){  // JSONRPCErrorの場合
        e = res.error;
        msg = "<b>RPC error: " + e.message + "(" + e.code + ")</b><br/>" + e.data;
        return [false, msg, null];
    }
    return [true, "OK", res.result.loid];
}

afterSubmit()はmixed配列を返し、[ステータス、メッセージ、新規レコードのID]となります。ステータスは真偽値で真なら成功です。ステータスに偽を返した場合、メッセージが表示され、操作が取り消されます。成功した場合は新規レコードのIDをセットすることでレコードに新たなIDが与えられます。

jqGrid JSON-RPCのまとめ  

これらをまとめると以下の表のようになります。

処理リクエストレスポンス
グリッドデータ取得serializeGridDatajsonReader
インライン編集serializeRowDataeditRowの第4引数
フォーム編集serializeEditDataafterSubmit

レスポンスの扱いがややこしいですね。別の方法としてはレスポンス時にHTTPエラーステータスを返すのも一つの手ですが、それはそれで弊害がありそうな・・・

jqGridのTreeGridでJSON-RPC  

TreeGridでツリーが追加される際にJSON-RPCにしたい場合、reloadGridイベントをあらかじめセットしておく。
バブリングにより、jqGrid本体のreloadGridが実行される前にここでセットしたreloadGridハンドラが実行される。

$("#treegrid").bind("reloadGrid", function() {
    ajaxアクセスが起こる前の処理...
});
$("#treegrid").jqGrid({パラメータ...});

なお、bindは自分を返すのでカスケード可能。

$("#treegrid").bind("reloadGrid",...).gqGrid({...});

こんな感じ。

TreeGridのtreeReader.expand_fieldが動作しない  

使わなければよい・・・
修正するなら以下のようにする。

--- grid.treegrid.js.orig       2010-12-23 09:10:43.000000000 +0900
+++ grid.treegrid.js    2010-12-23 09:11:22.000000000 +0900
@@ -412,8 +412,9 @@
        collapseNode : function(rc) {
                return this.each(function(){
                        if(!this.grid || !this.p.treeGrid) { return; }
-                       if(rc.expanded) {
-                               rc.expanded = false;
+                       var expanded = this.p.treeReader.expanded_field;
+                       if(rc[expanded]) {
+                               rc[expanded] = false;
                                var id = $.jgrid.getAccessor(rc,this.p.localReader.id);
                                var rc1 = $("#"+id,this.grid.bDiv)[0];

コメント