例程:AjaxCombox

之前曾有读者提到如何根据数据库码表生成下拉列表,当时我们推荐使用jquery.ajaxComboBox。但遗憾的是它的服务器端代码是PHP,日前我们已经将其迁移到了Grails。本来是打算把它做成Grails插件分发出来的。可在移植的过程中发现其接口设计带有明显的数据库特征,不太符合Grails的DomainClass风格,于是就打消了这个念头,仅止于完成一个TagLib和一个演示例程。本文将介绍例程的实现和使用,以供大家参考。本例程中使用两个Domain Class作为示例:Role,User,二者是一对多的关系。

例程很简单,就是演示了移植后jquery.ajaxComboBox的基本使用。跟其官网的例子不同,传入的不是数据库名,而是DomainClass。这个ComboBox有以下特点:

  • 可设置Domain Class以及Domain Class中的字段;
  • 如果查询结果相同,可使用子查询来辅助区别;
  • 对查询结果进行排序设置;
  • 对查询结果进行分页显示,并可自行设置每页显示的记录个数以及页码个数;
  • 可设置初始值;
  • 使用组ComboBox。

例程组成如下:

  • jquery-1.4.2.min.js:JQuery JS文件;
  • jquery.ajaxComboBox.3.5.min.js:jquery.ajaxComboBox的JS文件;
  • jquery.ajaxComboBox.css:jquery.ajaxComboBox的CSS文件;
  • jquery.ajaxComboBox自带的若干图片;
  • jqac.DomainController.groovy:根据参数执行查询,返回结果。
  • jqac.JqacTagLib.groovy:自定义的标签库,负责生成ComboBox和显示查询数据。

上述组成的前四项,除了对jquery.ajaxComboBox.3.5.min.js做了Ajax Post路径以及图片路径的更改外,其他均采用了原作者提供的文件。

首先看看TagLib:名字空间为jqac,其属性名主要来源于PHP接受的参数,但名字略有变化。对于无法通过原来参数传入的参数,使用了URL查询字符串的方式传入。部分代码如下:

static namespace= "jqac"
def domain= { attrs, body ->        
   def jsJQAC= """
   jQuery(document).ready(function(\$){
      \$('#${attrs.id}').ajaxComboBox(
         ${createURL(attrs.domain, attrs.subset)},  
         {
           'lang' : 'en', //提示信息的语言,可选择en(英文),ja(日文),es(西班牙文)
           'sub_info' : ${attrs.subInfo?:false},    
           'sub_as' : ${mapSubSetToSubAs(attrs.subset)},  
           'show_field' :"${attrs.showField?:''}",    
           'hide_field' :"${attrs.hideField?:''}",    
           'per_page' : ${attrs.pageSize?:10},        
           'navi_num' : ${attrs.naviNum?:5},         
           'navi_simple' : ${attrs.naviNum?:true},             
           'primary_key':"${attrs.primaryKey?:'id'}", 
           'field':"${attrs.field?:'name'}",          
           'order_field':"${attrs.orderField?:'name'}",  
           'order_by':"${attrs.orderBy?:'ASC'}",         
           'package' : ${attrs.package?:false},          
           'mini' : ${attrs.mini?:false},               
           'select_only':${attrs.selectOnly?:false},     
           'init_val' : [${attrs.initVal}],              
           'db_table':'${attrs.domain}'
       }
      );
  });
  """
  out << "<div id='$attrs.id' style='$attrs.style'></div>"
  out << "<script type='text/javascript'>"<< jsJQAC << "</script>"
}
......

注意上述代码中的db_table参数,前面已经说过jquery.ajaxComboBox是直接面向数据库的设计。这里向标签使用者隐藏了这个参数,改而用domain属性。同时,在Controller里面也不是直接去拼凑SQL,而是借用了GORM的基础设施。关于代码中的其他参数,请参见后面的用法介绍。

在GSP中可以这样使用这个TagLib:

<jqac:domain id="role-id" style="width:260px;" domain="jqac.Role"/>

上述代码会生成一个id为role-id_1的input框,单击右边的,效果如下:

下面看看数据的打包——jqac.DomainController.groovy,如下是部分代码:

def q_word      = params.q_word   //input框中输入的内容;
def page_num    =params.page_num  //每页显示的记录个数,缺省值为10;
def per_page    =params.per_page  //每页显示的页码个数,缺省值为5;
def field       = params.field    //查询字段;
def order_field = params.order_field   //排序字段,缺省值为name;
def order_by    =params.order_by       //排序策略,缺省值为ASC;
def domainname    = params.domain      //要查询的Domain Class;
def fieldInBF = field[0].toUpperCase() + field[1..-1]

//注入grailsApplication,使用其getClassForName方法获得Domain Class
def domain= grailsApplication.getClassForName(domainname) 

//注意这里的技巧
def domains= domain."findAllBy${fieldInBF}Like"("%$q_word%", 
                                            [max: per_page, 
                                             offset:offset,
                                             sort:order_field, 
                                             order:order_by])
def result=[:]    //result是存放结果的Map;
domains.each{
    result[..]=...  
    ....
}       
render result as JSON

效果图如下:

有的时候需要为ComboBox赋初始值,这个初始值可以是任意的字符串或者从Domcain Class中选择出来的。对于这样的需求,采用下述代码实现:

def results=[]   //存放结果的List
def domainname    = params.db_table //Domain Class
def q_word      = params.q_word     //查询条件
def field       = params.field      //初始值要显示的字段,缺省值为name
def primary_key = params.primary_key   //查询字段,缺省值为id
def fieldInBF = primary_key[0].toUpperCase() + primary_key[1..-1]

def domain= grailsApplication.getClassForName(domainname)

if(!q_word.equals('') && !q_word.equals('null')){
   def domains
   
   if(primary_key.equals('id'))
       domains= domain."findBy${fieldInBF}"(Long.parseLong(q_word))
   else 
       domains= domain."findBy${fieldInBF}"(q_word)
   
   domains.each{
       results<<it."$field"
   }
}
render results as JSON

好了,至此您应该已经了解这个小工具了,下面说明若干用法。

使用分页参数

对于大规模的数据,如果不分页,那势必得等到数据加载完全之后,界面才能有响应。这不仅对于用户体验来讲是件糟糕的事情,而且浪费了内存和带宽。这个jQuery的插件对此提供了分页支持。这也是我们推荐它的主要原因。分页涉及如下两个参数:

  • pageSize:每页显示的记录数,缺省值为10;
  • naviNum:显示的页码数,缺省值为5。

用法:

<jqac:domain id="role"   style="width:260px" domain="jqac.Role"
   pageSize="2"  
   naviNum="4"  
/>

效果如下:

查询结果排序

排序涉及如下两个参数:

  • orderField:排序字段,缺省为name字段;
  • orderBy:排序属性,缺省为ASC。

用法:

<jqac:domain id="role" style="width:260px" domain="jqac.Role"
   orderField="id"  
   orderBy="DESC"  
/>

效果如下:

显示附加信息

如果ComboBox中显示的是某个部门的人员名称,出现同名的情况该如何区分同名的人员呢?此时可以使用附加信息加以区别。附加信息及如下参数:

  • subInfo:将其值设置为"true",缺省会显示id以及name的附加信息,必须;
  • subSet:设置需要显示的附加信息以及字段名的显示;
  • showField:设置附加信息要显示的字段,如果字段为多个,可用逗号分隔,与subSet一同使用;
  • hideField:设置附加信息要隐藏的字段,如果字段为多个,可用逗号分隔,与subSet一同使用;

用法组合可以是:subInfo、subInfo + subSet、subInfo + subSet + showField、subInfo + subSet + hideField、subInfo + subSet + showField + hideField。

如下是一种用法示例:

 <jqac:domain id="subinfo" style="width:260px" domain="jqac.Role"
    subInfo="true"
    subset="[id:'ID', name:'Name', version:'Version']"
  />

效果如下:

ComboBox组

要使用ComboBox组,将package设置为true即可,缺省值为false,即不使用ComboBox组。

用法示例:

 <jqac:domain id="role-id" style="width:260px" domain="jqac.Role" 
   package="true"
/>

这里需要注意,生成的inpu框的id会根据ComboBox的个数来递增,形如:role-id_1,role-id_2...

效果如下:

初始化

有的时候需要给input框设置初始值,这个时候可以使用initVal参数,解释如下:

  • initVal:input的初始值,可以是任意的字符串,也可以是Domain Class的某个字段的值,如果使用了package=true,initVal则可以设置为用逗号分隔的多个值;
  • selectOnly:如果设置成true,表示input中的内容来自于ComboBox,该属性可单独使用,缺省值为false。如果input的初始值是从Domain Class获得,需要这个属性为true。否则,input的初始值将是initVal的值;
  • primaryKey:如果input的初始值是从Domain Class获得,这个属性是指定input的初始值来自于Domain Class的哪个字段。

使用组Combobox,input的初始值来自于jqac.Role的name字段,用法如下:

 <jqac:domain id="inival3" style="width:260px" domain="jqac.Role" 
   primaryKey="name"
   selectOnly="true" 
   initVal="'admin','guest'" 
   package="true" 
 />

效果如下:

如果initVal设置的是id字段的值,用法如下:

<jqac:domain id="inival3" style="width:260px" domain="jqac.Role" 
  primaryKey="id"
  selectOnly="true" 
  initVal="5,3"
  package="true"
 />

效果如下:

提交选择内容

上面讲述的是ComboBox中查询结果显示的各种用法,下面重点说说选择内容后,如何提交。这就需要注意两个参数:

  • selectOnly,为true时,表面意思是input中的内容必须是由从ComboBox中选择,隐含意思是会生成一个隐藏的input框,它的name属性与页面显示的input的name相同,用于存放primaryKey指定的字段内容(通过FireBug即可知道这一点)。
  • primaryKey,这个属性跟数据表中的primaryKey不一样。它用于设置页面POST时哪个字段的值被提交,即将field指向的字段以及primaryKey指向的字段以[field,primaryKey]的形式提交。该属性的缺省值为id。

这里需要注意,如果selectOnly设置为false,将不会生成隐藏的input框,那么primaryKey中设置的字段内容将不会被提交。

下面以User的新增和修改为例。在User的新增页面中可以选择Role,页面代码为

<jqac:domain id="roleid" style="width:260px" domain="jqac.Role" 
   selectOnly="true" 
   primaryKey="id" />

上述代码生成的HTML代码如下图:

注意上图中的两个input的name属性均为roleid_1。在页面提交后,Controller中通过params.roleid_1就能获得这两个input的值,形如:[admin,1]。要获得id,使用params.roleid_1[1]即可。比如:

def userInstance = new User(params)
userInstance.role=Role.get(Long.parseLong(params.roleid_1[1]))
userInstance.save()

在User的修改页面中,需要显示这个用户所属的Role,见如下代码:

<jqac:domain id="roleid" style="width:260px" domain="jqac.Role" 
   selectOnly="true" 
   initVal="${userInstance?.role?.id}" 
   primaryKey="id"/>

如果使用组ComboBox,比如新增Role的时候,可以选择多个User,页面代码:

<jqac:domain id="userid" style="width:260px" domain="jqac.User" 
   selectOnly="true" 
   primaryKey="id" 
   package="true" />

由于这里可以使用多个ComboBox,这就需要记录ComboBox的个数。由于原先的jquery.ajaxComboBox没有这个功能,我对这个js稍作改动,添加了一个名为packagenum的隐藏input框,用于记录ComboBox的个数。具体代码参见jquery.ajaxComboBox.3.5.min.js中注释为“add by huwh”的内容。

对于上述代码,页面提交后,Controller中的处理如下:

def roleInstance = new Role(params)
if(params.packagenum){
   Long.parseLong(params.packagenum).times{
      if(!params."userid_${it+1}"[1].equals(""))
         roleInstance.addToUsers(User.get(Long.parseLong(params."userid_${it+1}"[1])))
   }
}
roleInstance.save()

由于Role的User可能是多个,那么在Role的修改页面,同样需要使用组ComboBox,并且需要赋初值。

<jqac:domain id="userid" style="width:260px" domain="jqac.User" 
   selectOnly="true" 
   initVal="${userids}" 
   package="true" 
   primaryKey="id"/>

这里需要注意userids是经过预处理的,为该Role的多个User的id字符串,形如:2,4,5。

洋洋洒洒写了这么多,感谢您认真读到这里。以上就是关于这个例程的基本介绍,因为后来没有打算作为插件发布,因此代码并没有经过严格的测试,也没有测试代码。有兴趣的读者可以下载下来自行修改使用,本例程的所有代码均在Apache许可证V2下发布。

By huwh - Posted on 10 十二月 2010