刚接触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=''。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义