Full Ajax な CRUD アプリケーション その4

前回でデータ一覧を表示させる事が出来るようになりました。今回はデータの更新関係についてサンプルを作っていきます。

新規追加ウィンドウの作成

まずはモックアップから。ExtJSのダイナミックフォームを使用します。

フォームウィンドウのソース
	Ext.QuickTips.init();

	Ext.form.Field.prototype.msgTarget = 'side';

	var FormWindow = function(){
		var extWin = null;
		this.show = function(title, url){
			if (extWin == null) {
				var form = new Ext.FormPanel({
			    	method: 'POST',
					id: 'employee-form',
					baseCls: 'x-plain',
					labelWidth: 40, 
					defaults: {width: 190},
					defaultType: 'textfield',
					bodyStyle:'padding:5px 10px 0',
					items: [{
						fieldLabel: 'Id',
						id: 'id',
		    			xtype: 'numberfield'
					},{
						fieldLabel: '氏名',
						id: 'name',
						allowBlank: false
					},{
						fieldLabel: '職種',
						id: 'jobType'
					}, {
						fieldLabel: '給料',
						id: 'salary',
		    			xtype: 'numberfield'
					}, {
						fieldLabel: '部署',
						id: 'department'
					}]
				});
				extWin = new Ext.Window({
					title: '会社員情報 : ' + title,
					width: 300,
					height:220,
					minWidth: 300,
					minHeight: 250,
					layout: 'fit',
					closeAction :'hide',
					plain:true,
					bodyStyle:'padding:5px;',
					buttonAlign:'center',
					items: form,
					buttons: [{
						text: 'OK'
					},{
						text: 'キャンセル'
					}]
				});
			}
			extWin.show();
		}
	};
	var formWindow = new FormWindow();

Ext.Window()でウィンドウを作成、 Ext.FormPanel()でフォームを作成しウィンドウ内にセットします。これをFormWindow()というファンクションにしました。

イベントハンドラーのソース
	var addActionHandler = function(){
		formWindow.show('新規追加', './create');
		Ext.getDom('id').readOnly = true;
	}

ツールバーの追加ボタンに割り当てるハンドラーです。ウインドウのタイトルとフォームアクションを指定しています。

イベントハンドラーを割り当てる
	var addAction = new Ext.Action({
		text: '追加',
		iconCls: 'addIcon',
		handler: addActionHandler
	});

ツールバーにaddActionHandlerを割り当てました。

employee.js - フォームウィンドウを追加
Ext.onReady(function(){

	// HANDLER

	var addActionHandler = function(){
		formWindow.show('新規追加', './create');
		Ext.getDom('id').readOnly = true;
	}

	// MODEL, CONTROL

	var fields = [ 'id', 'name', 'jobType', 'salary', 'department' ];

	var store = new Ext.data.Store({
		proxy: new Ext.data.HttpProxy({
			url: './employee.json',
			method: 'GET'
		}),
		reader: new Ext.data.JsonReader({
		    root: 'root',
		    totalProperty: 'totalProperty',
			fields: fields
		})
	});

	// VIEW

	Ext.QuickTips.init();

	Ext.form.Field.prototype.msgTarget = 'side';

	var FormWindow = function(){
		var extWin = null;
		this.show = function(title, url){
			if (extWin == null) {
				var form = new Ext.FormPanel({
			    	method: 'POST',
					id: 'employee-form',
					baseCls: 'x-plain',
					labelWidth: 40, 
					defaults: {width: 190},
					defaultType: 'textfield',
					bodyStyle:'padding:5px 10px 0',
					items: [{
						fieldLabel: 'Id',
						id: 'id',
		    			xtype: 'numberfield'
					},{
						fieldLabel: '氏名',
						id: 'name',
						allowBlank: false
					},{
						fieldLabel: '職種',
						id: 'jobType'
					}, {
						fieldLabel: '給料',
						id: 'salary',
		    			xtype: 'numberfield'
					}, {
						fieldLabel: '部署',
						id: 'department'
					}]
				});
				extWin = new Ext.Window({
					title: '会社員情報 : ' + title,
					width: 300,
					height:220,
					minWidth: 300,
					minHeight: 250,
					layout: 'fit',
					closeAction :'hide',
					plain:true,
					bodyStyle:'padding:5px;',
					buttonAlign:'center',
					items: form,
					buttons: [{
						text: 'OK'
					},{
						text: 'キャンセル'
					}]
				});
			}
			extWin.show();
		}
	};
	var formWindow = new FormWindow();

	var colModel = new Ext.grid.ColumnModel([
		{header: "Id", width: 75, sortable: true, dataIndex: 'id'},
		{id:'name', header: "氏名", width: 160, sortable: true, dataIndex: 'name'},
		{header: "職種", width: 75, sortable: true, dataIndex: 'jobType'},
		{header: "給与", width: 75, sortable: true, dataIndex: 'salary'},
		{header: "部署", width: 85, sortable: true, dataIndex: 'department'}
	]);

	var addAction = new Ext.Action({
		text: '追加',
		iconCls: 'addIcon',
		handler: addActionHandler
	});

	var editAction = new Ext.Action({
		text: '編集',
		iconCls: 'editIcon'
	});

	var deleteAction = new Ext.Action({
		text: '削除',
		iconCls: 'deleteIcon'
	});

	var findAction = new Ext.Action({
		text: '検索',
		iconCls: 'findIcon'
	});

	var tbar = [
		addAction,
		'-',
		editAction,
		deleteAction,
		'-',
		findAction
	];

	var pageSize = 20;

	var bbar = new Ext.PagingToolbar({
		id: 'pagingToolbar',
		pageSize: pageSize,
		store: store,
		displayInfo: true,
		displayMsg: '社員の一覧 {2} 件中 {0} - {1} 件目',
		emptyMsg: "社員の一覧はありません"
	});

	var grid = new Ext.grid.GridPanel({
		title:'社員管理',
		stripeRows: true,
		autoExpandColumn: 'name',
		height:523,
		width:600,
		store: store,
		colModel: colModel,
		tbar: tbar,
		bbar: bbar
	});

	// INIT
	
	grid.render('grid-employee');

	store.load({params:{start:0, limit:pageSize}});

});

ブラウザからhttp://localhost:8080/ajax-app/employee/index.htmlを開き追加ボタンをクリックすると以下のようにフォームウィンドウが表示されます。

次はフォームのボタンにハンドラーをセットします。まずはOKボタンから。

OKボタンのハンドラー
	text: 'OK',
	handler: function(){
		form.getForm().submit({
			url: url,
			waitMsg: 'サブミットしています...', // Submit時に表示するメッセージ。二重サブミット防止にもなる。
			success: function(form, action) {
				Ext.select('.x-tbar-loading').item(0).dom.click();
				extWin.close();
				extWin = null;
			},
			failure: function(form, action) {
   					if (action.failureType == 'client') {
   						// do nothing
   					} else if (action.failureType == 'connect') {
					Ext.MessageBox.alert('コネクションエラー', '通信時にエラーが発生しました。');
				} else {
					Ext.MessageBox.alert('失敗', title + 'できませんでした。');
					Ext.select('.x-tbar-loading').item(0).dom.click();
					extWin.close();
					extWin = null;
				}
			}
		});
	}

urlはFormWindow.show(title, url)のurlです。この様にボタン毎にフォームアクションの指定が出来ます。

Ext.select('.x-tbar-loading').item(0).dom.click(); は裏技的なツールバーのリフレッシュボタンクリックイベントです。

キャンセルボタンのハンドラー
	text: 'キャンセル',
	handler: function(){
		form.getForm().reset();
		extWin.close();
		extWin = null;
	}
employee.js - OKボタンとキャンセルボタンのハンドラーをセット
Ext.onReady(function(){

	// HANDLER

	var addActionHandler = function(){
		formWindow.show('新規追加', './create');
		Ext.getDom('id').readOnly = true;
	}

	// MODEL, CONTROL

	var fields = [ 'id', 'name', 'jobType', 'salary', 'department' ];

	var store = new Ext.data.Store({
		proxy: new Ext.data.HttpProxy({
			url: './employee.json',
			method: 'GET'
		}),
		reader: new Ext.data.JsonReader({
		    root: 'root',
		    totalProperty: 'totalProperty',
			fields: fields
		})
	});

	// VIEW

	Ext.QuickTips.init();

	Ext.form.Field.prototype.msgTarget = 'side';

	var FormWindow = function(){
		var extWin = null;
		this.show = function(title, url){
			if (extWin == null) {
				var form = new Ext.FormPanel({
			    	method: 'POST',
					id: 'employee-form',
					baseCls: 'x-plain',
					labelWidth: 40, 
					defaults: {width: 190},
					defaultType: 'textfield',
					bodyStyle:'padding:5px 10px 0',
					items: [{
						fieldLabel: 'Id',
						id: 'id',
		    			xtype: 'numberfield'
					},{
						fieldLabel: '氏名',
						id: 'name',
						allowBlank: false
					},{
						fieldLabel: '職種',
						id: 'jobType'
					}, {
						fieldLabel: '給料',
						id: 'salary',
		    			xtype: 'numberfield'
					}, {
						fieldLabel: '部署',
						id: 'department'
					}]
				});
				extWin = new Ext.Window({
					title: '会社員情報 : ' + title,
					width: 300,
					height:220,
					minWidth: 300,
					minHeight: 250,
					layout: 'fit',
					closeAction :'hide',
					plain:true,
					bodyStyle:'padding:5px;',
					buttonAlign:'center',
					items: form,
					buttons: [{
						text: 'OK',
						handler: function(){
							form.getForm().submit({
								url: url,
								waitMsg: 'サブミットしています...', // Submit時に表示するメッセージ。二重サブミット防止にもなる。
								success: function(form, action) {
									Ext.select('.x-tbar-loading').item(0).dom.click();
									extWin.close();
									extWin = null;
								},
								failure: function(form, action) {
				   					if (action.failureType == 'client') {
				   						// do nothing
				   					} else if (action.failureType == 'connect') {
										Ext.MessageBox.alert('コネクションエラー', '通信時にエラーが発生しました。');
									} else {
										Ext.MessageBox.alert('失敗', title + 'できませんでした。');
										Ext.select('.x-tbar-loading').item(0).dom.click();
										extWin.close();
										extWin = null;
									}
								}
							});
						}
					},{
						text: 'キャンセル',
						handler: function(){
							form.getForm().reset();
							extWin.close();
							extWin = null;
						}
					}]
				});
			}
			extWin.show();
		}
	};
	var formWindow = new FormWindow();

	var colModel = new Ext.grid.ColumnModel([
		{header: "Id", width: 75, sortable: true, dataIndex: 'id'},
		{id:'name', header: "氏名", width: 160, sortable: true, dataIndex: 'name'},
		{header: "職種", width: 75, sortable: true, dataIndex: 'jobType'},
		{header: "給与", width: 75, sortable: true, dataIndex: 'salary'},
		{header: "部署", width: 85, sortable: true, dataIndex: 'department'}
	]);

	var addAction = new Ext.Action({
		text: '追加',
		iconCls: 'addIcon',
		handler: addActionHandler
	});

	var editAction = new Ext.Action({
		text: '編集',
		iconCls: 'editIcon'
	});

	var deleteAction = new Ext.Action({
		text: '削除',
		iconCls: 'deleteIcon'
	});

	var findAction = new Ext.Action({
		text: '検索',
		iconCls: 'findIcon'
	});

	var tbar = [
		addAction,
		'-',
		editAction,
		deleteAction,
		'-',
		findAction
	];

	var pageSize = 20;

	var bbar = new Ext.PagingToolbar({
		id: 'pagingToolbar',
		pageSize: pageSize,
		store: store,
		displayInfo: true,
		displayMsg: '社員の一覧 {2} 件中 {0} - {1} 件目',
		emptyMsg: "社員の一覧はありません"
	});

	var grid = new Ext.grid.GridPanel({
		title:'社員管理',
		stripeRows: true,
		autoExpandColumn: 'name',
		height:523,
		width:600,
		store: store,
		colModel: colModel,
		tbar: tbar,
		bbar: bbar
	});

	// INIT
	
	grid.render('grid-employee');

	store.load({params:{start:0, limit:pageSize}});

});

employee.jsは以上です。続いて呼び出されるフォームアクションを作成します。ソースは以下の通りです。

EmployeeAction.java
package webapplication.action;

import javax.annotation.Resource;

import org.seasar.framework.beans.util.Beans;
import org.seasar.struts.annotation.ActionForm;
import org.seasar.struts.annotation.Execute;
import org.seasar.struts.util.ResponseUtil;

import webapplication.entity.Employee;
import webapplication.form.EmployeeForm;
import webapplication.service.EmployeeService;


public class EmployeeAction {

	@ActionForm
	@Resource
	protected EmployeeForm employeeForm;

	@Resource
	protected EmployeeService employeeService;

	@Execute(validator = false)
	public String create() {
		Employee entity = Beans.createAndCopy(Employee.class, employeeForm)
				.execute();
		employeeService.insert(entity);
		ResponseUtil.write("{success:true}", "application/json");
		return null;
	}

}
EmployeeForm.java
package webapplication.form;

import org.seasar.struts.annotation.IntegerType;
import org.seasar.struts.annotation.Required;

public class EmployeeForm {

	@Required
	@IntegerType
	public String id;

	public String name;

	public String jobType;

	@IntegerType
	public String salary;

	public String department;

}

ごく一般的なSAStrutsのアクションのロジックと同じです。違う点は

  1. バリデートチェックはクライアント側のJavaScriptで行う為アクション側ではチェックしない。
  2. 返り値はnull。途中でResponseUtil.write("{success:true}", "application/json");とありJSONが返される。

です。

以上で実装は完了です。ブラウザでhttp://localhost:8080/ajax-app/employee/index.htmlをアクセスし追加ボタンを実行して確認します。

フォーム入力


追加された入力値。*idは自動採番。

今回は以上です。

ExtJSのダイナミックフォームとSAStrutsのアクションの相性がとても良いので簡単に作れました。次回はデータの編集、削除を掲載予定です。