YUI2.7.0のEditable Table(編集可能な表)でCSV形式のデータを更新・削除する。
これまでの一連のログで一通りの道具立てが揃ったので、YUI2.7.0のDataTable(EditableTable;編集可能な表)を使って、以下のフラットファイル(CSV形式)のデータを更新・削除するプログラムを書いてみた。
01,北海道,テスト 02,青森,- 03,秋田,- 04,岩手,- 05,宮城,- 06,山形,- 07,福島,- 08,新潟,- 09,茨城,- 10,栃木,- 11,群馬,- 12,山梨,- 13,埼玉,- 14,東京,- 15,神奈川,- 16,千葉,- 17,長野,- 18,岐阜,- 19,富山,- 20,石川,- 21,福井,- 22,静岡,- 23,愛知,- 24,三重,- 25,和歌山,- 26,奈良,- 27,京都,- 28,大阪,- 29,兵庫,- 30,滋賀,- 31,徳島,- 32,香川,- 33,愛媛,- 34,高知,- 35,岡山,- 36,広島,- 37,島根,- 38,鳥取,- 39,山口,- 40,福岡,- 41,佐賀,- 42,長崎,- 43,大分,- 44,宮崎,- 45,熊本,- 46,鹿児島,- 47,沖縄,-
これを表示した初期画面のスナップショットは以下。
以下は、areacode=02の青森のnotesを更新し、03の秋田を削除したところ。
この更新・削除後に画面をリフレッシュして、最新のデータをDataTableに反映するには、先のログにかいた「キャッシュデータの考慮」が必要になる。
以下の順にプログラムを示す。
最初は、上のフラットファイルを更新・削除するPHPのクラス。
フラットファイルにこれらの操作をするためには、(ファイルに対しての)排他制御をする必要があるので、以前のログに書いたBaseTextDao.class.phpをExtendしたクラスになっている(BaseTextDaoという名前を付けたのは、今回のようなフラットファイル用のDaoの基底にしたかったため)。ファイルへの操作を行う各メソッドがファイルポインタを引数にとるのは、呼び出し側のPHPプラグラムで排他制御を行いたいためである。
ところで、このクラスには、updateというメソッドが存在しない。フラットファイルのデータは、ピンポイントでの(=カーソルを合わせての)更新ができないので、行の削除と登録を組み合わせることにした。
また、YUIのDataTableには、画面をロードした際のソーティング機能が見当たらないので、データをソートするメソッドも含んでいる。
<?php /** * PrefTextDao.class.php * * (C) 2009, tetsuya.odaka(EzoGP). * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* * Pref.txtのためのDao * (注)ファイルのロック処理のため、呼び出し側から * ファイルポインタをもらうことを前提にする。 * * * author ; t.odaka * date ; 2009/5/7 * */ require_once('BaseTextDao.class.php'); class PrefTextDao extends BaseTextDao { /* * プロパティー */ var $Areacode, $Pref, $Notes1; /* * デフォルトのコンストラクタ */ function __construct() { parent::__construct(); } /* * Getter */ function getAreacode(){return $this->Areacode;} function getPref(){return $this->Pref;} function getNotes1(){return $this->Notes1;} /* * Setter */ function setAreacode($_str){$this->Areacode = $_str;} function setPref($_str){$this->Pref = $_str;} function setNotes1($_str){$this->Notes1 = $_str;} /* * findById * 主キーをもとに、データを取得する。 * パラメータ * $_fp : this->FileNameのファイルポインタ * 戻り値 : 見つかったらPrefTextDaoオブジェクト、なかったらnull. */ function findById($_fp, $_id){ while($_rarray = fgetcsv($_fp,1024)){ if(intval($_rarray[0]) == intval($_id)){ $retObj = new PrefTextDao(); $retObj->setAreacode($_rarray[0]); $retObj->setPref($_rarray[1]); $retObj->setNotes1($_rarray[2]); return retObj; } } return null; } /* * delete * 与えられる主キーに該当する特定の(行)データを削除する。 * パラメータ * $_fp : this->FileNameのファイルポインタ(注;rで開いてください) * $_id : 削除する行の主キー */ function delete($_fp, $_id){ /* * $_id(指定された主キー)以外の行をCSVファイルから、一時ファイルに書き出す。 */ // 一時ファイルの作成 $_tfp = tmpfile(); // 読み込んだデータを一時ファイルに書き出す。 while($_rarray = fgetcsv($_fp,1024)){ if($_rarray[0] != $_id){ /* 出力バッファへの書き出し */ $_data = $_rarray[0]; $_data.= ","; $_data.= $_rarray[1]; $_data.= ","; $_data.= $_rarray[2]; $_data.= "\n"; // 一時ファイルへの書き出し。 fwrite($_tfp,$_data); } } /* * 一時ファイルから、元のファイルに書き出す。 */ // wモードで開く(=書き直しをする)。 $_fp = fopen($this->FileName, "w"); // 一時ファイルのファイルポインタを、一時ファイルの先頭に戻す。 rewind($_tfp); // 一時がファイルから元のファイルへ、データを書き出す。 while($_rarray = fgets($_tfp,1024)){ fwrite($_fp,$_rarray); } // 一時ファイルの削除。 fclose($_tfp); // $_fp(元のファイルのポインタ)は呼び出し側で閉めるので、開けっ放しにしておく。 return; } /* * insert * データを登録する(注;aで開いてください)。主キー(areacode)は、最大値を算出する。 * パラメータ * $_fp : this->FileNameのファイルポインタ */ function insert($_fp){ /* * areacodeの最大値を取得する */ $_rmax = $this->findMaxId($_fp); /* * データの書き出し */ $_data = $_rmax + 1; $_data.= ","; $_data.= $this->Pref; $_data.= ","; $_data.= $this->Notes1; $_data.= "\n"; fwrite($_fp,$_data); return true; } /* * insertWithId * 主キー(Areacode)を指定してデータを登録する(注;aで開いてください)。 * パラメータ * $_fp : this->FileNameのファイルポインタ * $_id : 主キー(areacode) * 戻り値 : 登録できたらtrue/以外はfalse. */ function insertWithId($_fp){ /* * areacodeがダブらないか確認する。 */ $_id = $this->getAreacode(); // findbyIdの戻り値がnullのときに処理する。 if(!$this->findbyId($_fp, $_id)){ /* * データの書き出し */ $_data = $_id; $_data.= ","; $_data.= $this->Pref; $_data.= ","; $_data.= $this->Notes1; $_data.= "\n"; fwrite($_fp,$_data); return true; } return false; } /* * findMaxId * 主キー(数値)の最大値を求める * パラメータ * $_fp : this->FileNameのファイルポインタ * 戻り値 : 主キーの最大値 */ function findMaxId($_fp){ $_rmax = 0; /* areacodeの最大値を取得 */ $_wkarray = array(); while($_rarray = fgetcsv($_fp,1024)){ array_push($_wkarray, intval($_rarray[0])); } if(count($_wkarray) > 0) $_rmax = max($_wkarray); return $_rmax; } /* * sortById * 主キー(areacode)にもとづいてデータをソートする。 * パラメータ * $_fp : this->FileNameのファイルポインタ(注;rで開いてください) * $_id : 削除する行の主キー */ function sortById($_fp){ $_tArray = array(); // 読み込んだデータを一時ファイルに書き出す。 while($_rarray = fgetcsv($_fp,1024)){ if($_rarray[0] != $_id){ /* 出力バッファへの書き出し */ $_data = $_rarray[0]; $_data.= ","; $_data.= $_rarray[1]; $_data.= ","; $_data.= $_rarray[2]; $_data.= "\n"; // 一時ファイルへの書き出し。 array_push($_tArray,$_data); } } sort($_tArray); /* * 一時ファイルから、元のファイルに書き出す。 */ // wモードで開く(=書き直しをする)。 $_fp = fopen($this->FileName, "w"); // 一時がファイルから元のファイルへ、データを書き出す。 for($_i=0;$_i < count($_tArray);$_i++){ fwrite($_fp,$_tArray[$_i]); } // $_fp(元のファイルのポインタ)は呼び出し側で閉めるので、開けっ放しにしておく。 return; } } ?>
以下は、上のクラス(PrefTextDao.class.php)を呼び出すPHPプログラムで、上の画面のサブミットボタンで更新・削除情報を(Ajax=XHRで)受け取るもの。
画面(Javascript)からこのPHPへのデータの転送形式は、以前のログに書いた通り。
<?php /* * クライアントからのajax送信を受け取るサンプル * * author ; t.odaka * date ; 2009/5/6 */ require_once("PrefTextDao.class.php"); require_once("../Myznala/MyConverter.class.php"); // リクエストデータの取り出し switch($_SERVER['REQUEST_METHOD']) { case 'GET' : $rMethod = &$_GET; break; case 'POST' : $rMethod = &$_POST; break; default: } // パラメータをサニタイズして配列に入れる。 $cObj = new MyConverter(); $reqParm = array(); foreach ($rMethod as $key => $value) { $key =$cObj->sanitize($key,'UTF-8'); $value =$cObj->sanitize($value,'UTF-8'); $m_log->debug($key.'; '.$value); // Debugログ $reqParm[$key]=$value; } // データの更新と削除 $pObj = new PrefTextDao(); $pObj->setFileName('data/pref.txt'); if($reqParm['o']=='u'){ /* * Id(Areacode)を指定して削除する */ // ファイルポインタとロックを取得する。'r'でロックを獲得すること。 $fp = $pObj->getLock(1000000); $fp = fopen($pObj->getFileName(), "r"); $pObj->delete($fp, $reqParm['areacode']); // ロックを獲得したまま、(aモードで開くために)一旦、クローズする。 fclose($fp); /* * Id(Areacode)を指定して登録する */ $fp = fopen($pObj->getFileName(), "a"); $pObj->setAreacode($reqParm['areacode']); $pObj->setPref($reqParm['pref']); $pObj->setNotes1( $cObj->setDefault($reqParm['notes1'],'-','UTF-8') ); $pObj->insertWithId($fp); // ロックを獲得したまま、(rモードで開くために)再度、クローズする。 fclose($fp); /* * ソートする */ $fp = fopen($pObj->getFileName(), "r"); $pObj->sortById($fp); // ファイルポインタとロックを解放する。 $pObj->fileLockClose($fp); }else if($reqParm['o']=='d'){ // ファイルポインタとロックを取得する。('r'でロックを獲得すること) $fp = $pObj->fileLockOpen('r',1000000); /* * 削除する */ $pObj->delete($fp, $reqParm['areacode']); // ファイルポインタとロックを解放する。 $pObj->fileLockClose($fp); } return; ?>
最後に、Javascriptを含むHTMLの全文を示す。
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Pragma" content="no-cache"> <meta http-equiv="Cache-Control" content="no-cache"> <meta http-equiv="Expires" content="Thu, 01 Dec 1994 16:00:00 GMT"> <meta http-equiv="Content-Type" content="text/html;charset=utf-8"> <meta http-equiv="Content-Style-Type" content="text/css"> <meta http-equiv="Content-Script-Type" content="text/javascript"> <title>DataTable Validate</title> <style type="text/css"> </style> <link rel="stylesheet" type="text/css" href="../scripts/lib/yui/build/fonts/fonts-min.css" /> <link rel="stylesheet" type="text/css" href="../scripts/lib/yui/build/paginator/assets/skins/sam/paginator.css" /> <link rel="stylesheet" type="text/css" href="../scripts/lib/yui/build/datatable/assets/skins/sam/datatable.css" /> <script type="text/javascript" src="../scripts/lib/yui/build/yahoo-dom-event/yahoo-dom-event.js"></script> <script type="text/javascript" src="../scripts/lib/yui/build/animation/animation.js"></script> <script type="text/javascript" src="../scripts/lib/yui/build/element/element.js"></script> <script type="text/javascript" src="../scripts/lib/yui/build/container/container.js"></script> <script type="text/javascript" src="../scripts/lib/yui/build/dragdrop/dragdrop-min.js"></script> <script type="text/javascript" src="../scripts/lib/yui/build/paginator/paginator-min.js"></script> <script type="text/javascript" src="../scripts/lib/yui/build/datasource/datasource-min.js"></script> <script type="text/javascript" src="../scripts/lib/yui/build/datatable/datatable-min.js"></script> <script type="text/javascript" src="../scripts/lib/yui/build/connection/connection-min.js" ></script> <!--// MyValidatorの読み込み --> <script type="text/javascript" src="../scripts/myznala.js"> </script> <style type="text/css" id="defaultstyle"> #main { margin: 2px; padding: 3px; } .ez_error { /* red(エラー用) */ color:#ff0000; } .actdel img { /* 都道府県に付ける「×(行削除)」イメージのスタイル */ border:0; margin-bottom: -3px; } </style> <script type="text/javascript"> EzTable = function() { var myDataSource; var myDataTable; var Dom = YAHOO.util.Dom; var Event = YAHOO.util.Event; var Connect = YAHOO.util.Connect; // MyValidatorオブジェクト var valObj; // 選択された行を保管 var wData; // 削除された行と行数を保管 var delRow; var delNum; // 更新データ保管用 var columnName; var uValue; // データ転送確認用 var eValue; /* * 都道府県に「×(行削除)」のマークを付ける * (注)カラム定義より前のこと */ var ezInitFormatter = function(elCell, oRecord, oColumn, oData) { elCell.innerHTML = oData + ' ' + ' ' + '<a class="actdel" href="#">' + '<img class="actdel" src="../images/action_delete.png">' + '</a>'; }; /* * メモ1のバリデーション * 入力必須 */ var validateNotes1 = function(oData){ // エラーメッセージのクリア clearErrorMsg(); // MyValidatorをつかって入力必須チェックをする。 var _ret = valObj.validate('ja','isRequired',oData); if(_ret['isRequired']!=null &&_ret['isRequired'].length > 0){ Dom.get('ez_error_res').innerHTML = _ret['isRequired']; return undefined; } return oData; }; /* * クリックイベントで行データを退避する * 「×」マークがクリックされたら行を削除する。 */ var onRowClick = function(oArgs){ // 選ばれた行のデータ var _rowData = this._oAnchorRecord._oData; // 行データを退避 wData = Array(); for(var i in _rowData){ wData[i]=_rowData[i]; } // 選ばれた行の取得 var _rowIdx = this._oAnchorRecord._nCount; // 行を削除する関数 // (注) 削除するとmyDataTable.deleteRow()のインデックスがずれるので補正する。 var _corr = 0; for(var i=0; i<delRow.length; i++){ if(delRow[i]<_rowIdx) _corr++; } var _wk = _rowIdx-_corr; if(onClickDelete(oArgs, _wk)){ // 削除した行の保管 delRow[delNum++] = _rowIdx; // 保管 uValue[wData['areacode']] = ['d',wData]; //debug用 var wk = uValue; } return; }; /* * クリック時に、選択された行を削除する * 削除したらtrue * 削除しなかったらfalse * を返却します。 */ var onClickDelete = function(oArgs, _rowIdx){ // イベントの発生が画面のクリックか判定する。 var _target = Event.getTarget(oArgs.event); var _targetClassName = getClassName(_target); var _idx = _targetClassName.indexOf('actdel',0) if(_idx != -1){ Event.preventDefault(oArgs.event); // oArgsから行を取り出して削除する。 myDataTable.deleteRow(_rowIdx); return true; } return false; }; /* * ダブルクリックイベントでエラーメッセージを消す */ var clearErrorMsg = function(){ Dom.get('ez_error_res').innerHTML = ''; return; }; /* * 編集&(表への一時的)セーブ時にuValueへの保管を行う * (注)値が設定されていない列のデータはoArgsに入らない。 */ var onSave = function(oArgs){ // 初期化 var _u = new Array(); for(var i in columnName){ _u[columnName[i]] = ''; } for(var key in oArgs.editor._oRecord._oData){ _u[key] = String(oArgs.editor._oRecord._oData[key]); } var _id = oArgs.editor._oRecord._oData['areacode']; uValue[_id] = ['u',_u]; return; } /** * ボタンがクリックされたときのハンドラー */ var onButtonClickHdlr = function(_evt,_obj){ /* * サーバーに送る */ // 転送終了確認用フィールドにuValueを退避 uValue.sort(); eValue = uValue; // パラメータの設定 var _url = "test_updatePref.php"; for(var i in uValue){ var _parm = 'o='+uValue[i][0]; for(var j in uValue[i][1]){ _parm += '&' + j + '=' + uValue[i][1][j]; } // ajaxの引数 var _arg ={ 'resId': 'result', 'key' : i }; ajaxCallback.argument = _arg; // サーバーにajaxで送信する YAHOO.util.Connect.asyncRequest('POST',_url, ajaxCallback, _parm); } } /* * Ajaxハンドラー * */ var ajaxHandlers = { // 受信成功時の処理 responseSuccess: function(_oj){ var _resId = _oj.argument.resId; // 結果の表示 if(Dom.get(_resId)==null){ Dom.get(_resId).innerHTML = _oj.responseText; }else{ Dom.get(_resId).innerHTML += '<br>' + _oj.responseText; } // 成功したデータを削除 for(var i=0; i < eValue.length; i++){ if(i == _oj.argument.key){ eValue.splice(i,1); } } // 転送が全て終わったらuValueをリフレッシュする。 if(eValue.length == 0){ uValue = new Array(); eValue = new Array(); } }, // 受信失敗時の処理 responseFailure: function(_oj){ //alert("responseFailure"); var _resId = _oj.argument.resId; var _ret = '<br><span style="color:red;">エラー</span> ステータス: ' + _oj.status + '、ステータステキスト: ' + _oj.statusText + '<br> areacode: ' + _oj.argument.key + 'の送信に失敗しました。<br>' + '<span style="color:red;">システム管理者に連絡してください。' + '<strong>画面をリフレッシュすると編集データが失われます</stong></span><br>'; // alert(_ret); // 結果の表示 if(Dom.get(_resId)==null){ Dom.get(_resId).innerHTML = _ret; }else{ Dom.get(_resId).innerHTML += '<br>' + _ret; } } }; /* * コールバック成功/失敗時の振り分け * */ var ajaxCallback = { success: ajaxHandlers.responseSuccess, failure: ajaxHandlers.responseFailure, cache: false, scope: ajaxHandlers, argument: null }; /***************************************************** * 汎用関数 *****************************************************/ /** * クラス名の取得 */ var getClassName = function(_obj){ if(document.all){ //for IE var _keys = _obj.getAttribute('className'); }else{ // for FF, Chrome, Safari var _keys = _obj.getAttribute('class'); } return _keys; }; return { /** * 初期処理 */ init: function() { // DataTable用:列定義 var myColumnDefs = [ { key:"areacode", parser:"number", label:"コード", width:50, resizeable:true, sortable:true}, { key:"pref", label:"都道府県", width:150, formatter:ezInitFormatter, // 上で定義したフォーマッターを適用する resizeable:true, sortable:true}, { key:"notes1", label:"メモ1 (編集可:必須入力)", editor:new YAHOO.widget.TextboxCellEditor( { validator:validateNotes1, defaultValue:'-' } ), resizeable:true, sortable:true} ]; // DataTable用:コンフィグ属性 var myConfigs = { sortedBy:{key:"areacode",dir:YAHOO.widget.DataTable.CLASS_ASC}, paginator: new YAHOO.widget.Paginator({ rowsPerPage: 10, template: YAHOO.widget.Paginator.TEMPLATE_ROWS_PER_PAGE, rowsPerPageOptions: [10,25,50,100], pageLinks: 5 }), caption:"都道府県", // 列のDrag and Drop draggableColumns:true, // 行の選択は1つだけ selectionMode:"single" }; // DataSourceのインスタンス化 myDataSource = new YAHOO.util.DataSource('data/pref.txt'); myDataSource.responseType = YAHOO.util.DataSource.TYPE_TEXT; myDataSource.responseSchema = { // 行区切り recordDelim: '\n', // フィールド区切り fieldDelim: ',', fields: ['areacode','pref','notes1'] }; // DataTableのインスタンス化 myDataTable = new YAHOO.widget.DataTable("table1", myColumnDefs, myDataSource, myConfigs); /* * 行の選択補助。 */ // クリックでハイライトするようにハンドラを設定 myDataTable.subscribe("rowClickEvent", myDataTable.onEventSelectRow); myDataTable.subscribe("rowMouseoverEvent", myDataTable.onEventHighlightRow); myDataTable.subscribe("rowMouseoutEvent", myDataTable.onEventUnhighlightRow); // クリック時にデータを退避する。 myDataTable.subscribe("rowClickEvent", onRowClick); /* * セルの処理 */ // ダブルクリックでエラーメッセージを消す。 myDataTable.subscribe("cellDblclickEvent", clearErrorMsg); // ダブルクリックでセルのエディターを呼ぶ。 myDataTable.subscribe("cellDblclickEvent", myDataTable.onEventShowCellEditor); // 編集&(表への一時的)セーブ時にuValueへの保管を行う myDataTable.subscribe("editorSaveEvent", onSave); /* * MyValidatorの初期化 */ valObj = new MyValidator(); /* * 変数の初期化 */ delRow = new Array(); delNum = 0; // 列のキーを保管する uValue = new Array(); eValue = new Array(); columnName = new Array(); for(var i=0; i< myColumnDefs.length; i++){ columnName[i] = myColumnDefs[i].key; } // ボタンにハンドラーを仕掛ける。 Event.addListener('button1', 'click', onButtonClickHdlr, Dom.get('button1')); }, // initの終わり oDS: myDataSource, oDT: myDataTable }; }(); </script> </head> <body class=" yui-skin-sam" onload="EzTable.init()"> test_datatable_validate8 <br> <div id="main"> <p> メモ1とメモ2はダブルクリックで変更できます。「メモ1<メモ2」という相関チェックを行います。<br> 都道府県名の右の<img src="../images/action_delete.png" style="margin-bottom:-3px;">をクリックすると、行を削除します。<br> Submitボタンで、表の更新・削除の履歴をサーバーに送信して、./data/pref.txtを更新・削除します。 </p> <div id="ez_error_res" class="ez_error"></div> <div id="table1"></div> <form action="#"> <input id="button1" type="button" value="submit"> </form> <div id="result"></div> </div> </body> </html>