YUI2.7.0のEditable Table(編集可能な表)で編集したデータをサーバーに送信する(その2)

前回のログでは、YUI2.7.0のEditable Table(編集可能な表)について、更新・削除時に(DataTableオブジェクト内に)一時保管されるデータを、配列に蓄積するスクリプトを書いた。

今回は、それを発展させて、その内容をサーバーに送るコードを書いてみる。この際、

  • 配列に蓄積されたデータは、Ajax(XHR)によって非同期、かつ、連続的におくる

ことにする。

これらの一連の仕様では、「データの送信に問題があった場合」の扱いが問題になる。連続的にAjaxでデータを送ると、回線状況やサーバーの状況によっては、送れたデータと送れなかったデータが混在してしまう可能性がある。DataTable内に蓄積されるデータは、一時的なものであるので、ブラウザーをリフレッシュしてしまうと、編集したデータをロストしてしまう。
したがって、回線やネットワーク機器が不安定な場合や帯域が圧迫されている場合、Httpサーバーの負荷が高い場合などでは、方針を変更して、「更新や削除が発生するたびにサーバーに送る」といった仕様の方がいいだろう。

今回は、上記のような制約は仮定せず、「編集データを蓄積して、ボタンで確定(=永続化)させる」という方針であるが、以下の方針をとる。

  • データの送信に失敗した場合、そのデータを廃棄せず、再度の送信をエラーメッセージによって促す。

実際の利用局面では、この仕様では不十分かもしれない。ケースバイケース(利用環境やデータの性質など)で

  • 自動的に再送を行う(…これはよくないだろうなぁ)
  • エラーになったデータを保管する仕組みを用意する

といったことを考慮すればいいと思う。

前置きがながくなったが、以下が初期画面のスナップショット。

これを、以下に示す「POST/GETされたデータを、表形式にしてクライアントに送る」PHPプログラムに私、表の下に表示することとした。このPHPプログラムは、以前のログで使用したものと同じである。

<?php
/* クライアントからのajax送信を受け取るサンプル
	      
		author	; t.odaka
		date	; 2009/4/22
*/
	require("../Myznala/debugLog.php");
	$m_log->debug('test_setPostForm start'); // Debugログ
	
	switch($_SERVER['REQUEST_METHOD']) {
		case 'GET'	: $rMethod = &$_GET; break;
		case 'POST'	: $rMethod = &$_POST; break;
		default:
	}

	// パラメータをサニタイズして配列に入れる。
	$reqParm;
	foreach ($rMethod as $key => $value) {
		$key	=sanitize($key,"UTF-8");
		$value	=sanitize($value,"UTF-8");
		$m_log->debug($key.'; '.$value); // Debugログ
		$reqParm[$key]=$value;
	}
	
	// 戻すデータを作成する。
	$ret='<table border="1"><tr><th>key</th><th>value</th></tr>';
	foreach ($rMethod as $key => $value) {
		$ret.='<tr><td>'.$key.'</td><td>'.$value.'</td></tr>';
	}
	$ret.='</table>';
	
	// 出力
	header("Content-Type:text/html");
	echo($ret);

	// 入力データのサニタイズを行います //
	function sanitize($var,$encoding){
		$ret = htmlentities($var,ENT_QUOTES,$encoding);
		return $ret;
	}

?>


また、Ajaxのエラー発生時には、(これも)表下に表示することとした。以下は

  1. 1つのセル(areacode="01")を変更
  2. Submitボタンを押し、サーバーに送信
  3. もう1つのセル(areacode="03")を変更
  4. HTTPサーバーの停止(Ajaxを失敗させるため)
  5. Submitボタンを押し、サーバーに送信(エラーとなる)
  6. サーバーを起動
  7. 再度、Submitボタンを押し、サーバーに送信

とした場合の結果である。エラー後の再送信がうまくいっている。


以下が、HTMLの全文である。

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> 
<html> 
<head> 
    <meta http-equiv="content-type" content="text/html; charset=utf-8"> 
<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/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" src="data.js"></script> 
<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 + '&nbsp' + '&nbsp' +
    				'<a class="actdel" href="#">' 
    				+ '<img class="actdel" src="../images/action_delete.png">' + '</a>';
	};
	
	/* 
	* メモ2のバリデーション
	*/
	var validateNotes2 = function(oData){

		// エラーメッセージのクリア
		clearErrorMsg();
		
		// MyValidatorをつかって数値チェックをする。
		var _ret = valObj.validate('ja','isNum',oData);
		if(_ret['isNum']!=null &&_ret['isNum'].length > 0){
		    Dom.get('ez_error_res').innerHTML = _ret['isNum'];
                    return undefined;
		}

		/*
		* 以下、相関チェック
		*/
		// 数値化
		var _numData = 1 * oData;
		if("notes1" in wData){
			var _savData = 1 * wData["notes1"];
		}else{
			var _savData = 0;
		}
		
		// 相関チェック
		if(_savData >= _numData){
		    // エラーの場合、undefindを返せばいい。
		    Dom.get('ez_error_res').innerHTML = 'メモ2>メモ1でなければなりません。';
                    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 _data = 'レコード ';
		// 初期化
		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];
		//debug用
		var wk = uValue;
		
		return;
	}
	
	/**
	* ボタンがクリックされたときのハンドラー
	*/
	var onButtonClickHdlr = function(_evt,_obj){
		/* 
		* サーバーに送る
		*/
		// 転送終了確認用フィールドにuValueを退避
		uValue.sort();
		eValue = uValue;
		// パラメータの設定
		var _url = "test_SetPostForm.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){
			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",
                     	label:"コード",
                     	width:50,
                     	resizeable:true,
                     	sortable:true},
                    {	key:"state",
                        label:"都道府県",
                        width:150,
                        formatter:ezInitFormatter, // 上で定義したフォーマッターを適用する
                        resizeable:true,
                        sortable:true},
                    {	key:"notes1",
                        label:"メモ1 (編集可:数値)",
                        editor:new YAHOO.widget.TextboxCellEditor(
   	               		{	
                                        validator:YAHOO.widget.DataTable.validateNumber,
     					defaultValue:0
    	                	}
						),
                        resizeable:true,
                        sortable:true},
                   {	key:"notes2",
                        label:"メモ 2(編集可:数値)",
                        editor:new YAHOO.widget.TextboxCellEditor(
       	               		{	
    					validator:validateNotes2,
         				defaultValue:0
        	                }
   						),
                        resizeable:true,
                        sortable:true}
        	];

        	// DataTable用:コンフィグ属性
        	var myConfigs = {
                sortedBy:{key:"areacode",dir:"asc"},
                paginator: 
                    new YAHOO.widget.Paginator({
                    	rowsPerPage: 10,
                    	template: YAHOO.widget.Paginator.TEMPLATE_ROWS_PER_PAGE,
                    	rowsPerPageOptions: [10,25,50,100],
                    	pageLinks: 5
                	}),
               	caption:"都道府県"
        	};

        	// DataSourceのインスタンス化
	        myDataSource = new YAHOO.util.DataSource(Data.areacodes);
    	        myDataSource.responseType = YAHOO.util.DataSource.TYPE_JSARRAY;
        	myDataSource.responseSchema = {
            	    fields: ["areacode","state"]
        	};

	        // 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();
		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_validate7
<br>
<div id="main">
<p>
メモ1とメモ2はダブルクリックで変更できます。「メモ1<メモ2」という相関チェックを行います。<br>
都道府県名の右の<img src="../images/action_delete.png" style="margin-bottom:-3px;">をクリックすると、行を削除します。<br>
Submitボタンで、表の更新・削除の履歴をサーバーに送信し、結果を表下に表示します。
</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>