项目实施中,表单开发是二次开发最重要、最复杂、最不易维护的部分之一。
表单开发中,最常见的场景包括:
- 页面加载后,基于某个属性的值,执行业务逻辑
- 某个表单字段修改后,根据新的值,执行业务逻辑
而表单业务中,最常见的不外乎:
- 显示/隐藏某些表单部分
- 设置/清空表单字段的值
- 启用/禁用/必填/只读表单字段
常规的开发方式一般:
- 在 body 标签的 onload 事件、或者使用 js 绑定表单加载事件来执行业务,如 $j(function(){ .... }}
- 在各个表单字段的 onchange/onclick 事件、或者使用 js 绑定上述事件来执行业务,如 $j("#foo").on("change", function(){ ... }) 、$j(document).on("change", "#bar", function(){ ... })
诚然,上面的开发方式没有任何问题、也被大家所熟知,但是在实际开发过程中,很容易导致大量类似/重复的代码、以及混乱的调用关系让人抓狂、并且难于调试;
因此 BroFWK 提供了一个 JavaScript 的表单规则引擎,以提升表单业务开发的效率与可维护性、并且便于调试。
先看下面的代码,基本上涵盖了表单规则中的所有元素:
$j.formRules({
// 页面元素定义、并绑定事件
define: {
foo1: {onload: true}, // == {selector: "#foo1", on: "change", onload: true},
foo2: {selector: "input[name='foo2']", on: "click"},
foo3: ".foo3" // == {selector: ".foo3", on: "change"}
},
define2: "foo4, foo5", // == ["foo4", "foo5"],
// 其他页面元素定义
defineOnly: {
bar1: ".bar1" // == {selector: ".bar1"}
},
defineOnly2: "bar2, bar3", // == ["bar2", "bar3"]
// 规则定义
ruleFoo1: {
"for": "foo1, foo2" // 或 "foo1, foo2", ["foo1", "foo2"]
"when": function(ss, vals) {
console.log(ss.target); // 使用jQuery包装的、触发事件的页面元素
return vals.foo1 == "123";
},
"when2": "123", // 或 ["123", "456"],表示值为 123 或 456
"then": function(ss, vals) {
ss.foo1.hide();
ss.bar1.show();
},
"then2": {
"*": "hide",
"bar2, bar3": "hide, disable, abc",
".bar4": "show, !disable"
},
"else": function(ss, vals) {
ss.foo1.show();
ss.bar1.hide();
},
"else2": { ... }
"follow": "ruleFoo2" // 或 "ruleFoo2, ruleFoo3"、["ruleFoo2", "ruleFoo3"]
},
// 在 then、else 中执行的自定义操作
handlers: {
abc: function( tgt, ss, vals ) {
...
}
}
})
将上面的代码分为几个部分解释。
- 使用 $j.formRules 来定义规则,参数为一个 Map
- Map 包含 define、defineOnly、ruleXxx、handlers 等几段,除了 define、ruleXxx 外,其他可选
formRules 的第一段为一个或多个 define 元素,这里定义的元素会自动绑定事件(如 onchange),通过事件触发规则。
定义方式可以为 Map 或字符串、数组等,因此为避免重名,可以在 define 后加数字或其他字符区分,如上例分别采用了不同的方式定义了 define 和 define2。
- Map 结构
- key 为定义的名称(在for、各个回调函数中使用)
- 如果 value 也是 Map 结构,则包括:
- selector: HTML元素的jQuery选择器,如果selector未设置,则默认为 "#key值";
- on: 数组或者以逗号分隔的监控事件名列表,默认为 "change",即监控HTML元素的 onchange 事件;
- onload:是否在页面加载完后立即针对本元素运行一次规则引擎。
- 如果value为字符串,则等同于 Map 中的 selector,即 foo:"#foo" 等于 foo:{selector:"#foo"}。
- 为数组或者以逗号分隔的HTML元素ID
如 "foo, bar" 或 ["foo", "bar"],等同于 {foo:{selector:"#foo", on:"change"}, bar:{selector:"#bar", on:"change}}
这里定义的 key,在定义规则时,可以用于规则的 for/when/then/else 等处。
定义不绑定任何事件的表单元素,结构同 define。
一个 formRules 中可以有多个规则,每个规则的名称固定以 rule 开头,其结构为一个 Map,包括:
- for
规则适用范围,数组或者以逗号分隔的定义名(rules.define)列表,只有这些元素触发的(通过下面的follow触发的忽略本参数),才会执行此规则 - when
规则适用条件:- 如果没有设置when条件、或者设为true值、或不为0的数字,则表示始终满足条件;如果为false或数字0,则表示始终不满足条件;
- 如果为回调函数,则
- 可以接收session、values参数,"session.定义名" 可以获得jQuery包装的对象,"values.定义名"可获得其值(等于session.foobar.val()),"session.target" 可获得触发规则的jQ对象
- 函数返回true则表明满足条件;
- 如果for中只包含一个定义,且when为数组或字符串,如果数组包含for的值或者字符串等于for的值,则表明满足条件。
- whenXx
如果有多个适用条件,可以设置多个以 when 为前缀的适用条件,它们之间的关系为“或”。 - then
如果满足条件,则执行 then 中的操作,类型为回调函数或Map:- key为以逗号分隔的定义名或jquery selector,或者用 * 表示 for 中配置的所有定义;
- value
- 操作函数(接收session参数和values参数);
- 或者数组或以逗号分隔的操作名
- 操作可以通过 handlers 定义操作(如上面代码中的abc)
- 或触发 key 对应的页面元素的某个事件,如 fireClick、fireChange
- 或直接调用内置操作:
- disable、!disable:禁用、启用
- readonly、!readonly:设为只读、取消只读
- required、!required:设为必填、取消必填
- clear:清空值
- hide、show:隐藏、显示
- hideTD、showTD:隐藏所在单元格、显示所在单元格
- hideTR、showTR:隐藏所在行、显示所在行
- thenXx
当一个规则中包含多个业务、或者需要用不同的定义方式(回调函数或Map)时,可以将 then 拆成多个,如 then1、then2,便于代码维护 - else
如果不满足条件,则执行,同 then - elseXxx
同 else - follow
如果满足条件,执行完 then 中的操作后,则继续执行其他规则,这里为规则名称数组或以逗号分隔的字符串。
自定义操作 - handlers
then/else执行时,虽然内置操作 + 回调函数能满足大多数需求,但是这里仍然可以定义其他操作,以便简化 then/else 中的代码,使其可读性、可维护性更强。如上例中的 abc。
以项目中的实际代码为例:
<script>
$j.formRules({
define: {
allGranted: {onload: true}
},
defineOnly: {
roles: ".roles"
},
ruleHideRole: {
"for": "allGranted",
"when": function(ss, vals) {
return vals.allGranted > "0"
},
"then": {
"roles" : "hide"
},
"else" : {
"roles" : "show"
}
}
})
</script>
<tr class="prop">
<td class="name">xxxx</td>
<td class="value" colspan="3">
<g:select name="allGranted" ..../>
</td>
</tr>
<tr class="prop roles">
.....
</tr>
<tr class="prop roles">
.....
</tr>
页面上定义了一个 allGranted 的下拉列表,一个多个 class 为 roles 的 tr;定义了规则 ruleHideRole,当 allGranted 的值大于 0 时,隐藏 roles 行,否则显示 roles 行,并且在页面加载时运行一遍该规则。
通过 F12 监控控制台,修改 allGranted 的值时,可以看到下面的输出,整个规则的执行过程一目了然:
init session for allGranted, values are
Object { allGranted="1", roles=""}
test ruleHideRole.
execute ruleHideRole, when=true
do [hide] for [roles].
end session for allGranted.
下面再贴几段项目代码供学习。
$j.formRules({
define: "employeeId",
// 切换员工,重新加载,以便显示相关岗位
ruleEmployee : {
"for": "employeeId",
"then": function(ss, vals) {
if ( vals.employeeId && "create" == "${params.action}" ) {
$j(".changed").removeClass("changed");
var url = "${createLink(action:'create')}";
url += "?" + $j("form:first").serialize();
location = url;
}
}
}
})
$j.formRules({
"define": {
insAmountReport : {selector: "[name='ins.amountReport']"},
insActiveDate: {selector: "[name='ins.activeDate']"},
activeDate: {selector: "#activeDate"},
form: {selector: "form", on: "submit"},
employee: {}
},
// 生效日期变更
ruleActiveDate : {
"for": "activeDate",
"then" : function( ss, vals ) {
if ( vals.activeDate ) {
ss.insActiveDate.each(function(i, el) {
if ( !$j(el).val() ) {
el.value = vals.activeDate.substring(0, 7); // 基数的生效日期精确到月份
$j(el).trigger("change");
}
})
}
}
},
// 申报基数变更
ruleInsAmountReport : {
"for": "insAmountReport",
"when": function( ss, vals ) {
return vals.insAmountReport && isFinite(vals.insAmountReport);
},
"then": function( ss, vals ) {
// 计算核定基数等
var calError = calInsBaseAmount( ss.target.closest("tr") );
// 给其他空行赋值
ss.insAmountReport.each(function(i, el) {
if ( !el.value ) {
el.value = vals.insAmountReport;
if ( !calError ) calInsBaseAmount( $j(el).closest("tr") );
}
})
},
"else": function( ss, vals ) {
// 如果申报基数不合法,则清空核定基数等
ss.target.closest("tr").find("[name='ins.amount'], [name='ins.amountStatus']").val("");
}
},
// 基金生效日期变更
ruleInsActiveDate : {
"for": "insActiveDate",
"then": function( ss, vals ) {
// 计算核定基数等
var calError = calInsBaseAmount( ss.target.closest("tr") );
// 给其他空行赋值
ss.insActiveDate.each(function(i, el) {
if ( !$j(el).val() ) {
el.value = vals.insActiveDate;
if ( !calError ) calInsBaseAmount( $j(el).closest("tr") );
}
})
}
},
// 表单保存前,重新核定基数
ruleSubmit: {
"for": "form",
"then": function( ss ) {
ss.insAmountReport.each(function(i, el) {
calInsBaseAmount( $j(el).closest("tr") );
});
}
},
// 员工变更时,加载对应公司的薪酬结构
ruleEmployeeChange: {
"for": "employee",
"then": function( ss, vals ) {
var ps = $j("#payrollStruct"), val = ps.val();
ps.html(""); // 先清空
if ( vals.employee ) {
var opts = ps.get(0).options;
$j.post("${createLink(action:'getPayrollStructures')}", {employeeId: vals.employee}, function(data) {
$j(data).each(function(i, m) {
opts[i] = new Option(m.name, m.id);
})
});
}
}
}
})
阅读完上面的代码和长长的说明后,可能会觉得这样上面的代码写起来貌似更复杂、代码量更多了,其实不然:
- 把第一段代码格式化后、保存到一个地方,需要的时候拿出来改一下就可以用,不需要死记整个结构的写法
- 将复杂的业务拆分成元方法,再通过规则调用,代码会显得特别干净和清晰,易于维护
- 从代码量的角度来说,行数可能会增加,但代码量总体上来说肯定是减少了的
- 在个人的开发过程中,编写表单规则时的唯一常见问题就是 Map 结构中,每个 key/value 定义后需要加个逗号,否则 js 编译报错,比如: ruleXx: {...}, rule:Yy{...}