Easyui框架里的validatebox组件在表单相关的一系列组件中占有极其重要的位置,大多表单类组件都直接或者间接的依赖它,所以弄清楚validatabox是如何对用户输入做校验是很有必要的。注意,本篇文章只讨论如何实现校验,而不讨论具体的校验规则。
原理分析:
说到校验,就必须说道事件的监听,做过自动完成同学应该都知道,我们往往选择监听兼容性较好的keyup事件来实现。不过validatebox却不是通过keyup,而是通过监听focus来实现校验的。
这样问题就来了,当用户将焦点放到输入框A上的时候会触发一次绑定focus事件,后面鼠标不移开,继续输入的时候并不会触发focus了,validatebox是如何实现不停监听的呢,熟悉js的朋友可能立刻想到了setTimeout,对就是它。
不过仅仅靠setTimeout还是不够的,还必须要循环或者递归地setTimeout才行,我们看看1.2.4版本的实现源码,或许就能豁然开朗了:
function bindEvents(target){ var box = $(target); var validatebox = $.data(target, 'validatebox'); validatebox.validating = false; box.unbind('.validatebox').bind('focus.validatebox', function(){ validatebox.validating = true; (function(){ if (validatebox.validating) { validate(target); setTimeout(arguments.callee, 200); } })(); }).bind('blur.validatebox', function(){ validatebox.validating = false; hideTip(target); }).bind('mouseenter.validatebox', function(){ if (box.hasClass('validatebox-invalid')) { showTip(target); } }).bind('mouseleave.validatebox', function(){ hideTip(target); }); };
其中,最关键的代码是这一段:
(function(){ if (validatebox.validating) { validate(target); setTimeout(arguments.callee, 200); } })();
大费周折地定义并立即运行匿名函数的目的是什么?知道arguments.callee的朋友,一眼就能看出来了,是为了不停地递归,arguments.callee就是调用运行中函数自身的意思,是一种实现递归的很便捷的方案。
只要满足validatebox.validating条件,就会一直递归调用,而validatebox.validating只有在blur的时候才被设置为false,到这里各位应该能理解validatebox为什么能够实时校验用户的输入内容了。
涉及问题:
form组件的validate方法内部的focus常引发的问题:
老早以前,我写过一篇《My97DatePicker与easyui共用问题》,该文对故障原因作了粗略分析;同时,如果将我上篇文章中扩展combo组件的disableTextbox方法用到表单中,并且最后用form组件的validate方法校验整个form时,便会报错。
为什么form组件的validate方法如此容易引发问题?关键点还是在focus上,先看1.2.4的源码:
function validate(target){ if ($.fn.validatebox) { var box = $('.validatebox-text', target); if (box.length) { box.validatebox('validate'); box.trigger('blur'); var valid = $('.validatebox-invalid:first', target).focus(); return valid.length == 0; } } return true; };
代码首先查找form中带validatebox-text样式的元素,找到后直接调用validatebox组件的validate方法对每一个找到的元素做一次校验,然后主动触发blur事件,最后将焦点房到第一个验证不通过的元素上。
考虑得周全一点的话,如果我查找的box当中,有的元素是disabled掉的呢?disabled的元素上没法focus的,而在1.2.5、1.2.6、1.3版本中,该方法做了稍微改动,这个改动在作者的更新日志中并未提及,拿1.3的压缩码来看:
function _39f(_3a3){ if ($.fn.validatebox) { var box = $(".validatebox-text", _3a3); if (box.length) { box.validatebox("validate"); box.trigger("focus"); box.trigger("blur"); var _3a4 = $(".validatebox-invalid:first", _3a3).focus(); return _3a4.length == 0; } } return true; };
多了行box.trigger("focus");主动触发当前表单内的所有符合条件的输入框使其顺序focus,这行代码可谓杀伤力巨大,真是“宁可枉杀一千,不可漏掉”一个,本来强行将焦点定位到最后一个元素上就有跟disabled元素冲突的风险,这下倒好,这种风险一下子扩散到所有需要校验的元素上了。
既然原因我们已经分析出来了,那么就得想办法解决这个问题,不改源码,那就只能复写form组件的validate方法,首先box.trigger("focus");这句杀伤力巨大的代码需要去掉,再者,我们得想办法解决第一个校验不通过元素就是disabled的情况。
本文给出一个不算太完美的覆写方法:
$.extend($.fn.form.methods, { validate: function(jq){ function showTip(target){ var box = $(target); var msg = $.data(target, "validatebox").message; var tip = $.data(target, "validatebox").tip; if (!tip) { tip = $("<div class=\"validatebox-tip\">" + "<span class=\"validatebox-tip-content\">" + "</span>" + "<span class=\"validatebox-tip-pointer\">" + "</span>" + "</div>").appendTo("body"); $.data(target, "validatebox").tip = tip; } tip.find(".validatebox-tip-content").html(msg); tip.css({ display: "block", left: box.offset().left + box.outerWidth(), top: box.offset().top }); $('.validatebox-tip').bind('mouseover', function(){ var tip = $.data(target, "validatebox").tip; if (tip) { tip.remove(); $.data(target, "validatebox").tip = null; } }); }; function validate(target){ if ($.fn.validatebox) { var box = $(".validatebox-text", target); if (box.length) { box.validatebox("validate"); box.trigger("blur"); var valid = $(".validatebox-invalid:first", target); if(valid.prop('disabled')){ showTip(valid[0]); }else{ valid.focus(); } return valid.length == 0; } } return true; }; return validate(jq[0]); } });
细心的同学应该还是可以通过演示页面找出不足之处的,如果大家有什么好的方法,欢迎留言指教,或者一起讨论。
管理员 世纪之光 : 2012年08月10日13:04:40 -48楼
管理员 世纪之光 : 2012年08月29日09:02:10 -45楼