Full Ajax な CRUD アプリケーション その6
今回はデータの検索機能を実装します。
検索ウィンドウの作成
検索ウィンドウは以下に示したようなフォームになります。検索項目は更新系のフォームとほぼ同じです。仕様は
- Idは完全一致検索
- 氏名はLike検索
- 職種は完全一致検索
- 給料は>=、<=検索
- 部署は完全一致検索
とします。
検索ウィンドウ
var FindWindow = function(){ var extWin = null; this.show = function(title, url){ var form; if (extWin == null) { form = new Ext.FormPanel({ method: 'POST', id: 'employee-form', baseCls: 'x-plain', labelWidth: 50, defaults: {width: 190}, defaultType: 'textfield', bodyStyle:'padding:5px 10px 0', items: [{ fieldLabel: 'Id', id: 'id', xtype: 'numberfield' },{ fieldLabel: '氏名', id: 'name' },{ fieldLabel: '職種', id: 'jobType' }, { fieldLabel: '給料 >=', id: 'salaryGe', xtype: 'numberfield' }, { fieldLabel: '給料 <=', id: 'salaryLe', xtype: 'numberfield' }, { fieldLabel: '部署', id: 'department' }] }); extWin = new Ext.Window({ title: '会社員情報 : ' + title, width: 300, height:240, 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: 'サブミットしています...', success: function(form, action) { Ext.select('.x-tbar-page-first').item(0).dom.click(); 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 findWindow = new FindWindow();
Ext.select('.x-tbar-page-first').item(0).dom.click();は検索時に必ず1ページ目から表示させる為のイベントです。
検索アクションハンドラー
var findActionHandler = function(){ findWindow.show('検索', '../employeeSearch/read'); }
検索アクションハンドラーの割り当て
var findAction = new Ext.Action({ text: '検索', iconCls: 'findIcon', handler: findActionHandler });
employee.js - 検索機能追加
Ext.onReady(function(){ // HANDLER var addActionHandler = function(){ formWindow.show('新規追加', './create'); Ext.getDom('id').readOnly = true; } var editActionHandler = function(){ var list = grid.selModel.getSelections(); if (list.length > 0) { formWindow.show('編集', './update'); var e = list[0]; Ext.getDom('id').value = e.get('id'); Ext.getDom('name').value = e.get('name'); Ext.getDom('jobType').value = e.get('jobType'); Ext.getDom('salary').value = e.get('salary'); Ext.getDom('department').value = e.get('department'); Ext.getDom('id').readOnly = true; } } var deleteActionHandler = function(){ var list = grid.selModel.getSelections(); if (list.length > 0) { formWindow.show('削除', './delete'); var e = list[0]; Ext.getDom('id').value = e.get('id'); Ext.getDom('name').value = e.get('name'); Ext.getDom('jobType').value = e.get('jobType'); Ext.getDom('salary').value = e.get('salary'); Ext.getDom('department').value = e.get('department'); Ext.getDom('id').readOnly = true; Ext.getDom('name').readOnly = true; Ext.getDom('jobType').readOnly = true; Ext.getDom('salary').readOnly = true; Ext.getDom('department').readOnly = true; } } var findActionHandler = function(){ findWindow.show('検索', '../employeeSearch/read'); } // 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 FindWindow = function(){ var extWin = null; this.show = function(title, url){ var form; if (extWin == null) { form = new Ext.FormPanel({ method: 'POST', id: 'employee-form', baseCls: 'x-plain', labelWidth: 50, defaults: {width: 190}, defaultType: 'textfield', bodyStyle:'padding:5px 10px 0', items: [{ fieldLabel: 'Id', id: 'id', xtype: 'numberfield' },{ fieldLabel: '氏名', id: 'name' },{ fieldLabel: '職種', id: 'jobType' }, { fieldLabel: '給料 >=', id: 'salaryGe', xtype: 'numberfield' }, { fieldLabel: '給料 <=', id: 'salaryLe', xtype: 'numberfield' }, { fieldLabel: '部署', id: 'department' }] }); extWin = new Ext.Window({ title: '会社員情報 : ' + title, width: 300, height:240, 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: 'サブミットしています...', success: function(form, action) { Ext.select('.x-tbar-page-first').item(0).dom.click(); 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 findWindow = new FindWindow(); 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 selModel = new Ext.grid.RowSelectionModel({ singleSelect:true }); var addAction = new Ext.Action({ text: '追加', iconCls: 'addIcon', handler: addActionHandler }); var editAction = new Ext.Action({ text: '編集', iconCls: 'editIcon', handler: editActionHandler }); var deleteAction = new Ext.Action({ text: '削除', iconCls: 'deleteIcon', handler: deleteActionHandler }); var findAction = new Ext.Action({ text: '検索', iconCls: 'findIcon', handler: findActionHandler }); 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, selModel: selModel, tbar: tbar, bbar: bbar }); // INIT grid.render('grid-employee'); store.load({params:{start:0, limit:pageSize}}); });
employee.jsは以上です。続いて呼び出されるフォームアクションを作成します。検索のアクションを新規作成します。ソースは以下の通りです。
EmployeeSearchAction.java
package webapplication.action; import javax.annotation.Resource; import org.seasar.struts.annotation.ActionForm; import org.seasar.struts.annotation.Execute; import webapplication.dto.EmployeeSearchConditionDto; import webapplication.form.EmployeeSearchForm; import webapplication.service.EmployeeService; import webapplication.util.MyBeans; public class EmployeeSearchAction { @ActionForm @Resource protected EmployeeSearchForm employeeSearchForm; @Resource protected EmployeeSearchConditionDto employeeSearchConditionDto; @Execute(validator = false) public String read() { MyBeans.copy(employeeSearchForm, employeeSearchConditionDto).execute(); return null; } }
EmployeeSearchForm.java
package webapplication.form; import org.seasar.struts.annotation.IntegerType; import org.seasar.struts.annotation.Required; public class EmployeeSearchForm { @Required @IntegerType public String id; public String name; public String jobType; @IntegerType public String salaryGe; @IntegerType public String salaryLe; public String department; }
EmployeeSearchConditionDto.java
package webapplication.dto; import java.io.Serializable; import org.seasar.framework.container.annotation.tiger.Component; import org.seasar.framework.container.annotation.tiger.InstanceType; @Component(instance = InstanceType.SESSION) public class EmployeeSearchConditionDto implements Serializable { private static final long serialVersionUID = 1L; public Integer id; public String name; public String jobType; public Integer salaryGe; public Integer salaryLe; public String department; }
MyBeans.java
package webapplication.util; import java.lang.reflect.Field; import org.seasar.framework.beans.util.Copy; public class MyBeans { /** * プロパティをコピーするオブジェクトを作成します。 * コピー元フィールドの値が文字列長0の文字列 * の場合はnullがコピーされます。 * * @param src * コピー元 * @param dest * コピー先 * @return コピー用のオブジェクト */ public static Copy copy(Object src, Object dest) { changeWhitespaceToNull(src); return new Copy(src, dest); } @SuppressWarnings("unchecked") public static void changeWhitespaceToNull(Object obj) { Class cls = obj.getClass(); Field[] fieldList = cls.getFields(); for (int i=0;i<fieldList.length;i++) { try { if (fieldList[i].get(obj).equals("")) { fieldList[i].set(obj, null); } } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } } }
検索条件はセッションに保存するようにしました。これは検索機能はEmployeeService#find()で、そのメソッドに値を渡すためです。EmployeeService#find()のロジックを以下に示します。
EmployeeService.java
package webapplication.service; import java.util.HashMap; import java.util.List; import java.util.Map; import org.seasar.extension.jdbc.where.SimpleWhere; import webapplication.dto.EmployeeSearchConditionDto; import webapplication.dto.EmployeeSearchConditionNames; import webapplication.dto.PagerConditionDto; import webapplication.entity.Employee; /** * {@link Employee}のサービスクラスです。 * * @author tom */ public class EmployeeService extends AbstractService<Employee> implements EmployeeSearchConditionNames { /** * 検索条件 * */ public EmployeeSearchConditionDto employeeSearchConditionDto; /** * 識別子でエンティティを検索します。 * * @param id * 識別子 * @return エンティティ */ public Employee findById(Integer id) { return select().id(id).getSingleResult(); } /** * JSONIC RESTモードで使用。エンティティ一覧を検索します。 * * @param conditionDto * ページング条件 * @return JSONの為のMapオブジェクト */ public Map<String, Object> find(PagerConditionDto conditionDto) { SimpleWhere where = new SimpleWhere() .eq(id, employeeSearchConditionDto.id) .contains(name, employeeSearchConditionDto.name) .eq(jobType, employeeSearchConditionDto.jobType) .ge(salaryGe, employeeSearchConditionDto.salaryGe) .le(salaryLe, employeeSearchConditionDto.salaryLe) .eq(department, employeeSearchConditionDto.department); long count = jdbcManager .from(Employee.class) .where(where) .getCount(); List<Employee> list = jdbcManager .from(Employee.class) .where(where) .orderBy(id) .offset(conditionDto.start) .limit(conditionDto.limit) .getResultList(); Map<String, Object> m = new HashMap<String, Object>(); m.put("totalProperty", count); m.put("root", list); return m; } }
EmployeeSearchConditionNames.java
package webapplication.dto; /** * {@link EmployeeSearchConditionDto}のプロパティ名の集合です。 * */ public interface EmployeeSearchConditionNames { /** {@link EmployeeSearchConditionDto#id}の名前 */ String id = "id"; /** {@link EmployeeSearchConditionDto#name}の名前 */ String name = "name"; /** {@link EmployeeSearchConditionDto#jobType}の名前 */ String jobType = "jobType"; /** {@link EmployeeSearchConditionDto#salaryGe}の名前 */ String salaryGe = "salaryGe"; /** {@link EmployeeSearchConditionDto#salaryLe}の名前 */ String salaryLe = "salaryLe"; /** {@link EmployeeSearchConditionDto#department}の名前 */ String department = "department"; }
検索機能の実装は以上です。ブラウザでhttp://localhost:8080/ajax-app/employee/index.htmlをアクセスし検索ボタンを押して検索操作を行います。
検索後に再度、検索ウィンドウを開くと入力した条件は消えています。このままでは検索条件の確認が出来ない等何かと不便です。サーバー側では検索条件を保持していますので検索ウィンドウを開いた際に検索条件をフォームに読み込むようにします。
フォームに検索条件を読み込む
~ form.getForm().load({url: '../employeeSearch/readSearchCondition'}); extWin.show(); } }; var findWindow = new FindWindow(); ~
employee.js - 検索条件読み込み追加
Ext.onReady(function(){ // HANDLER var addActionHandler = function(){ formWindow.show('新規追加', './create'); Ext.getDom('id').readOnly = true; } var editActionHandler = function(){ var list = grid.selModel.getSelections(); if (list.length > 0) { formWindow.show('編集', './update'); var e = list[0]; Ext.getDom('id').value = e.get('id'); Ext.getDom('name').value = e.get('name'); Ext.getDom('jobType').value = e.get('jobType'); Ext.getDom('salary').value = e.get('salary'); Ext.getDom('department').value = e.get('department'); Ext.getDom('id').readOnly = true; } } var deleteActionHandler = function(){ var list = grid.selModel.getSelections(); if (list.length > 0) { formWindow.show('削除', './delete'); var e = list[0]; Ext.getDom('id').value = e.get('id'); Ext.getDom('name').value = e.get('name'); Ext.getDom('jobType').value = e.get('jobType'); Ext.getDom('salary').value = e.get('salary'); Ext.getDom('department').value = e.get('department'); Ext.getDom('id').readOnly = true; Ext.getDom('name').readOnly = true; Ext.getDom('jobType').readOnly = true; Ext.getDom('salary').readOnly = true; Ext.getDom('department').readOnly = true; } } var findActionHandler = function(){ findWindow.show('検索', '../employeeSearch/read'); } // 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 FindWindow = function(){ var extWin = null; this.show = function(title, url){ var form; if (extWin == null) { form = new Ext.FormPanel({ method: 'POST', id: 'employee-form', baseCls: 'x-plain', labelWidth: 50, defaults: {width: 190}, defaultType: 'textfield', bodyStyle:'padding:5px 10px 0', items: [{ fieldLabel: 'Id', id: 'id', xtype: 'numberfield' },{ fieldLabel: '氏名', id: 'name' },{ fieldLabel: '職種', id: 'jobType' }, { fieldLabel: '給料 >=', id: 'salaryGe', xtype: 'numberfield' }, { fieldLabel: '給料 <=', id: 'salaryLe', xtype: 'numberfield' }, { fieldLabel: '部署', id: 'department' }] }); extWin = new Ext.Window({ title: '会社員情報 : ' + title, width: 300, height:240, 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: 'サブミットしています...', success: function(form, action) { Ext.select('.x-tbar-page-first').item(0).dom.click(); 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; } }] }); } form.getForm().load({url: '../employeeSearch/readSearchCondition'}); extWin.show(); } }; var findWindow = new FindWindow(); 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 selModel = new Ext.grid.RowSelectionModel({ singleSelect:true }); var addAction = new Ext.Action({ text: '追加', iconCls: 'addIcon', handler: addActionHandler }); var editAction = new Ext.Action({ text: '編集', iconCls: 'editIcon', handler: editActionHandler }); var deleteAction = new Ext.Action({ text: '削除', iconCls: 'deleteIcon', handler: deleteActionHandler }); var findAction = new Ext.Action({ text: '検索', iconCls: 'findIcon', handler: findActionHandler }); 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, selModel: selModel, tbar: tbar, bbar: bbar }); // INIT grid.render('grid-employee'); store.load({params:{start:0, limit:pageSize}}); });
EmployeeSearch.java - readSearchCondition()追加
package webapplication.action; import java.util.HashMap; import java.util.Map; import javax.annotation.Resource; import net.arnx.jsonic.JSON; import org.seasar.struts.annotation.ActionForm; import org.seasar.struts.annotation.Execute; import org.seasar.struts.util.ResponseUtil; import webapplication.dto.EmployeeSearchConditionDto; import webapplication.form.EmployeeSearchForm; import webapplication.service.EmployeeService; import webapplication.util.MyBeans; public class EmployeeSearchAction { @ActionForm @Resource protected EmployeeSearchForm employeeSearchForm; @Resource protected EmployeeSearchConditionDto employeeSearchConditionDto; @Execute(validator = false) public String read() { MyBeans.copy(employeeSearchForm, employeeSearchConditionDto).execute(); return null; } @Execute(validator = false) public String readSearchCondition() { Map<String, Object> m = new HashMap<String, Object>(); m.put("success", true); m.put("data", employeeSearchConditionDto); ResponseUtil.write(JSON.encode(m), "application/json"); return null; } }
readSearchCondition()で返すJSONの書式は
{"success":true, "data":{"department":null,"id":null,"jobType":null,"name":"aaa","salaryGe":null,"salaryLe":null}}
の様になっています。net.arnx.jsonic.JSON.encode()を使用すれば簡単にJSONを生成出来ます。
ブラウザでhttp://localhost:8080/ajax-app/employee/index.htmlをリロードし検索ボタンを押してウィンドウを開きます。
入力した検索条件が表示されるのが確認出来ます。
以上で検索機能の実装は完了です。
これで全ての機能実装が完了しました。