剛接觸ExtJs不到一周,項目使用ExtJs框架,有個版塊用到了combobox的級聯(兩級),遇到了一系列的問題,兩天來一直查API、網絡資料,終於解決了。
先列出遇到的一系列問題(也許你也遇到過!),再看是如何一步步解決這些問題的,最后給出個人覺得ExtJs的ComboBox級聯的最佳方案。
***首先聲明,測試使用[年級]和[班級]的級聯,數據從服務端獲取。最終效果是:年級列表顯示所有年級,默認顯示第一個年級;班級列表顯示第一個年級下的班級,默認顯示"所有";***
遇到的問題:
1.為何每次點擊班級列表時就把所有的班級加載出來了,但切換另一個年級后就正常了?
2.打開火狐的Firebug可以看到,班級列表已經加載一次了,但點擊下拉列表框后又加載了一次,怎么回事?其實點擊年級列表也會再加載一次的,why?
3.如何為combobox設置一個默認值?
4.如何為combobox添加一個值(“所有”)
5.想在監聽事件afterrender或者change事件中來處理上述問題,覺得不是你想的那樣?
6.queryMode、triggerAction、autoLoad這些屬性怎么配合使用?
----------解決-------------------------------------------------------------------------------------------------------------------------------------------------------------
先貼出測試的Servlet類:主要用於獲取年級列表和班級列表,數據是靜態的,以JSON格式返回。
1 package com.lizhou.bms.controller; 2
3 import com.lizhou.bms.entity.Clazz; 4 import com.lizhou.bms.entity.Grade; 5 import net.sf.json.JSONArray; 6
7 import javax.servlet.ServletException; 8 import javax.servlet.http.HttpServlet; 9 import javax.servlet.http.HttpServletRequest; 10 import javax.servlet.http.HttpServletResponse; 11 import java.io.IOException; 12 import java.util.ArrayList; 13 import java.util.LinkedList; 14 import java.util.List; 15
16 /**
17 * 模擬獲取數據 18 * @author bojiangzhou 19 * @date 2016/8/8 20 */
21 public class StudentController extends HttpServlet { 22
23 private static List<Grade> gradeList = new LinkedList<Grade>(); 24
25 private static List<Clazz> clazzList = new LinkedList<Clazz>(); 26
27 /**
28 * 數據源 29 */
30 static { 31 //年級
32 Grade g1 = new Grade(1, "一年級"); 33 Grade g2 = new Grade(2, "二年級"); 34 Grade g3 = new Grade(3, "三年級"); 35
36 gradeList.add(g1); 37 gradeList.add(g2); 38 gradeList.add(g3); 39
40 //班級
41 Clazz g1c1 = new Clazz(1, 1, "一年級 1班"); 42 Clazz g1c2 = new Clazz(2, 1, "一年級 2班"); 43 Clazz g1c3 = new Clazz(3, 1, "一年級 3班"); 44 Clazz g1c4 = new Clazz(4, 1, "一年級 4班"); 45 Clazz g1c5 = new Clazz(5, 1, "一年級 5班"); 46 Clazz g1c6 = new Clazz(6, 1, "一年級 6班"); 47 Clazz g1c7 = new Clazz(7, 1, "一年級 7班"); 48
49 Clazz g2c1 = new Clazz(8, 2, "二年級 1班"); 50 Clazz g2c2 = new Clazz(9, 2, "二年級 2班"); 51 Clazz g2c3 = new Clazz(10, 2, "二年級 3班"); 52 Clazz g2c4 = new Clazz(11, 2, "二年級 4班"); 53 Clazz g2c5 = new Clazz(12, 2, "二年級 5班"); 54 Clazz g2c6 = new Clazz(13, 2, "二年級 6班"); 55 Clazz g2c7 = new Clazz(14, 2, "二年級 7班"); 56
57 Clazz g3c1 = new Clazz(15, 3, "三年級 1班"); 58 Clazz g3c2 = new Clazz(16, 3, "三年級 2班"); 59 Clazz g3c3 = new Clazz(17, 3, "三年級 3班"); 60 Clazz g3c4 = new Clazz(18, 3, "三年級 4班"); 61 Clazz g3c5 = new Clazz(19, 3, "三年級 5班"); 62 Clazz g3c6 = new Clazz(20, 3, "三年級 6班"); 63 Clazz g3c7 = new Clazz(21, 3, "三年級 7班"); 64
65 clazzList.add(g1c1); 66 clazzList.add(g1c2); 67 clazzList.add(g1c3); 68 clazzList.add(g1c4); 69 clazzList.add(g1c5); 70 clazzList.add(g1c6); 71 clazzList.add(g1c7); 72
73 clazzList.add(g2c1); 74 clazzList.add(g2c2); 75 clazzList.add(g2c3); 76 clazzList.add(g2c4); 77 clazzList.add(g2c5); 78 clazzList.add(g2c6); 79 clazzList.add(g2c7); 80
81 clazzList.add(g3c1); 82 clazzList.add(g3c2); 83 clazzList.add(g3c3); 84 clazzList.add(g3c4); 85 clazzList.add(g3c5); 86 clazzList.add(g3c6); 87 clazzList.add(g3c7); 88
89 } 90
91 @Override 92 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 93
94 //前台傳一個method參數,getGradeList即請求獲取年級列表,getClazzList即請求獲取班級列表
95 String method = request.getParameter("method"); 96
97 response.setCharacterEncoding("UTF-8"); 98
99 if("getGradeList".equals(method)){ 100 JSONArray jsonArray = JSONArray.fromObject(gradeList); 101 String ret = jsonArray.toString(); 102 response.getWriter().write(ret); 103
104 } else if("getClazzList".equals(method)){ 105 List<Clazz> clist = new ArrayList<Clazz>(); 106 //年級id
107 String sgid = request.getParameter("gid"); 108 if(sgid != null){ 109 int gid = Integer.parseInt(sgid); 110
111 for(Clazz c : clazzList){ 112 if(c.getGid() == gid){ 113 clist.add(c); 114 } 115 } 116 } else{ 117 clist.addAll(clazzList); 118 } 119 JSONArray jsonArray = JSONArray.fromObject(clist); 120 String ret = jsonArray.toString(); 121 response.getWriter().write(ret); 122
123 } 124 } 125 }
然后是最初版的JS代碼:
1 <%--
2
3 @author bojiangzhou 4 @date 2016/8/8
5 --%>
6 <%@ page contentType="text/html;charset=UTF-8" language="java" %>
7 <%@ page isELIgnored="false" %>
8 <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
9 <html>
10 <head>
11 <title>Combobox</title>
12 <link rel="stylesheet" href="js/ext/resources/css/ext-all.css" />
13 <script type="text/javascript" src="js/ext/ext-all.js"></script>
14 <script>
15 Ext.onReady(function () { 16
17 /** 18 * 創建年級Combo 19 */
20 Ext.create('Ext.form.ComboBox', { 21 renderTo: Ext.getBody(), 22 id: 'gradeId', 23 displayField: 'name', 24 valueField: 'id', 25 editable: false, 26 readonly: true, 27 allowBlank: true, 28 fieldLabel: '選擇年級', 29 margin: '50 10 0 0', 30 labelAlign: 'right', 31 triggerAction: 'all', //點擊下拉列表時執行的操作
32 queryMode: 'remote', //store的查詢模式
33 store: Ext.create('Ext.data.JsonStore', { 34 fields: [ 35 {name: 'id'}, 36 {name: 'name'} 37 ], 38 autoLoad: true, //啟動自動加載
39 proxy: { //通過ajax代理加載數據
40 type: 'ajax', 41 url: 'student?method=getGradeList', 42 reader: { 43 type: 'json', 44 root: 'content'
45 } 46 } 47 }), 48 listeners: { 49 'change': function(o, gid){ //change事件
50 if(gid){ 51 var clazzId = Ext.getCmp("clazzId"); //獲取Clazz Combo組件
52 clazzId.getStore().removeAll(); // 清空已加載列表
53 clazzId.reset(); // 清空已存在結果
54
55 //發生change事件后將年級id傳到后台獲取該年級下的班級
56 clazzId.getStore().load({ 57 params: {'gid': gid} 58 }); 59 } 60 } 61 } 62
63 }); 64
65 /** 66 * 創建班級Combo 67 */
68 Ext.create('Ext.form.ComboBox', { 69 renderTo: Ext.getBody(), 70 id: 'clazzId', 71 displayField: 'name', 72 valueField: 'id', 73 editable: false, 74 readonly: true, 75 allowBlank: true, 76 fieldLabel: '選擇班級', 77 margin: '50 10 0 0', 78 labelAlign: 'right', 79 triggerAction: 'all', //點擊下拉列表時執行的操作
80 queryMode: 'remote', //store的查詢模式
81 store: Ext.create('Ext.data.JsonStore', { 82 fields: [ 83 {name: 'id'}, 84 {name: 'gid'}, 85 {name: 'name'} 86 ], 87 autoLoad: true, //啟動自動加載
88 proxy: { //通過ajax代理加載數據
89 type: 'ajax', 90 url: 'student?method=getClazzList', 91 reader: { 92 type: 'json', 93 root: 'content'
94 } 95 } 96 }) 97 }); 98
99
100 }); 101
102 </script>
103 </head>
104 <body>
105
106 </body>
107 </html>
一、queryMode、autoLoad
第一次刷新頁面顯示效果如下:可以看到兩個列表都沒有默認值,其次是一開始就發送了兩次請求,也就是說已經將年級和班級的數據加載進來了(而且還是所有數據)。
然后點擊班級列表,選擇一年級,看到如下效果:點擊年級下拉列表的時候又發送了一次請求的,然后這個時候會觸發年級combobox的change事件,加載班級列表,可以看到請求已經發送過去了,年級id也傳過去了,那班級列表按理說應該是一年級的班級;
再看第二張圖片的效果:點擊下拉列表框的時候也同樣發送了一次請求,而班級顯示的是所有班級,這就是出現的問題了,為什么會這樣呢?
從第一次刷新頁面來說整個過程:首先刷新頁面,因為配置的store為自動加載(autoLoad: true),所以在刷新頁面的時候,會自動將數據加載到store中,然后渲染到列表里。
然后點擊年級列表,因為我們設置的queryMode: 'remote',(remote是默認屬性值);個人理解:queryMode屬性決定着當【第一次】點擊下拉列表的時候,列表的查詢模式,remote即從遠程加載,相當於點擊下拉列表的時候又加載了一次,這就是點擊列表的時候為什么又發送了一次請求的原因。queryModel的另一個屬性值是'local',從本地加載;我的理解是,數據如果已經從遠端加載到store中了(比如autoLoad,年級列表change事件觸發加載班級列表),所謂的local就是當第一次點擊下拉列表的時候直接從store中獲取數據,而相對的,remote則會從遠端加載,而且會覆蓋掉store中的數據。
再是點擊班級列表,雖然點擊年級列表觸發了change事件來使班級列表加載當前年級下的班級,原因上面已經說了,點擊班級列表的時候,同樣重新發送了一個請求加載了所有的班級,所以之前的被覆蓋了。
解決辦法:將二者的queryMode設置為local,使其從Store中獲取數據,年級列表自動加載,設置為local后點擊下拉列表時不會再發送一次請求;但是班級列表是與年級列表聯動的,所以在沒有年級列表的時候,我不希望顯示班級列表,那么可以設置班級ComboBox的store的autoLoad:false,讓其不自動加載,只有在選擇年級的時候才去加載相應年級下的班級。這樣一來刷新頁面的時候就只發送了一次加載年級的請求,班級只會在選擇年級后加載,但是每次還是會發送請求的。
二、如何為讓年級列表默認選擇第一個,班級列表默認顯示"所有"
讓第一級列表(年級列表)默認顯示第一條,剛開始想的辦法是給班級Combobox加一個afterrender事件,即組件渲染完成后給年級列表設置第一個選項,這樣也會觸發change事件,就能加載班級了;
或者給年級列表添加一個屬性value=1,默認選擇第一個選項,但是第一次不會加載班級,沒有觸發change事件。這兩種方式都有一個小問題,就是刷新頁面的時候,會看到列表框首先顯示的1,再才顯示第一個選項的,尤其在加載比較慢的時候就很明顯了。所以這兩種方式不可取。
1 listeners: { 2 'afterrender': function (o) { 3 var gradeId = Ext.getCmp("gradeId"); //獲取Grade Combo組件
4 gradeId.setValue(1); 5 } 6 }
再說說如何為班級列表插入一個選項"所有",之前嘗試過很多種方式都不行,然后想了一個不算好的辦法可以在后台獲取到數據后,再向集合中插入一個含有"所有"的對象,就能直接加載過來了,但是這種方式不是很好。其實主要是添加的時機不對,導致沒有添加進去。
最后經過一系列的測試,對於數據的操作應放在Store的load事件中來操作,就都正常了,Store本身就是數據倉庫,所以在ComboBox上做的操作都有所不妥。
看最后解決上述問題的代碼:注意看注釋部分,是解決問題的關鍵。
1 <%--
2
3 @author bojiangzhou 4 @date 2016/8/7
5 --%>
6 <%@ page contentType="text/html;charset=UTF-8" language="java" %>
7 <%@ page isELIgnored="false" %>
8 <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
9 <html>
10 <head>
11 <title>Combobox</title>
12 <link rel="stylesheet" href="js/ext/resources/css/ext-all.css" />
13 <script type="text/javascript" src="js/ext/ext-all.js"></script>
14 <script>
15 Ext.onReady(function () { 16
17 /** 18 * 年級列表 19 */
20 Ext.create('Ext.form.ComboBox', { 21 renderTo: Ext.getBody(), 22 id: 'gradeId', 23 displayField: 'name', 24 valueField: 'id', 25 editable: false, 26 readonly: true, 27 allowBlank: true, 28 fieldLabel: '選擇年級級', 29 margin: '50 10 0 0', 30 labelAlign: 'right', 31 queryMode: 'local', //本地查詢,配置這個屬性,在第一次點擊下拉列表的時候就不會從服務端加載數據了
32 triggerAction: 'all', 33 store: Ext.create('Ext.data.JsonStore', { //Store數據倉庫
34 fields: [ 35 {name: 'id'}, 36 {name: 'name'} 37 ], 38 autoLoad: true, //第一級列表設置自動加載
39 proxy: { //通過ajax代理加載數據
40 type: 'ajax', 41 url: 'student?method=getGradeList', 42 reader: { 43 type: 'json', 44 root: 'ret'
45 } 46 }, 47 listeners: { //注意是store的監聽器
48 'load': function (store, records) { //store的load事件
49 //設置第一個值為默認值
50 Ext.getCmp("gradeId").setValue(records[0]); 51 } 52 } 53 }), 54 listeners: { //這是ComboBox的監聽器
55 'change': function(o, nv){ //change事件
56 if(nv){ 57 var clazzId = Ext.getCmp("clazzId"); 58 clazzId.getStore().removeAll();// 清空已加載列表
59 clazzId.reset();// 清空已存在結果
60
61 //在年級列表發生改變時將年級ID傳到后台,加載該年級下的班級,
62 //但是每次改變年級時都會從服務器加載,有點消耗服務器資源
63 clazzId.getStore().load({ 64 params: {'gid': nv}, //參數
65 callback: function(records, operation, success) { //加載完成調用的函數
66 //添加一個所有選項
67 clazzId.getStore().insert(0, {id: 0, name: '所有' }); 68 clazzId.setValue(0); //設置默認第一個 69 } 70 }); 71 } 72 } 73 } 74
75 }); 76
77 /** 78 * 班級列表 79 */
80 Ext.create('Ext.form.ComboBox', { 81 renderTo: Ext.getBody(), 82 id: 'clazzId', 83 displayField: 'name', 84 valueField: 'id', 85 editable: false, 86 readonly: true, 87 allowBlank: true, 88 fieldLabel: '選擇年級', 89 margin: '50 10 0 0', 90 labelAlign: 'right', 91 triggerAction: 'all', 92 queryMode: 'local', //本地加載模式
93 store: Ext.create('Ext.data.JsonStore', { //Store數據倉庫
94 fields: [ 95 {name: 'id'}, 96 {name: 'name'} 97 ], 98 autoLoad: false, //設置第二級不自動加載
99 proxy: { 100 type: 'ajax', 101 url: 'student?method=getClazzList', 102 reader: { 103 type: 'json', 104 root: 'content'
105 } 106 } 107 }) 108 }); 109 }); 110
111 </script>
112 </head>
113 <body>
114
115 </body>
116 </html>
上面的代碼還有一個問題就是每次都會從服務端加載班級列表,會消耗服務端資源,這對於大型系統來說還是應該優化下的,於是我將數據加載到本地,每次用的時候就去取,整個過程只會向服務端發送兩次請求。注意看注釋部分!
1 <%--
2
3 @author bojiangzhou 4 @date 2016/8/7
5 --%>
6 <%@ page contentType="text/html;charset=UTF-8" language="java" %>
7 <%@ page isELIgnored="false" %>
8 <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
9 <html>
10 <head>
11 <title>Combobox</title>
12 <link rel="stylesheet" href="js/ext/resources/css/ext-all.css" />
13 <script type="text/javascript" src="js/ext/ext-all.js"></script>
14 <script>
15 Ext.onReady(function () { 16
17 var clazzList = {}; //班級列表
18
19 /** 20 * 年級列表 21 */
22 Ext.create('Ext.form.ComboBox', { 23 renderTo: Ext.getBody(), 24 id: 'gradeId', 25 displayField: 'name', 26 valueField: 'id', 27 editable: false, 28 readonly: true, 29 allowBlank: true, 30 fieldLabel: '選擇年級', 31 margin: '50 10 0 0', 32 labelAlign: 'right', 33 queryMode: 'local', //本地查詢,配置這個屬性,在第一次點擊下拉列表的時候就不會從服務端加載數據了
34 triggerAction: 'all', 35 store: Ext.create('Ext.data.JsonStore', { //Store數據倉庫
36 fields: [ 37 {name: 'id'}, 38 {name: 'name'} 39 ], 40 autoLoad: true, //第一級列表設置自動加載
41 proxy: { //通過ajax代理加載數據
42 type: 'ajax', 43 url: 'student?method=getGradeList', 44 reader: { 45 type: 'json', 46 root: 'ret'
47 } 48 }, 49 listeners: { //注意是store的監聽器
50 'load': function (store, gRecords) { //store的load事件
51
52 //在Store的load事件中,加載班級的數據,返回成功后進行一些處理
53 Ext.getCmp("clazzId").getStore().load({ 54 callback: function(records, operation, success) { //加載成功返回后調用的函數
55 //將年級全部加載出來放到全局中
56 for(var i = 0;i < records.length;i++){ 57 var gid = records[i].data['gid']; //獲取班級所屬的年級id
58 if(!clazzList[gid]){ 59 clazzList[gid] = []; //數組用於存放班級
60 clazzList[gid].push({id:0, name: '所有'}); //添加一個所有選項
61 } 62
63 clazzList[gid].push(records[i]); //將record添加到該年級的數組下
64 } 65
66 //要先加載后在設置默認值,由於異步加載,change事件可能會不起作用。
67 //設置年級的第一個值為默認值
68 Ext.getCmp("gradeId").setValue(gRecords[0]); //注意是外部的gRecords
69 } 70 }); 71 } 72 } 73 }), 74 listeners: { //這是ComboBox的監聽器
75 'change': function(o, nv){ //change事件
76 if(nv){ 77 var clazzId = Ext.getCmp("clazzId"); 78 clazzId.getStore().removeAll();// 清空已加載列表
79 clazzId.reset();// 清空已存在結果
80
81 if(clazzList[nv]){ 82 //發生change事件后,從班級列表中取出該年級下的班級添加到班級store中
83 clazzId.getStore().insert(0,clazzList[nv]); 84 clazzId.setValue(0); //設置第一個值默認,即"所有"
85 } 86 } 87 } 88 } 89
90 }); 91
92 /** 93 * 班級列表 94 */
95 Ext.create('Ext.form.ComboBox', { 96 renderTo: Ext.getBody(), 97 id: 'clazzId', 98 displayField: 'name', 99 valueField: 'id', 100 editable: false, 101 readonly: true, 102 allowBlank: true, 103 fieldLabel: '選擇年級', 104 margin: '50 10 0 0', 105 labelAlign: 'right', 106 triggerAction: 'all', 107 queryMode: 'local', //本地加載模式
108 store: Ext.create('Ext.data.JsonStore', { //Store數據倉庫
109 fields: [ 110 {name: 'id'}, 111 {name: 'gid'}, 112 {name: 'name'} 113 ], 114 autoLoad: false, //設置第二級不自動加載
115 proxy: { 116 type: 'ajax', 117 url: 'student?method=getClazzList', 118 reader: { 119 type: 'json', 120 root: 'content'
121 } 122 } 123 }) 124 }); 125
126
127 }); 128
129 </script>
130 </head>
131 <body>
132
133 </body>
134 </html>
三、再說說triggerAction
下面是文檔對triggerAction的說明,剛開始不怎么明白這個屬性的用途,只知道設置為all的時候能查詢出數據來,設置成query的時候就查不出來了....
后來看到一本書上的例子才對它的用法理解了,triggerAction一般來說會和allQuery、queryParam兩個屬性配合使用,而且一般combobox是可編輯的,這幾個參數是用於輸入查詢的。
在triggerAction:'all'的時候,點擊下拉列表的時候會根據allQuery的值查詢所有相關的數據,queryParam和allQuery可以理解成鍵和值關系。
比如配置:queryParam:'grade', allQuery:'一年級', triggerAction:'all',點擊下拉列表時,注意是點擊下拉列表的時候,就會向后台請求,並帶上參數:grade='一年級',然后后台就可以根據這組參數查詢該年級下的班級返回來。
如果你在combo中輸入值,且配置了minChars,比如:minChars:3,則在你輸入的字符數大於3的時候就會自動向后台發送請求,並帶上參數:grade='你輸入的值',然后查詢。
設置triggerAction:'query'的時候,在點擊下拉列表的時候,發送的參數就是grade='你輸入的值',如果沒有輸入,相當於發送grade=''。