欢迎进入Wiki » FAQ » 表单规则开发说明?

表单规则开发说明?

在2015-05-22 15:20上被李小翔修改
评论 (0) · 附件 (0) · 记录 · 信息

项目实施中,表单开发是二次开发最重要、最复杂、最不易维护的部分之一。

表单开发中,最常见的场景包括:

  • 页面加载后,基于某个属性的值,执行业务逻辑
  • 某个表单字段修改后,根据新的值,执行业务逻辑

而表单业务中,最常见的不外乎:

  • 显示/隐藏某些表单部分
  • 设置/清空表单字段的值
  • 启用/禁用/必填/只读表单字段

常规的开发方式一般:

  • 在 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{...}
标签: BroFramework
在2015-05-22 14:01上被李小翔创建

Copyright © 2013 北京博瑞开源软件有限公司
京ICP备12048974号