前言
最近在做一个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'=>'devlyh@163.com','phone'=>'9999','group'=>'组1','group_id'=>1), array('id'=>2,'username'=>'lvyahui','email'=>'devlyh@163.com','phone'=>'9999','group'=>'组2','group_id'=>2), array('id'=>3,'username'=>'lvyahui','email'=>'devlyh@163.com','phone'=>'9999','group'=>'组1','group_id'=>1), array('id'=>4,'username'=>'lvyahui','email'=>'devlyh@163.com','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 : ['lvyahui8@126.com','devlyh@163.com'] }, 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 : 'lvyahui@126.com', phone : '100000', gourp_id : 1 }); </script>
上面的代码体现了设计思路,提交数据的时候,提交的是rows的某一行,显示的时候,只显示columns中有的属性。对于关系型数据,可以在提交数据之前将关系数据绑定提交。最下面触发的add.table_id(之所以事件跟一个table_id是为了保证一个页面有多个这个table的时候不冲突)事件,新增了一条数据。
你就可以看到生成了这样一个表格。
下面以一个实际的例子为例
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);