前言
最近在做一个OA系统,需要将大量excel中的数据录入,并且希望以后新建数据时,也能像excel那样方便。并且这个后台系统有非常多这样的表单。因此需要做一个内敛的表单控件。
上网找到一款编辑起来非常方便的控件
handsontable,这个表单控件可以支持excel中的多种操作,比如多行复制粘贴,ctrl+z撤销,ctr+r重做等等。但是这个插件对数据的提交和数据的校验做的并不好。于是自己对这个插件进一步封装成为一个控件。
写好后的控件
演示地址目标
控件希望至少做到的下几点
- 当某一行有单元格发生变化时,自动校验这一行的数据,如果校验成功,将数据post到一个save url,post能绑定固定参数。
- 当有多行发生变化时,依次校验各行,依次提交校验通过的数据。
- 当最后一行数据发生变化时,不管是否已经提交,都立即在后面追加一行。
- 当保存的行是新建的时候,要将响应的记录ID更新到table上。
- 绑定table.render刷新事件,当刷新时间被触发时,重新渲染列表。
- 绑定add.table_id事件,事件将绑定一行新的数据(由触发的时候传递过来),响应事件时,将这行新的记录追加到表格中。
- 选择多行快速删除,get一个请求传递参数ids(逗号分隔id)
使用
首先看看如何使用这个控件,只需要像下面这样
<?php
$data = array(
'rows' => array(
array('id'=>1,'username'=>'lvyahui','email'=>'[email protected]','phone'=>'9999','group'=>'组1','group_id'=>1),
array('id'=>2,'username'=>'lvyahui','email'=>'[email protected]','phone'=>'9999','group'=>'组2','group_id'=>2),
array('id'=>3,'username'=>'lvyahui','email'=>'[email protected]','phone'=>'9999','group'=>'组1','group_id'=>1),
array('id'=>4,'username'=>'lvyahui','email'=>'[email protected]','phone'=>'9999','group'=>'组3','group_id'=>3),
),
);
?>
<div id="dataTable">
</div>
{{HTML::script('js/handsontable.full.min.js')}}
{{HTML::style('css/handsontable.full.min.css')}}
{{HTML::script('js/editTable.js')}}
<script>
$('#dataTable').initEdit({
rows : JSON.parse('<?=json_encode($data['rows'])?>'),
colHeaders : ['ID','用户名','邮箱','电话号码'],
columns : {
id : {
readOnly : true
},
username : {
label : '用户名',
required : true,
validator : /^\w+$/
},
email :{
required : true,
//validator : /^\w+$/
editor: 'select',
selectOptions : ['[email protected]','[email protected]']
},
phone :{
validator : function(value,callback){
//return value.length > 1;
callback(true);
return true;
},
allowVaild : true
},
group : {
required : true,
editor: 'select',
selectOptions : ['组1','组2','组3']
}
},
bindData : {
cus : 1,
category_id : 2
},
beforeSave : function(data){
var groups = [{name:'组1',id:1},{name:'组2',id:2},{name:'组3',id:3}];
var has = groups.filter(function(item){
if(item.name == data.group){
return true;
}else{
return false;
}
});
if(has.length > 0){
data.group_id = has[0].id;
}
return data;
},
afterSave : function(resp){
},
url : {
save : '<?=URL::to('test/table-row')?>',
delete : '<?=URL::to('test/table-delete')?>'
}
});
$('body').trigger('add.dataTable',{
id : 111,
username : 'devlyh',
email : '[email protected]',
phone : '100000',
gourp_id : 1
});
</script>
上面的代码体现了设计思路,提交数据的时候,提交的是rows的某一行,显示的时候,只显示columns中有的属性。对于关系型数据,可以在提交数据之前将关系数据绑定提交。最下面触发的add.table_id(之所以事件跟一个table_id是为了保证一个页面有多个这个table的时候不冲突)事件,新增了一条数据。
你就可以看到生成了这样一个表格。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIn5GcuADZhJDN1MDZkN2MhhjMhNzN1YGOhZWY0cjM1MWMjNGMfdWbp9CXt92Yu4GZjlGbh5SZslmZxl3Lc9CX6MHc0RHaiojIsJye.png)
下面以一个实际的例子为例
1 <div class="table" id="editTable"></div>
2 {{HTML::script('js/handsontable.full.min.js')}}
3 {{HTML::style('css/handsontable.full.min.css')}}
4 {{HTML::script('js/editTable.js')}}
5
6 <script>
7 $('#editTable').initEdit({
8 rows : [<?= implode(",",array_map(function($item){
9 return "{id:$item->id,serial:'$item->serial',name:'$item->name',store:'$item->store',ship_time:'$item->ship_time',number:$item->number}";
10 },$model->items->all()));?>],
11 columns : {
12 id : {
13 label : 'ID',
14 readOnly : true
15 },
16 store : {
17 label : 'PO #',
18 required: true
19 },
20 serial : {
21 label : '产品ID',
22 required : true
23 },
24
25 name :{
26 label : '产品名称',
27 required : true
28 },
29
30 description: {
31 label : '产品描述',
32 required : false
33 },
34
35 ship_time : {
36 label : '发货截止时间',
37 required : true
38 },
39 number : {
40 label : '数量',
41 required : true
42 }
43 },
44 bindData : {
45 customer_orders_id : '<?=$model->id?>'
46 },
47 url : {
48 save : '<?=URL::to('customerOrderItem/edit')?>',
49 delete : '<?=URL::to('customerOrderItem/delete')?>'
50 }
51 });
52 </script>
效果
生成的表单就像这样
下面来批量新建,可以看到但出现两行符合要求是,向后台post了两个请求,请求响应了成功,将ID更新到单元格
修改单元格
触发add.table_id事件,向表格添加一行数据
1 $("body").delegate(".select-item", "click", function (e) {
2 var m = $("#add-product");
3 $.get($(this).attr('href')+'&type='+$that.data('type'),function(resp){
4 console.log(resp);
5 resp.number = resp.number || 0;
6 $('body').trigger('add.'+$that.data('table'),resp);
7 m.modal('hide');
8 },"json");
9 return false;
10 });
触发刷新
$('.order-item').one('shown.bs.collapse',function(){
$('div.edit-table').trigger('table.render');
});
多行删除,这里因为后台还没做,所以会报错,但是数据时请求到了delete url上的
代码
下面是这个控件的核心代码。
1 /*
2 * editTable.js
3 */
4 ;(function($,global){
5
6 var objToArray = function(obj){
7 var arr = [];
8 for(var x in obj){
9 arr.push(obj[x]);
10 }
11 return arr;
12 },
13 requiredRender = function(hot, td, row, col, prop, value, cellProperties){
14 Handsontable.renderers.TextRenderer.apply(this, arguments);
15 td.className += 'required';
16 //td.style.backgroundColor = 'yellow';
17 };
18
19 var ExcelTable = function(element,options){
20 var that = this;
21
22 this.element = element;
23 this.hot = null;
24 this.edit = null;
25 this.defaults = {
26 bindData : {},
27 rows : [],
28 url : {
29 save : '',
30 delete : ''
31 },
32 columns : {},
33 tableClassName : '',
34 afterSave : function(resp){},
35 beforeSave : function(data){}
36 };
37
38 this.options = $.extend({},this.defaults,options);
39 this.rows = this.options.rows;
40 //this.cols = objToArray(that.options.columns);
41 var colHeaders = [],
42 columns = [];
43 for(var x in this.options.columns){
44 if(this.options.columns[x].hasOwnProperty('label')){
45 colHeaders.push(this.options.columns[x].label);
46 }else{
47 colHeaders.push(x);
48 }
49 columns.push($.extend({data:x},this.options.columns[x]));
50 }
51 this.cols = columns;
52 var hotOptions = {
53 data : options.rows,
54 colHeaders : colHeaders,
55 afterChange : function(changes,source){
56 if(source !== 'loadData'){
57 that.save(changes);
58 }
59 },
60 beforeRemoveRow:function(index,amount){
61 that.delete(index,amount);
62 },
63 columns : columns ,
64 minSpareRows: 1,
65 contextMenu: ['remove_row'],
66 cells: function (row, col, prop) {
67 if(col < that.cols.length && that.cols[col].required){
68 this.renderer = requiredRender;
69 }
70 },
71 tableClassName : this.options.tableClassName,
72 width : '100%',
73 stretchH: "all",
74 colWidths : this.options.colWidths
75 };
76
77 if(typeof Handsontable === "function"){
78 this.hot = new Handsontable(this.element,hotOptions);
79 }
80
81 $('body').bind('add.'+$(element).attr('id'),function(e,row){
82 that.rows.splice(that.rows.length-1,0,row);
83 that.hot.render();
84 });
85 $(element).bind('table.render',function(){
86 that.hot.render();
87 });
88 };
89
90 ExcelTable.prototype = {
91 constructor : ExcelTable,
92 post : function(data){
93 var that = this;
94 var ret = this.options.beforeSave(data);
95 if(typeof ret === "object"){
96 data = ret;
97 }
98 if(data.id){
99 data.action = 'edit';
100 }else{
101 data.id = '';
102 data.action = 'edit';
103 }
104 if(this.options.url.save){
105 $.post(this.options.url.save,data,function(resp){
106 if(!data.id && resp.id !== null){
107 // 新建了记录,重新渲染
108 that.options.rows[resp.index].id = resp.id;
109 that.hot.render();
110 }
111 that.options.afterSave(resp);
112 },'json');
113 }
114 },
115 getDelete : function(ids){
116 if(this.options.url.delete){
117 $.get(this.options.url.delete+'?id='+ids,function(resp){
118
119 });
120 }
121 },
122 save : function(cells){
123 var that = this,
124 rows = [];
125 cells.forEach(function(cell){
126 if(cell[2] !== cell[3]){
127 rows[cell[0]] = cell[0];
128 }
129 });
130 rows.forEach(function(rowIndex){
131 var row = that.rows[rowIndex],
132 data = $.extend(row,that.options.bindData);
133 data.index = rowIndex;
134 if(that.validate(row)){
135 console.log(data);
136 that.post(data);
137 }
138 });
139 },
140 delete : function(start,amount){
141 var ids = [];
142 for(var x = start;x < start + amount;x++){
143 ids.push(this.rows[x].id);
144 }
145 this.getDelete(ids.join(','));
146 },
147 validate : function(row){
148 var that = this;
149 return that.cols.filter(function(col,index){
150 if(row.hasOwnProperty(col.data)){
151 var valitator = that.hot.getCellValidator(0,index);
152 if(col.required){
153 if(!row[col.data]) return true;
154 if(valitator){
155 return !that.execValidator(valitator,row[col.data]);
156 }
157 return false;
158 }else if(row[col.data] && valitator){
159 return !that.execValidator(valitator,row[col.data]);
160 }else{
161 return false;
162 }
163 }
164 else{
165 return false;
166 }
167 }).length == 0;
168 },
169 execValidator:function(validator,value){
170 if(validator instanceof RegExp === true){
171 return validator.test(value);
172 }else if(typeof validator === "function"){
173 return validator(value,function(){});
174 }else{
175 return false;
176 }
177 },
178
179 isEmptyRow : function(rowIndex){
180 var rowData = this.hot.getData()[rowIndex];
181
182 for (var i = 0, ilen = rowData.length; i < ilen; i++) {
183 if (rowData[i] !== null) {
184 return false;
185 }
186 }
187 return true;
188 }
189 };
190
191 $.fn.initEdit = function(options){
192 return this.each(function(){
193 var excelTable = new ExcelTable(this,options);
194 });
195 }
196
197 })($ || jQuery,window);