表单规则开发说明?
李小翔
2015-05-22 15:20

内容列表
规则定义 	 # 
页面元素定义与事件绑定 - define 	 # 
其他页面元素定义 - defineOnly 	 # 
规则定义 - ruleXxxx 	 # 
自定义操作 - handlers 	 # 
规则执行 	 # 
注意事项 & 其他 	 # 

目录规则定义页面元素定义与事件绑定 - define
其他页面元素定义 - defineOnly
规则定义 - ruleXxxx
自定义操作 - handlers
规则执行
注意事项 & 其他
项目实施中，表单开发是二次开发最重要、最复杂、最不易维护的部分之一。
表单开发中，最常见的场景包括：
页面加载后，基于某个属性的值，执行业务逻辑
某个表单字段修改后，根据新的值，执行业务逻辑
而表单业务中，最常见的不外乎：
显示/隐藏某些表单部分
设置/清空表单字段的值
启用/禁用/必填/只读表单字段
常规的开发方式一般：
在 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 外，其他可选

页面元素定义与事件绑定 - define
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 等处。

其他页面元素定义 - defineOnly
定义不绑定任何事件的表单元素，结构同 define。

规则定义 - ruleXxxx
一个 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{ ...}