Grails完美解决一对多(One to many)Model动态表单的自动组装

这里的试验是在Grails下做的,但是在Spring MVC中应该也能适用,即便不行,通过简单的扩展也能达到效果。对于其他框架像Struct之类应该也都没问题(不了解,不确定)。

能实现的自动封装包含:

1.一层简单模型

[java]  view plain copy
  1. class AddressCommand {  
  2.     String city  
  3.     String street  
  4.     int hourseNumber  
  5. }  

2.One to one,多层模型

[java]  view plain copy
  1. class PersonCommand {  
  2.     String name  
  3.     int age  
  4.     AddressCommand address  
  5. }  

3.One to many, 多层模型

[java]  view plain copy
  1. class ChildCommand {  
  2.     String name  
  3.     int age  
  4. }  
  5.   
  6. class PersonCommand {  
  7.     String name  
  8.     int age  
  9.     AddressCommand address  
  10.     List children  
  11. }  

这个模型模型就是我们最终要使用的模型。这里我们不讨论Many to One 和 Many to Many模型,不常见(我还没遇到过),而且也可以通过转换转成以上几种类型;例子中的模型只有两层,多层的依葫芦画瓢,原理不变,增加了一些复杂性。

我们先创建一个Controller,之后我们都会把数据Post到save Action,如果数据对的话,Grails就会帮助自动组装好模型:

[java]  view plain copy
  1. class PersonController {      
  2.     def save={PersonCommand person->       
  3.           
  4.     }  
  5. }  

一.数据传输格式

数据传输格式是最重要的问题,直接关系到后续过程中前,后台的工作量。

对于一层简单模型和One to one多层模型,数据格式是很成熟的。现有的框架都处理的很好,对于上面的model,他们的数据格式如下:

NameValue
nameyanical
age26
address.cityZhuhai
address.streetJida
address.hourseNumber344

其实我们要解决的主要是一对多的问题。一对多模型里面有个list(array)对象,在form里的数据是扁平的,我们要在扁平的数据里带上list顺序特性。

对于Grails,我尝试了几种数据格式,比如:

NameValue
children.0.namelittle
children.0.age2
都没有成功,最后在网上别人建议了如下的格式:

NameValue
children[0].namelittle
children[0].age2
children[1].namelarge
children[1].age4

这里List有两个元素。这个时候还是没有成功,但是出了如下的exception:

[java]  view plain copy
  1. Stacktrace follows:  
  2. java.lang.IndexOutOfBoundsException: Index: 0, Size: 0  
  3.     at java.util.ArrayList.RangeCheck(ArrayList.java:547)  
  4.     at java.util.ArrayList.get(ArrayList.java:322)  
  5.     at java.lang.Thread.run(Thread.java:619)  

这个exception说明,grails的form->model模型组装模块已经识别了上传的数据,只是因为我们的list是空的,在组装模型的时候Grails试图从List中找到对应的element来塞数据,但是没有成功。修改Person类后,模型就可以成功组装了。

[java]  view plain copy
  1. class PersonCommand {  
  2.     String name  
  3.     int age  
  4.     AddressCommand address  
  5.     List children = []  
  6.       
  7.     Person() {  
  8.         children[0] = new ChildCommand()  
  9.         children[1] = new ChildCommand()  
  10.     }  
  11. }  

二.子对象的初始化

上面我们发现对于根对象Person,和One to one的对象,Grails都会帮忙创建对象实例,但是对于One to many,Grails并没有做相关工作。我们要想个方案能够在组装模型前,能把对应的对象实例都创建好。

有两种思路:1.是在代码里就先hardcode创建好一定数量的实例(像前面做的那样),比如10个,在能确定实例数量会很小的时候,这种方案也还可行。2.是考虑可以不可以通过插入一些代码,实现在组装模型的时候动态创建实例。 

这里的实现采用第二种思路。

看回上面的Exception,这个exception是去list里取数据的时候发现数据不存在的时候抛出来的。说明数据组装模块会去List里拿实例,再把数据塞到实例里面去,我们就在数据组装模块去List拿数据的时候插入一些代码。

我们使用一个工具来达到这个目的,这个工具就是org.apache.commons.collections.list.LazyList,LazyList在我们这里的作用就是确保数据组装模块去List取数据的时候始终能够把实例取出来。在List的里面有这个对象实例的时候,就把对象实例返回,否则就创建一个新的对象实例。修改Person类如下:

[java]  view plain copy
  1. class PersonCommand {  
  2.     String name  
  3.     int age  
  4.     AddressCommand address  
  5.     List children = LazyList.decorate([],  
  6.               FactoryUtils.instantiateFactory(ChildCommand.class))  
  7. }  

这个时候模型就已经可以成功创建了,即便List里面会有很多的元素,也能很好的处理。

三.Form表单定义

接下来,前台的任务就是要组装出前面要求的数据格式,对于One to one,我们还是一笔带过,form表单如下:

[html]  view plain copy
  1.   
[html]  view plain copy
  1. <input type="text" name="name" id="name" value="">  
  2. <input type="text" name="age" id="age" value="">  
  3. <input type="text" name="address.city" id="age" value="">  
  4. <input type="text" name="address.street" id="age" value="">  
  5. <input type="text" name="address.hourseNumber" id="age" value="">  


对于One to many,页面中肯定会有Add和Delete的地方来增加和删除某一个元素。由于增加或者删除元素的过程中会调整UI,所以希望能够提取出一段公用的JS来实现这个功能。代码大致如下,包含两个方法onOneToManyAddClick和onOneToManyDeleteClick ,分别用来处理Add和Delete点击事件(基于JQuery,现在不需要理会这段代码的细节):

[javascript]  view plain copy
  1. function onOneToManyAddClick(triggerElement, oneToManyProperty) {  
  2.     var table = $("table[scaffoldFor='"+oneToManyProperty+"']");  
  3.     var count = table.attr("count");  
  4.     if(count == ""){  
  5.         count = 0;  
  6.     }  
  7.       
  8.     var tr = $("tr[scaffoldFor='"+oneToManyProperty+"']").filter(function() {  
  9.         return $(this).is(":hidden");  
  10.     }).clone();  
  11.           
  12.     tr.attr("index",count);  
  13.     var inputs = $(tr).find(":input");//has include input, textarea, select and button  
  14.     $.each(inputs, function(i, n) {  
  15.         var input = $(n);  
  16.         var name = input.attr("name");  
  17.         name = name.replace("*","["+count+"]");  
  18.         input.attr("name",name);  
  19.         input.attr("id",name);  
  20.     });  
  21.           
  22.     count++;  
  23.     table.attr("count", count);  
  24.     tr.appendTo(table)  
  25.     tr.show();  
  26. }  
  27. function onOneToManyDeleteClick(triggerElement, oneToManyProperty) {  
  28.     var current = $(triggerElement);  
  29.     var currentTr;  
  30.     while(true) {  
  31.         if(current.is("tr") && current.attr("scaffoldFor") && oneToManyProperty.indexOf(current.attr("scaffoldFor"))>=0) {  
  32.             currentTr = current;  
  33.             break;  
  34.         } else {  
  35.             current = current.parent();  
  36.         }  
  37.     }  
  38.           
  39.     var currentIndex = currentTr.attr("index");  
  40.     var currentFor = currentTr.attr('scaffoldFor');  
  41.           
  42.     var table = $("table[scaffoldFor='"+currentFor+"']");  
  43.     var count = table.attr("count");  
  44.           
  45.     //remove element -- start  
  46.     var nextTrs = currentTr.nextAll("tr").filter(function() {  
  47.         return $(this).attr("scaffoldFor") == currentFor;  
  48.     });  
  49.     $.each(nextTrs, function(i, n) {  
  50.         var tr = $(n);  
  51.         var index = tr.attr("index");  
  52.         var replaceFrom = currentFor+"["+index+"]";  
  53.         var replaceTo = currentFor+"["+(index-1)+"]";  
  54.         tr.attr("index", (index-1));  
  55.         $.each($(tr).find(":input"), function(i, n) {  
  56.             var input = $(n);  
  57.             var name = input.attr("name");  
  58.             name = name.replace(replaceFrom, replaceTo);  
  59.             input.attr("name",name);  
  60.             input.attr("id",name);  
  61.         });  
  62.     });  
  63.           
  64.     if(count == ""){  
  65.         table.attr("count", 0);  
  66.     }  
  67.     count--;  
  68.     table.attr("count", count);  
  69.       
  70.     currentTr.remove();  
  71.     //remove element -- end  
  72. }  

而,我们的One to many form表单就是这样的了:

[html]  view plain copy
  1. <table scaffoldfor="children" count="1">  
  2.     <tbody>  
  3.         <tr scaffoldfor="children" style="display: none">  
  4.             <td>  
  5.                 <input type="text" name="children*.name" id="children*.name" value="">   
  6.                 <input type="text" name="children*.age" id="children*.age" value="">  
  7.                 <span>   
  8.                     <a href="#" onclick="onOneToManyDeleteClick(this,'children');return false;">Add</a>  
  9.                     <a href="#" onclick="onOneToManyAddClick(this,'children');return false;">Delete</a>  
  10.                 </span>  
  11.             </td>  
  12.         </tr>  
  13.         <tr>  
  14.         </tr>  
  15.         <tr scaffoldfor="children" index="0">  
  16.             <td>  
  17.                 <input type="text" name="children[0].name" id="children[0].name" value="">   
  18.                 <input type="text" name="children[0].age" id="children[0].age" value="">  
  19.                 <span>   
  20.                     <a href="#" onclick="onOneToManyDeleteClick(this,'children');return false;">Add</a>  
  21.                     <a href="#" onclick="onOneToManyAddClick(this,'children');return false;">Delete</a>  
  22.                 </span>  
  23.             </td>  
  24.         </tr>  
  25.     </tbody>  
  26. </table>  


初始化的时候包含两个TR:第二个是可见的,就是用户打开页面是可以看到的输入框;第一个是隐藏的,用户点击Add的时候,我们会克隆这个TR并append到table最后显示给用户,隐藏的TR的数据不需要提交给后台,我们设置特殊的input name属性(中间包含*,如children*.name),这样后台Grails就会忽略这些input。 

用户在点击Add的时候,我们要修正克隆的input的name熟悉;用户点击Delete的时候,可能要调整现有input的name属性。这些都在JS里实现,详情请参考JS代码,有兴趣的话,现在可以去读那段代码了。

关键字: Grails, Spring MVC,Java web, 一对多, one to may, 动态表单, 自动组装

 版权所有,转发请注明来源

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值