再帰処理でHTMLリスト化しjsTreeでツリー表示

2015/10/07

Javascript

jsTreeはツリーを生成するjQueryのプラグインで、HTMLかXMLかJSONフォーマットのデータを読み込ます前提ですが、業務システムでDBのテーブルにある部品構成(BOM)や部門構成等をツリーで表示する場合には、HTMLリストを動的に生成してツリーに変換させます。

ジャカルタ

インドネシアのITサービス

インターネット技術の急速な発展と普及により、優秀なIT人材を輩出することで知られるジャカルタのビヌス大学(BINUS)やバンドゥンのバンドゥン工科大学、インドネシアコンピューター大学(UNIKOM)の学生の多くがインターネット・WEB業界やソフトウェア業界を志望するようです。

続きを見る

jQuery UIのタブ&パネルウィジェット

生産管理システムでは部品構成表(BOM)の設定が不可欠ですが、親子関係の追加はグリッド表示画面から行うとしても、設定が正しいかどうかのチェックはツリー表示で視覚的に行うことができれば便利です。しかもグリッド表示とツリー表示は画面を切り替えることなくタブの切り替えで素早く実現できればユーザビリティは間違いなく向上します。

これはERPの部門マスタの統括部門とコストセンターの関係を視覚的に表示するのにも使えると思います。

タブ(パネルとセット)を追加するには以下の2つを実装すれば実現できそうです。

  1. タブ&パネルを追加する関数を作成
  2. onClickイベントでタブタイトルと表示させる内容のURLの2つを引数として呼び出す。

肝心の「タブを追加する関数」はjQueryと、jQuery UIにあるTabsウィジェットを呼び出すことで実現できます。jQueryはアプリケーションを操作するJavaScriptのライブラリで、頻繁に使う機能を関数として集約しておりJavaScript上で動きます。

jQUeryとjQuery UIライブラリの呼び出し

まずheadタグの中でjQueryライブラリとjQuery用のUIライブラリ(インターフェースデザイン用ライブラリ)であるjQuery UIを読み込む必要がありますが、jQuery UIはライブラリ本体とスタイルシートを読み込む必要があります。

<head>
//jQuery EasyUI用のスタイルシート
<link rel="stylesheet" type="text/css" href="jquery_easyui/themes/default/easyui.css">
<link rel="stylesheet" type="text/css" href="jquery_easyui/themes/icon.css">
<link rel="stylesheet" type="text/css" href="jquery_easyui/themes/panel.css" />

//jQueryとjQuery EasyUIライブラリ本体
<script type="text/javascript" src="jquery_easyui/jquery.min.js"></script>
<script type="text/javascript" src="jquery_easyui/jquery.easyui.min.js"></script>
</head>

タブとパネルの実装

タブを表示する関数ですが、引数としてタブのタイトルとパネルに表示するコンテンツのURLを、イベントの発生元であるリンクやボタンなどに仕込みます。またTabsウィジェットのclosableオプションにtureをセットすることで、タブが閉じるボタン付きになります。

<script>
function addTab(title, url){
	if ($('#tt').tabs('exists', title)){
		$('#tt').tabs('select', title);
	} else {
		var content = '<iframe scrolling="auto" frameborder="0"  src="'+url+'" style="width:100%;height:100%;"></iframe>';
                //var content = url;
		$('#tt').tabs('add',{
			title:title,
			content:content,
			closable:true
			});
	}
}
</script>

あとはリンク元のonClickイベントにタイトルとURLを引数として仕込みむだけです。

<a href="#" class="easyui-linkbutton" iconCls="icon-user" plain="true" onClick="addTab('Drilldown BOM','jstree/yama.php')">Drilldown BOM</a><br />
<a href="#" class="easyui-linkbutton" iconCls="icon-box-fill" plain="true" onClick="addTab('Integrated Master','jstree/integrated.php')">Integrated Master</a>

出来上がりはこんな感じ。ツリー表示は「再帰処理でHTMLリスト化しjsTreeでツリー表示」で実現し、グリッドもjQuery UIのGridプラグインで実現できます。

jQuery UIでタブを追加

ライブラリとプラグインの違い

業務システムのマスタ管理画面にはCRUD(Create/Retrieve/Update/Delete)機能付きのデータグリッドが便利なのですが、一番シンプルなのはjQueryとJQuery Easy UIのみで実現するDataGridです。

ツリー表示で使ったjsTreeはjQueryのプラグインであり<script>タグの中に別途jstreeプラグインを読み込む必要がありましたが、DataGridはTabsなどと同じようにJQuery UIのウィジェットにあたります。

ライブラリとプラグインの違いは汎用的か特定ソフト用かなのでjQuery UIはライブラリでもありjQuery用のプラグインでもあると言えると思います。またWordPressのウィジェットがサイドバーへの機能追加に特化したプラグインなのに対し、jQuery UIのウィジェットは言葉の使われ方が若干異なります。

ということでちょっと言葉の整理をしておきたいのですが・・・

  1. ライブラリは汎用的関数の集合体
  2. プラグインは特定ソフト用拡張プログラム
  3. ウィジェットはライブラリ(プラグイン)が提供するコントロール(部品)
  4. jQueryはJavaScriptでよく使われる機能を簡単に呼び出せるようにしたライブラリ
  5. AjaxはJavaScriptの非同期通信に焦点をあてた概念

DataGridがあるにもかかわらず多くのメーカーがデータグリッド用のライブラリ(プラグイン)を出している理由は以下の2つで差別化を図りたいからです。

  1. 見栄え > 見栄えの良いRIA(Rich Internet Applications)技術
  2. スピード > 時間のかかる処理を非同期実行(Ajax利用)

jQuery UIの基本DataGrid実装方法

それで今回はDataGridを使うのですが、ざっくり以下の3つを実装すれば実現できそうです。

  1. HTMLテーブルにグリッド装飾class要素とCRUDアクションのid要素
  2. 親子関係からデータを取得する再帰処理関数を作成
  3. CRUDアクション時のダイアログを定義し新規・更新・削除ボタンにマッピング

まずタブ&パネルと同じようにheadタグの中でjQueryライブラリとjQuery用のUIライブラリ(インターフェースデザイン用ライブラリ)であるjQuery UIを読み込む必要がありますが、jQuery UIはライブラリ本体とスタイルシートを読み込む必要があります。

<head>
//jQuery EasyUI用のスタイルシート
<link rel="stylesheet" type="text/css" href="jquery_easyui/themes/default/easyui.css">
<link rel="stylesheet" type="text/css" href="jquery_easyui/themes/icon.css">
<link rel="stylesheet" type="text/css" href="jquery_easyui/themes/style.css" />

//jQueryとjQuery EasyUIライブラリ本体
<script type="text/javascript" src="jquery_easyui/jquery.min.js"></script>
<script type="text/javascript" src="jquery_easyui/jquery.easyui.min.js"></script>
</head>

データグリッド画面はid要素dgで定義し、HTMLテーブルをデータグリッド化するためのclass要素easyui-datagridを指定し、データは再帰関数で取得しレコードセットの値を表に表示します。

//初期表示画面
<table id="dg" title="" class="easyui-datagrid" style="height:auto;" toolbar="#toolbar" pagination="true" rownumbers="true" fitColumns="false" singleSelect="true">
	<thead>
		<tr>
			<th field="P_ITM_CD">Parent Item Code</th>
			<th field="IMaster_ProcNo">Proc No</th>
	              	<th field="IMaster_ProcCode">Proc Code</th>
	              	<th field="IMaster_InstructionType">Instruction Type</th>
	              	<th field="IMaster_InstructionCode">Instruction Code</th>
	              	<th field="C_ITM_CD">Child Item Code</th>
			<th field="IMaster_Task2Expr">Production</th>
		</tr>
	</thead>
	<tbody>
		<?php
		if($_GET['node'] != ""){
			while ($row = mysql_fetch_array($result, MYSQL_ASSOC)) {
				$main[] = $row["P_ITM_CD"];
			}
			if(isset($main)){
				foreach($main as $main_item){
					echo getChild($koneksi, $main_item, $table);
				}
			}
		}else{
			while ($row = mysql_fetch_array($result, MYSQL_ASSOC)) {
				echo "<tr>";
				echo "<td>".$row["P_ITM_CD"]."</td>";
				echo "<td>".$row["IMaster_ProcNo"]."</td>";
				echo "<td>".$row["IMaster_ProcCode"]."</td>";
				echo "<td>".$row["IMaster_InstructionType"]."</td>";
				echo "<td>".$row["IMaster_InstructionCode"]."</td>";
				echo "<td>".$row["C_ITM_CD"]."</td>";
				echo "<td>".$row["IMaster_Task2Expr"]."</td>";
				echo "</tr>";
			}
		}
		?>
	</tbody>
</table>

<div id="toolbar">
 <a href="#" class="easyui-linkbutton" iconCls="icon-add" plain="true" onclick="newData()">新規</a>
 <a href="#" class="easyui-linkbutton" iconCls="icon-edit" plain="true" onclick="editData()">更新</a>
 <a href="#" class="easyui-linkbutton" iconCls="icon-remove" plain="true" onclick="removeData()">削除</a>
 </div>

 

DBから再帰処理で親子関係のデータを取得しDataGridに流す処理

ツリー表示でやった再帰処理のテーブルバージョンです。やってることは同じで、子品目(C_ITM_CD)にマッチする親品目(P_ITM_CD)があればデータを表示し、再帰的に自関数を読む処理をマッチする親品目がなくなる(最下層品目)まで繰り返します。

function getChild($con, $item, $table){
	$sql = "select * from $table where P_ITM_CD='".$item."' and IMaster_InstructionType<>'U'";
	$result = mysql_query($sql, $con);
	$num_rows = mysql_num_rows($result);
	
	if($num_rows > 0){
		//子品目ありの場合
		unset($sem);
		while ($row = mysql_fetch_array($result, MYSQL_ASSOC)) {
			if($row["C_ITM_CD"] != ""){
				$sem[] = $row["C_ITM_CD"];
			}
		}
		
		$str = "";
		foreach($sem as $sem_item){
			$test = getChild($con, $sem_item, $table);
			if( $test == ""){ 
				$sql_detil = "select IMaster_ProcNo, IMaster_ProcCode, IMaster_InstructionCode, IMaster_Task2Expr, IMaster_InstructionType from  $table where P_ITM_CD='".$item."' and C_ITM_CD='".$sem_item."'";
				$result_detil = mysql_query($sql_detil, $con);
				$row_detil = mysql_fetch_array($result_detil, MYSQL_ASSOC);
				
				$str .= "<tr>";
				$str .= "<td>".$item."</td>";
				$str .= "<td>".$row_detil["IMaster_ProcNo"]."</td>";
				$str .= "<td>".$row_detil["IMaster_ProcCode"]."</td>";
				$str .= "<td>".$row_detil["IMaster_InstructionType"]."</td>";
				$str .= "<td>".$row_detil["IMaster_InstructionCode"]."</td>";
				$str .= "<td>".$sem_item."</td>";
				$str .= "<td>".$row_detil["IMaster_Task2Expr"]."</td>";
				$str .= "</tr>";
				
				$sql_blue = "select * from $table where P_ITM_CD='".$item."' and IMaster_InstructionType='U'";
				$result_blue = mysql_query($sql_blue, $con);
				$num_rows_blue = mysql_num_rows($result_blue);
				if($num_rows_blue > 0){
					while ($row_blue = mysql_fetch_array($result_blue, MYSQL_ASSOC)) {
						$str .= "<tr>";
						$str .= "<td>".$row_blue["P_ITM_CD"]."</td>";
						$str .= "<td>".$row_blue["IMaster_ProcNo"]."</td>";
						$str .= "<td>".$row_blue["IMaster_ProcCode"]."</td>";
						$str .= "<td>".$row_blue["IMaster_InstructionType"]."</td>";
						$str .= "<td>".$row_blue["IMaster_InstructionCode"]."</td>";
						$str .= "<td>".$row_blue["C_ITM_CD"]."</td>";
						$str .= "<td>".$row_blue["IMaster_Task2Expr"]."</td>";
						$str .= "</tr>";
					}
				}
			}else{
				$sql_detil = "select IMaster_ProcNo, IMaster_ProcCode, IMaster_InstructionCode, IMaster_Task2Expr, IMaster_InstructionType from  $table where P_ITM_CD='".$item."' and C_ITM_CD='".$sem_item."'";
				$result_detil = mysql_query($sql_detil, $con);
				$row_detil = mysql_fetch_array($result_detil, MYSQL_ASSOC);
				
				$str .= "<tr>";
				$str .= "<td>".$item."</td>";
				$str .= "<td>".$row_detil["IMaster_ProcNo"]."</td>";
				$str .= "<td>".$row_detil["IMaster_ProcCode"]."</td>";
				$str .= "<td>".$row_detil["IMaster_InstructionType"]."</td>";
				$str .= "<td>".$row_detil["IMaster_InstructionCode"]."</td>";
				$str .= "<td>".$sem_item."</td>";
				$str .= "<td>".$row_detil["IMaster_Task2Expr"]."</td>";
				$str .= "</tr>";
				$str .= $test;
				
				$sql_blue = "select * from $table where P_ITM_CD='".$item."' and IMaster_InstructionType='U'";
				$result_blue = mysql_query($sql_blue, $con);
				$num_rows_blue = mysql_num_rows($result_blue);
				if($num_rows_blue > 0){
					while ($row_blue = mysql_fetch_array($result_blue, MYSQL_ASSOC)) {
						$str .= "<tr>";
						$str .= "<td>".$row_blue["P_ITM_CD"]."</td>";
						$str .= "<td>".$row_blue["IMaster_ProcNo"]."</td>";
						$str .= "<td>".$row_blue["IMaster_ProcCode"]."</td>";
						$str .= "<td>".$row_blue["IMaster_InstructionType"]."</td>";
						$str .= "<td>".$row_blue["IMaster_InstructionCode"]."</td>";
						$str .= "<td>".$row_blue["C_ITM_CD"]."</td>";
						$str .= "<td>".$row_blue["IMaster_Task2Expr"]."</td>";
						$str .= "</tr>";
					}
				}
			}
		}
		
		unset($row);
		return $str;
		
	}else{
		//子品目なしの場合
		return "";
	}
}

関数を定義しidセレクタでダイアログを取得し、datagridメソッドで選択行に対して処理を行います。

<script>
//新規
function newData(){
	$('#dlg').dialog('open').dialog('setTitle','Tambah Data');
	$('#fm').form('clear');
	$('#id_user').removeAttr('readonly','readonly');
	url = 'proses.php?cmd=newrow';
}

//修正
function editData(){
	var row = $('#dg').datagrid('getSelected');
	if (row){
		$('#dlg').dialog('open').dialog('setTitle','Edit Data');
		$('#fm').form('load',row);
		$('#id_user').attr('readonly','readonly');
		url = 'proses.php?cmd=updaterow';
		document.getElementById('hdn_P_ITM_CD').value = document.getElementById('P_ITM_CD').value;
		document.getElementById('hdn_IMaster_InstructionType').value = document.getElementById('IMaster_InstructionType').value;
		document.getElementById('hdn_C_ITM_CD').value = document.getElementById('C_ITM_CD').value;
	}
}

//削除
function removeData(){
	var row = $('#dg').datagrid('getSelected');
	if (row){
		$.messager.confirm('Confirm','Anda yakin akan menghapus data ini?',function(r){
			if (r){
				$.post('proses.php?cmd=deleterow',{
						P_ITM_CD:row.P_ITM_CD,
						IMaster_ProcNo:row.IMaster_ProcNo,
						IMaster_ProcCode:row.IMaster_ProcCode,
						IMaster_InstructionType:row.IMaster_InstructionType,
						IMaster_InstructionCode:row.IMaster_InstructionCode,
						C_ITM_CD:row.C_ITM_CD,
						IMaster_Task2Expr:row.IMaster_Task2Expr
					},function(result){
					if (result.success){
						$('#dg').datagrid('reload');	// reload the user data
						location.reload();
					} else {
						$.messager.show({	// show error message
							title: 'Error',
							msg: result.msg
						});
					}
				},'json');
			}
		});
	}
}

//保存
function saveData(){
	$('#fm').form('submit',{
		url: url,
		onSubmit: function(){
			return $(this).form('validate');
		},
		success: function(result){
			var result = eval('('+result+')');
			if (result.success){
				$('#dlg').dialog('close');		// close the dialog
				$('#dg').datagrid('reload');	// reload the user data
						location.reload();
			} else {
				$.messager.show({
					title: 'Error',
					msg: result.msg
				});
			}
		}
	});
			
}

document.getElementById("msg").innerHTML = "";
</script>

新規・更新・削除用のダイアログはid要素dlgで定義し、それぞれの関数にマッピングする。

//新規・修正・削除ポップアップ
<div id="dlg" class="easyui-dialog" style="width:400px;height:280px;padding:10px 20px" closed="true" buttons="#dlg-buttons">
<div class="ftitle">Informasi Record</div>
	<form id="fm" method="post" novalidate>
		<input type="hidden" name="hdn_P_ITM_CD" id="hdn_P_ITM_CD">
		<input type="hidden" name="hdn_IMaster_InstructionType" id="hdn_IMaster_InstructionType">
		<input type="hidden" name="hdn_C_ITM_CD" id="hdn_C_ITM_CD">
		<div class="fitem">
		<table>
			<tr>
				<td>Final Item Code:</td>
				<td><input name="P_ITM_CD" id="P_ITM_CD" class="easyui-validatebox" required="true"></td>
			</tr>
			<tr>
				<td>Proc No:</td>
				<td><input name="IMaster_ProcNo" id="IMaster_ProcNo" class="easyui-validatebox" required="true"></td>
			</tr>
			<tr>
				<td>Proc Code:</td>
				<td><input name="IMaster_ProcCode" id="IMaster_ProcCode" class="easyui-validatebox" required="true"></td>
			</tr>
			<tr>
				<td>Instruction Type:</td>
				<td><input name="IMaster_InstructionType" id="IMaster_InstructionType" class="easyui-validatebox" required="true"></td>
			</tr>
			<tr>
				<td>Instruction Code:</td>
				<td><input name="IMaster_InstructionCode" id="IMaster_InstructionCode" class="easyui-validatebox" required="true"></td>
			</tr>
			<tr>
				<td>Item Or Resource:</td>
				<td><input name="C_ITM_CD" id="C_ITM_CD" class="easyui-validatebox" required="true"></td>
			</tr>
			<tr>
				<td>Production:</td>
				<td><input name="IMaster_Task2Expr" id="IMaster_Task2Expr" class="easyui-validatebox"></td>
			</tr>
		</table>
		</div>
	</form>
</div>

<div id="dlg-buttons">
	<a href="#" class="easyui-linkbutton" iconCls="icon-ok" onclick="saveData()">Save</a>
	<a href="#" class="easyui-linkbutton" iconCls="icon-cancel" onclick="javascript:$('#dlg').dialog('close')">Cancel</a>
</div>

HTMLリストの構造

HTMLリストは<ul>で開始してリストの各アイテムを<li>と</li>で囲んで列挙し</ul>で終了します。当然以下のように入れ子に出来ますので、再帰処理でDBのテーブルから動的に入れ子のHTMLリストを生成することになります。

<ul>
  <li>node1_1
    <ul>
      <li>node2_1</li>
      <li>node2_2</li>
    </ul>
  </li>
  <li>node1_2</li>
</ul>

このHTMLリストをjsTreeでツリー表示するには、HTMLリストをdiv要素のidセレクタ指定で囲み、jQuery関数からidセレクタでHTMLリストを探して、jstreeメソッドでツリー表示します。

//HTMLリストをdiv要素で囲む

<div id="html">

<ul>
  <li>node1_1
    <ul>
      <li>node2_1</li>
      <li>node2_2</li>
    </ul>
  </li>
  <li>node1_2</li>
</ul>

</div>


// idセレクタでHTMLリストを取得しjstreeメソッドでツリー表示
$('#html').jstree({

で、今回やろうとしていることは以下のような部品構成の親子関係をHTMLリストに生成してjsTreeメソッドでツリー表示することです。

再帰処理でHTMLリスト化しjsTreeでツリー表示

上記のテーブルの親子関係からこんな感じでツリー表示します。
再帰処理でHTMLリスト化しjsTreeでツリー表示

jQueryとjsTreeの呼び出し

Headタグ内にjsTreeのテーマテンプレートとjQuery関数とjsTreeプラグインの3つを読み込み、

<head>
	 <link rel="stylesheet" href="themes/default/style.min.css" />
     <script src="jquery.js"></script>
         <script src="jstree.min.js"></script>
</head>

ツリーのソース用DB(BOMの親子関係を定義するテーブル)のパラメータ定義。

$host = "localhost";
$user = "root";
$password = "password";
$db = "asp";
$table = "asp_integrated";

HTMLリストをdiv要素のidセレクタ(html)で囲み、ツリーの根っこ部分をRootとして定義。

<div id="html">
<ul>
    <li>Root
        <ul>

再帰処理関数による親子レコード取得とツリー実装の流れ

以上が前準備で、ここから処理が始まります。
1.子品目データが存在すればマッチする親品目のレコードセットを配列$mainに格納。
2.子品目データが存在しなければそのまま親品目のレコードセットを配列$mainに格納。

$con = mysql_connect($host, $user, $password);
$db_selected = mysql_select_db($db, $con);
$sql = "SELECT C_ITM_CD from $table";
$result = mysql_query($sql);
		
if(isset($_GET['node']) and $_GET['node']<>""){
	$sql = "select P_ITM_CD from $table where P_ITM_CD='".$_GET['node']."'";

}else{
	$sql = "select P_ITM_CD from $table";
			
}
		
$result = mysql_query($sql);
		
while ($row = mysql_fetch_array($result, MYSQL_ASSOC)) {
	$main[] = $row["P_ITM_CD"];
}

配列$mainから要素(親品目のレコード)を1個ずつ取り出して再帰処理関数getChild()に渡す。

if(isset($main)){
	foreach($main as $main_item){
					
		echo "<li>".$main_item;
		echo getChild($con, $main_item, $table)."</li>\n";
	}
}
		
mysql_close($con);

この再帰処理関数が難しいのですがポイントです。

function getChild($con, $item, $table){
	
	$sql = "select * from $table where P_ITM_CD='".$item."'";
	$result = mysql_query($sql, $con);
	$num_rows = mysql_num_rows($result);
	
	if($num_rows > 0){
		//子品目ありの場合
		unset($sem);
		while ($row = mysql_fetch_array($result, MYSQL_ASSOC)) {
			if($row["C_ITM_CD"] != ""){
				$sem[] = $row["C_ITM_CD"];
			}
		}
		$str = "";
		foreach($sem as $sem_item){
			$child = getChild($con, $sem_item, $table);
			if( $child == ""){ 					
				$str .= "<ul><li>".$sem_item."</li></ul>\n";
			}else{
				$str .= "<ul><li>".$sem_item."$child</li></ul>\n";
			}
		}
		return $str;
	}else{
		//子品目なしの場合
	}
}

上記までで生成した再帰HTMLリストをツリービューで表示します。まずjstreeメソッドでツリー表示のインスタンスを生成する部分ですが、基本JavaScriptですので<script>タグ内に記述します。

jstreeメソッドのオプションとしてcoreとdndとpluginsの3つを指定しています。

<script>
//jstreeメソッドでツリービューのインスタンス作成
$('#html')
	.jstree({
		"core" : {
			'check_callback' : function (operation, node, node_parent, node_position, more) {
				node_dest = node_parent["text"].replace(regex, "").trim();
				var res = node_dest.split("[");

				if(res[0] == "Root"){
					return false;
					node_dest = "";
				}else{
					node_dest = res[0];
					return true;
				}
			}
		},

		"dnd" : {
	        },

        	"plugins" : [ "themes", "html_data", "dnd" ]
	});

ノードの各種情報をuiGetParents()関数で取得しています。

function uiGetParents(loSelectedNode) {
        try {
            var lnLevel = loSelectedNode.node.parents.length;
            var lsSelectedID = loSelectedNode.node.id;
            var loParent = $("#" + lsSelectedID);
            var lsParents =  loSelectedNode.node.text + ' >';
            for (var ln = 0; ln <= lnLevel -1 ; ln++) {
                var loParent = loParent.parent().parent();
                if (loParent.children()[1] != undefined) {
                    lsParents += loParent.children()[1].text + " > ";
                }
            }
            if (lsParents.length > 0) {
                lsParents = lsParents.substring(0, lsParents.length - 1);
            }
			str_parent = lsParents.replace(/(<([^>]+)>)/ig,"");
        }
        catch (err) {
            alert('Error in uiGetParents');
        }
    }
	
</script>