Skip to content
20 changes: 17 additions & 3 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -62,18 +62,32 @@ <h3># form validator</h3>
<input type="text" data-url="https://api.github.com/legacy/user/search/china" data-method="getJSON" required></label></p>

<p><label>A or B:</label>
<input type="text" data-aorb="a" required>
<input type="text" data-limits="1" required name="ab">
<br />
<input type="text" data-aorb="b" required>
<input type="text" data-limits="1" required name="ab">
</p>

<p><label>Checkbox #1:
<small>(at least 1, up to 4)</small></label>
<label><input type="checkbox" data-limits="1,4" required name="abcde" value="a">a</label>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

会不会 data-group=3-1, data-group=3-2, data-group=3-3 比较好?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

如果是用作分组,我也考虑过用 data-group,只是 3-1 这样区分每一个元素可能没什么必要,因为需要额外处理。

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

看这个实现:

linkageValidate = function($item, klass, isErrorOnParent){

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3-1 看上去虽然比更适合,但是操作难度更大,以社交类表单的兴趣调查为例,假设已有 12 个选择项,12-N,如果这时候增删选项,意味着所有 HTML 都可能需要改,如果需要改动顺序,那更容易改错。

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

考虑一下循环输出,代码总有办法 dry,而且,1,4 这种也需要把所有项变成 1,3 如果删除一项的话;另外,从语义上看,3-1 这种更好理解。大家都知道是3个中的第1个

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1,4 这种其实会忽略掉后面的,只取第一个的值,至于用 , 是从正则那借过来,因为这货和 3-1 的区别在于,它只是一个区间。

3-13-23-3 只能告诉别人「有多少个」、「这是第几个」,但 1,4 的意思则是「至少 1 个,最多 4 个」。

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3-13-2 这种方法用在 group 上我没意见,但是用在 limits 上就不适合,如果只是用 - 代替 , 那也可以,但也是用在 data-limits 上,而不是 data-group

<label><input type="checkbox" data-limits="1,4" required name="abcde" value="b">b</label>
<label><input type="checkbox" data-limits="1,4" required name="abcde" value="c">c</label>
<label><input type="checkbox" data-limits="1,4" required name="abcde" value="d">d</label>
<label><input type="checkbox" data-limits="1,4" required name="abcde" value="e">e</label>

<p><label>Checkbox #2:
<small>(can be 0, up to 2)</small></label>
<label><input type="checkbox" data-limits=",2" required name="123" value="1">1</label>
<label><input type="checkbox" data-limits=",2" required name="123" value="2">2</label>
<label><input type="checkbox" data-limits=",2" required name="123" value="3">3</label>

<p>
<label><input type="radio" required name="abc" value="A">[A]</label>
<label><input type="radio" required name="abc" value="B">[B]</label>
<label><input type="radio" required name="abc" value="C">[C]</label>
<p>

<p><label>Custom Event: <input id="event" type="text" data-event="hello" required></label>
<p><label>Custom Event: <input id="event" type="text" data-event="hello" required></label>

<p><textarea placeholder="some text" required data-error="textarea"></textarea></p>
<input type="submit" value="send"/>
Expand Down
128 changes: 74 additions & 54 deletions validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

var patterns, fields, errorElement, addErrorClass, removeErrorClass, novalidate, validateForm
, validateFields, radios, removeFromUnvalidFields, asyncValidate, linkageValidate
, aorbValidate, validateReturn, unvalidFields = []
, limitsValidate, validateReturn, unvalidFields = []

// 类型判断
patterns = {
Expand Down Expand Up @@ -115,6 +115,11 @@
return result;
},

// 目前只检验是否已选,多选需结合 limitsValidate
checkbox: function(){
return !!this.$item.attr('checked');
},

// text[notEmpty] 表单项不为空
// [type=text] 也会进这项
text: function(text){
Expand Down Expand Up @@ -148,44 +153,39 @@
});
}


// 二选一:二个项中必须的一个项是已经填
// <input data-aorb="a" >
// <input data-aorb="b" >
aorbValidate = function($item, klass, isErrorOnParent){
var id = $item.data('aorb') === 'a' ? 'b' : 'a'
, $pair = $('[data-aorb=' + id + ']', $item.parents('form').eq(0))
, a = [$item, klass, isErrorOnParent]
, b = [$pair, klass, isErrorOnParent]
// 把有相同 name 属性的元素编为一组,判断组内所有合法元素数量是否在范围内
// limits: String, 'min,max', 'min,' 只有最小值,',max' 只有最大值,'n' 则只允许有 n 个
limitsValidate = function($item, klass, parent){
var $items = $item.closest('form').find('[type=' + $item.attr('type') + '][name="' + $item.attr('name') + '"]')
, limits = $item.attr('data-limits')
, taste = /^(\d)*( *, *)*(\d)*$/.exec(limits) || []
, min = parseInt(taste[1], 10)
, max = parseInt(taste[3], 10)
, result = 0
, _unvalidFields = []
, message, field

result += validateReturn.apply(this, a) ? 0 : 1
result += validateReturn.apply(this, b) ? 0 : 1;
$.each($items, function(idx, item){
result += true === validate.apply(this, [$(item), klass, parent]) ? 1 : 0
})

isNaN(min) && (min = 0)
;(isNaN(max) || !taste[2]) && (max = result)
// FIXME: different error messages for different elements
;(min > result || result > max) && (message = 'unvalid')

result = result > 0 ? (removeErrorClass.apply(this, a), removeErrorClass.apply(this, b), false) : true;
// TODO: better way to handle error elements
$.each($items, function(){
field = validateReturn.apply(this, [$(this), klass, parent, message])
field && _unvalidFields.push(field)
})

// 通过则返回 false
return result;
return _unvalidFields;
}

// 验证后的返回值
validateReturn = function($item, klass, parent, message){

if(!$item) return 'DONT VALIDATE UNEXIST ELEMENT';

var pattern, type, val, ret

pattern = $item.attr('pattern');
type = $item.attr('type') || 'text';
val = $item.val().trim();
event = $item.data('event');

// HTML5 pattern 支持
// TODO: new 出来的这个正则是否与浏览器一致?
message = message ? message :
pattern ? (new RegExp(pattern).test(val) || 'unvalid') :
patterns[type](val) || 'unvalid';

var ret
// 返回的错误对象 = {
// $el: {jQuery Element Object} // 当前表单项
// , type: {String} //表单的类型,如 [type=radio]
Expand All @@ -194,10 +194,10 @@
// NOTE: 把 jQuery Object 传到 trigger 方法中作为参数,会变成原生的 DOM Object
return /^(?:unvalid|empty)$/.test(message) ? (ret = {
$el: addErrorClass.call(this, $item, klass, parent)
, type: type
, type: $item.attr('type') || 'text'
, error: message
}, $item.trigger('after:' + event, $item), ret):
(removeErrorClass.call(this, $item, klass, parent), $item.trigger('after:' + event, $item), false);
}, $item.trigger('after:' + $item.data('event'), $item), ret):
(removeErrorClass.call(this, $item, klass, parent), $item.trigger('after:' + $item.data('event'), $item), false);
}

// 获取待校验的项
Expand All @@ -208,37 +208,42 @@
// 校验一个表单项
// 出错时返回一个对象,当前表单项和类型;通过时返回 false
validate = function($item, klass, parent){
var async, aorb, type, val, commonArgs

// 把当前元素放到 patterns 对象中备用
patterns.$item = $item;
type = $item.attr('type');
val = $item.val();
if(!$item) return 'DONT VALIDATE UNEXIST ELEMENT';

var async, type, val, limits, pattern, message

async = $item.data('url');
aorb = $item.data('aorb');
event = $item.data('event');

commonArgs = [$item, klass, parent]

// 当指定 `data-event` 的时候在检测前触发自定义事件
// NOTE: 把 jQuery Object 传到 trigger 方法中作为参数,会变成原生的 DOM Object
event && $item.trigger('before:' + event, $item);

// 异步验证则不进行普通验证
if(async) return asyncValidate.apply(this, [$item, klass, parent]);

// 把当前元素放到 patterns 对象中备用
patterns.$item = $item;
type = $item.attr('type') || 'text';
val = $item.val();
limits = $item.attr('data-limits') || false

// 所有都最先测试是不是 empty,checkbox 是可以有值
// 但通过来说我们更需要的是 checked 的状态
// 暂时去掉 radio/checkbox/linkage/aorb 的 notEmpty 检测
if(!(/^(?:radio|checkbox)$/.test(type) || aorb) && !patterns['text'](val))
return validateReturn.call(this, $item, klass, parent, 'empty')

// 二选一验证:有可能为空
if(aorb) return aorbValidate.apply(this, commonArgs);
if (!limits && !/^(?:radio|checkbox)$/.test(type) && !patterns['text'](val))
return validateReturn.call(this, $item, klass, parent, 'empty');

// 异步验证则不进行普通验证
if(async) return asyncValidate.apply(this, commonArgs);
pattern = $item.attr('pattern');
// HTML5 pattern 支持
// TODO: new 出来的这个正则是否与浏览器一致?
message = message ? message :
pattern ? (new RegExp(pattern).test(val) || 'unvalid') :
patterns[type](val) || 'unvalid';

// 正常验证返回值
return validateReturn.call(this, $item, klass, parent);
return limits ? message : validateReturn.call(this, $item, klass, parent, message);
}

// 校验表单项
Expand All @@ -247,22 +252,36 @@
var field
$fields.on(method, function(){
// 如果有错误,返回的结果是一个对象,传入 validedFields 可提供更快的 `validateForm`
(field = validate.call(this, $(this), klass, parent)) && unvalidFields.push(field);
if (this.getAttribute('data-limits')) {
unvalidFields = unvalidFields.concat(limitsValidate.apply(this, [$(this), klass, parent]))
} else {
(field = validate.call(this, $(this), klass, parent)) && unvalidFields.push(field);
}
})
}

// 校验表单:表单通过时返回 false,不然返回所有出错的对象
validateForm = function ($fields, method, klass, parent) {
if(method && !validateFields.length) return true;
var field
var field, groups = {}

// 防止 push 重复项
unvalidFields = [];

$fields.each(function(i) {
(field = validate.call(this, $(this), klass, parent)) && unvalidFields.push(field);
$fields.each(function() {
if ($(this).attr('data-limits')) {
groups[this.name] instanceof Array || (groups[this.name] = [])
if (1 < groups[this.name].length) return;
groups[this.name].push(this)
} else {
(field = validate.call(this, $(this), klass, parent)) && unvalidFields.push(field)
}
})

for (var k in groups) {
unvalidFields = unvalidFields.concat(limitsValidate.apply(this, [$(groups[k]), klass, parent]))
}
// FIXME: Checkbox demo 2: validateFields.length alway equal true, can't return invalid elements
return validateFields.length ? unvalidFields : false;
}

Expand Down Expand Up @@ -334,6 +353,7 @@

// 提交校验
$form.on('submit', function(e){
e.preventDefault()
before.call(this, $items);
validateForm.call(this, $items, method, klass, isErrorOnParent);

Expand Down