1. 表单事件

表单事件

initialValues

表单初始化数据时执行。

initialValues 可以定义为同步函数或是异步函数。

# xxx.object.yml
form:
  initialValues: !!js/function |
    function(){
      return {
          name: "Hello World",
          code: "hello_world"
      }
    }

onRendered

  • 表单加载完成后执行

  • 一般用于给表单字段绑定原生事件,比如点击某个字段时触发执行相关业务逻辑,也可用于给表单赋初始值。

  • 一个参数,参数内容如下:

    {
      formId: "default",  // 表单唯一标识,默认为default
      form: {...},   // Ant Design FormInstance,用于调用ant表单的一些基础操作函数
      record: {...}, //编辑记录时表单从接口中获取到的记录内容,新建记录时为空
      bindFormEvent: function(type, eventHandler){}, //一个事件函数,用于给表单绑定原生事件,比如点击表单事件
      bindFieldEvent: function(type, fields, eventHandler){}, //一个事件函数,用于给表单上特定字段绑定原生事件,比如点击某个字段时弹出窗口
    }
    
    • formId属性是表单唯一标识,它也是每个表单生成的<form>标签中的id属性值。
    • form属性详情请参阅 Ant Design FormInstance
    • bindFormEvent属性是一个函数,当需要给表单绑定原生事件时可以调用它,详请请参考下面的 绑定原生事件
    • bindFieldEvent属性是一个函数,当需要给表单中的字段绑定原生事件时可以调用它,详请请参考下面的 绑定原生事件
  • 示例1,点击整个表单任意位置时弹出新窗口,在表单的编辑和只读状态新窗口分别跳转到不同的网址。

    # xxx.object.yml
    form:
      onRendered: !!js/function |
        function(args){
          args.bindFormEvent("click", function(e, {form, mode, field, record}){
            let url = mode === "read" ? "http://www.xxx.com" : "http://www.yyy.com";
            window.open(url);
          });
        }
    
  • 示例2,当表单加载完成后,根据任务优先级请求第三方API接口获取到期日期并设置为默认值。

    # tasks.object.yml
    form:
      onRendered: !!js/function |
        function(args){
          if(args.record){
            /*编辑记录时不执行下面的默认值逻辑*/
            return;
          }
          let priority = args.form.getFieldValue("priority");
          let defaultDueDate = ... //请求第三方API接口,传入priority参数值获取到期日期默认值
          args.form.setFieldsValue({due_date: defaultDueDate});
        }
    

onDestroy

  • 表单销毁时执行

  • 一般用于解除在onRendered函数中绑定的特殊事件,比如window的message事件。

  • 一个参数,参数内容与onRendered函数一致。

  • 需要注意的是onRendered函数中通过bindFormEvent/bindFieldEvent绑定的事件会在表单销毁时自动解除绑定,不需要再手动写代码来解除相关事件的绑定。

  • 如果在可视化界面配置onRendered函数时需要注意通过原生的addEventListener函数绑定事件时应该把事件函数设置到全局变量中,以便可以在onDestroy函数中通过removeEventListener来解除事件绑定。

  • 示例,以下代码演示了onRendered函数绑定了message事件时,如何在onDestroy函数中解除绑定:

# xxx.object.yml
form:
  onRendered: !!js/function |
    function(args){
      if(!window.MyApp){
        window.MyApp = {};
      }
      MyApp.onMessage = function(e){...};
      window.addEventListener("message", MyApp.onMessage);
    }
  onDestroy: !!js/function |
    function(args){
      window.removeEventListener("message", MyApp.onMessage);
    }

onValuesChange

  • 修改记录时执行

  • 一个参数,参数内容如下:

    {
      changedValues: {...},  // 正在被修改的字段信息
      values: {...}, // 表单所有字段的信息
      form: {...}   // 表单中的一些函数
    }
    
  • 示例,根据”每月开销(元)“自动判断属于”低、中、高“哪个消费”层次”。代码如下:

    # xxx.object.yml
    form:
      onValuesChange: !!js/function |
        function(args){
          const money = args.changedValues.money;
          if(money < 1000){
            args.form.setFieldsValue({type: '低'});
          }else if(money>=1000 && money<=2000){
            args.form.setFieldsValue({type: '中'});
          }else{
            args.form.setFieldsValue({type: '高'});
          }
        }
    

beforeDelete

  • 删除记录之前执行

  • 无参函数,所属数据可从this中获取:

    { 
      doc: 当前记录, 
      id: 记录ID,
      object_name: 当前对象名称, 
      spaceId: 当前工作区唯一标识, 
      userId: 当前用户唯一标识, 
    }
    
  • 返回值:

    • return false: 终止提交
    • return {字段名1: 错误原因1, 字段名2: 错误原因2}: 终止提交,根据字段名将错误原因显示在编辑窗口的字段下
    • throw new Error('需要显示的错误信息'): 终止提交,并自动在页面右上角报错信息
  • 示例:

    # xxx.object.yml
    form:
      beforeDelete: !!js/function |
        function () {
          ...
          throw new Error('禁止删除');
        }
    

afterDelete

  • 删除记录成功后执行

  • 无参函数,所属数据可从this中获取:

    { 
      doc: 当前记录, 
      id: 记录ID,
      object_name: 当前对象名称, 
      previousDoc: 删除前的完整记录,
      spaceId: 当前工作区唯一标识, 
      userId: 当前用户唯一标识, 
    }
    
  • 示例:

    # xxx.object.yml
    form:
      afterDelete: !!js/function |
        function () {
          ...
          window.open('xxx');
        }
    

beforeView

  • 记录详细页面:记录显示前执行

  • 无参函数,所属数据可从this中获取:

    { 
      doc: 要显示的记录, 
      id: 记录ID,
      object_name: 当前对象名称, 
      schema: Schema,
      spaceId: 当前工作区唯一标识, 
      userId: 当前用户唯一标识, 
    }
    
  • 示例:

    # xxx.object.yml
    form:
      beforeView: !!js/function |
        function () {
          if(this.doc.is_trial){
            this.doc.name = this.doc.name + "(试用)";
          }
        }
    

afterView

  • 记录详细页面:记录显示之后执行

  • 无参函数,所属数据可从this中获取:

    { 
      doc: 要显示的记录, 
      id: 记录ID,
      object_name: 当前对象名称, 
      schema: Schema,
      spaceId: 当前工作区唯一标识, 
      userId: 当前用户唯一标识, 
    }
    
  • 示例:

    # xxx.object.yml
    form:
      afterView: !!js/function |
        function () {
          //如果当前记录的is_trial为true,则修改详细页面header背景颜色、字体颜色
          if(this.doc.is_trial){
            $(".slds-page-header_bleed").css('background-color', '#4CAF50').css('color', '#ffffff');
          }
        }
    

绑定原生事件

bindFormEvent

▸ bindFormEvent(type: keyof HTMLElementEventMap, eventHandler: eventHandler): void

调用该函数可以给整个表单绑定原生事件,比如点击表单事件。

参数列表:

名称类型描述
typekeyof HTMLElementEventMap事件类型
eventHandlereventHandler事件回调函数

参数type表示的事件类型是浏览器原生事件类型,比如click,change等。

虽然该函数是给整个表单绑定原生事件,但是可以在callBack函数中很方便的判断当前触发事件的是表单上的哪个字段,详情请参阅后续 eventHandler field option 小节。

可以在一个onRendered函数中多次调用bindFormEvent为表单绑定多个事件函数,当相关事件被触发时,所有的事件函数都会按绑定次序依次执行,详情请参阅后续 绑定多个函数 小节。

bindFieldEvent

▸ bindFieldEvent(type: keyof HTMLElementEventMap, fields: string[] | string, eventHandler: eventHandler): void

调用该函数可以给表单上特定字段绑定原生事件,比如点击某个字段时弹出窗口,或者某个字段的change事件等。

参数列表:

名称类型描述
typekeyof HTMLElementEventMap事件类型
fieldsstring/string[]字段
eventHandlereventHandler事件回调函数

参数type表示的事件类型是浏览器原生事件类型,比如click,change等。

参数fields表示的是要把事件绑定到哪个字段或哪些字段上,只有触发了该参数指定的字段的相关事件时才会调用事件回调函数。

可以在一个onRendered函数中多次调用bindFieldEvent为不同表单字段绑定事件函数,也可以为同一个字段绑定多个事件函数,当相关事件被触发时,所有的事件函数都会按绑定次序依次执行,详情请参阅后续 绑定多个函数 小节。

eventHandler

▸ eventHandler(event: Event, options: EventHandlerOptions): void

调用 bindFormEventbindFieldEvent 绑定原生事件时的回调函数。

参数列表:

名称类型描述
eventEvent原生事件
optionsobjectEventHandlerOptions

EventHandlerOptions

Ƭ EventHandlerOptions: object

调用 bindFormEventbindFieldEvent 绑定原生事件时回调函数所带的额外参数。

属性名称类型描述
formobjectAnt Design FormInstance
modestringread/edit
fieldobject字段Schema
recordobject记录内容

form

该属性指向一个 Ant Design FormInstance,用于调用ant表单的一些基础操作函数。

以下代码描述了如何通过form属性获取和设置表单字段值:

# xxx.object.yml
form:
  onRendered: !!js/function |
    function(args){
      let name = args.form.getFieldValue("name");
      args.form.setFieldsValue({due_date: new Date()});
    }

mode

mode属性是表单的编辑状态,在 bindFormEventbindFieldEvent 函数中意思是一样的,即在绑定表单字段事件时该参数值依然是表单的mode状态而不是字段的mode状态值。

要判断字段的编辑状态需要结合field属性,请参阅后续 field 属性说明。

以下代码描述了如何判断表单是否编辑状态:

# xxx.object.yml
form:
  onRendered: !!js/function |
    function(args){
      args.bindFormEvent("click", function(e, {form, mode, field, record}){
        if(mode === "read"){
          /*只读状态不响应相关逻辑代码*/
          return;
        }
        ...
      });
    }

field

field属性在 bindFormEventbindFieldEvent 函数中都表示触发的是哪个字段上的相关事件,它返回的是字段的Schema,比如field.readonly表示字段是否只读。

不过 bindFormEvent 因为响应的是整个表单的事件,所以该属性值可能为空,而 bindFieldEvent 因为事件是直接绑定到字段上,所以肯定有值。

比如通过 bindFormEvent 给表单绑定点击事件时,如果点击的正好是某个字段范围,那么field属性值就会传入该字段的Schema,否则会传入空值。

以下代码描述了如何判断字段是否编辑状态:

# xxx.object.yml
form:
  onRendered: !!js/function |
    function(args){
      args.bindFormEvent("click", function(e, {form, mode, field, record}){
        if(mode === "read" || !field || field.readonly || field.name !== "code"){
          return;
        }
        /*只有触发了编辑状态的code字段的点击事件才会继续执行后面的代码*/
        ...
      });
    }

# xxx.object.yml
form:
  onRendered: !!js/function |
    function(args){
      args.bindFieldEvent("click", ["code"], function(e, {form, mode, field, record}){
        if(mode === "read" || field.readonly){
          return;
        }
        /*只有触发了编辑状态的code字段的点击事件才会继续执行后面的代码*/
        ...
      });
    }

record

record表示修改记录时从接口中抓取到的记录内容,新建记录时该属性值为空,可以根据该属性值是否为空来判断是新建还是编辑记录。

以下代码描述了如何判断表单是新建还是编辑状态:

# xxx.object.yml
form:
  onRendered: !!js/function |
    function(args){
      if(args.record){
        return;
      }
      /*以下代码只会在新建记录的表单中执行,编辑记录时会跳过*/
      ...
    }

绑定多个函数

可以在一个 onRendered 函数中多次调用 bindFormEventbindFieldEvent 函数同时为表单或字段绑定多个事件函数,当相关事件被触发时,所有的事件函数都会按绑定次序依次执行。

绑定多个事件

以下示例代码为表单以及名称字段绑定了 clickchange 两种事件函数,当相关事件被触发时,它们都会被执行。

# xxx.object.yml
form:
  onRendered: !!js/function |
    function(args){
      args.bindFormEvent("click", function(e, {form, mode, field, record}){
        args.bindFormEvent("click", function(e, {form, mode, field, record}){
          console.log("bindFormEvent click",form, mode, field, record);
        });
        args.bindFormEvent("change", function(e, {form, mode, field, record}){
          console.log("bindFormEvent change",form, mode, field, record);
        });
        args.bindFieldEvent("click", ["name"], function(e, {form, mode, field, record}){
          console.log("bindFieldEvent click",form, mode, field, record);
        });
        args.bindFieldEvent("change", ["name"], function(e, {form, mode, field, record}){
          console.log("bindFieldEvent change",form, mode, field, record);
        });
      });
    }

当点击名称字段时,会依次输出日志 bindFieldEvent clickbindFormEvent click 日志,这是因为使用 bindFieldEvent 绑定的事件函数会先于使用 bindFormEvent 绑定的事件函数执行,详细请参阅后续 事件函数执行次序 小节。

同一个事件绑定多个函数

以下示例代码为表单以及名称字段都绑定了两个 click 事件函数,当事件被触发时,它们都会被执行。

# xxx.object.yml
form:
  onRendered: !!js/function |
    function(args){
      args.bindFormEvent("click", function(e, {form, mode, field, record}){
        args.bindFormEvent("click", function(e, {form, mode, field, record}){
          console.log("bindFormEvent click1",form, mode, field, record);
        });
        args.bindFormEvent("click", function(e, {form, mode, field, record}){
          console.log("bindFormEvent click2",form, mode, field, record);
        });
        args.bindFieldEvent("click", ["name"], function(e, {form, mode, field, record}){
          console.log("bindFieldEvent click1",form, mode, field, record);
        });
        args.bindFieldEvent("click", ["name"], function(e, {form, mode, field, record}){
          console.log("bindFieldEvent click2",form, mode, field, record);
        });
      });
    }

当点击名称字段时,会依次输出日志bindFieldEvent click1bindFieldEvent click2bindFormEvent click1bindFormEvent click2

以下两段示例代码都实现了为经度和纬度两个字段绑定点击事件函数,它们的效果是一样的。

# xxx.object.yml
form:
  onRendered: !!js/function |
    function(args){
      args.bindFormEvent("click", function(e, {form, mode, field, record}){
        args.bindFieldEvent("click", ["long__c", "lat__c"], function(e, {form, mode, field, record}){
          console.log("bindFieldEvent click1", field.name);
        });
      });
    }

# xxx.object.yml
form:
  onRendered: !!js/function |
    function(args){
      args.bindFormEvent("click", function(e, {form, mode, field, record}){
        args.bindFieldEvent("click", "long__c", function(e, {form, mode, field, record}){
          console.log("bindFieldEvent click2", field.name);
        });
        args.bindFieldEvent("click", "lat__c", function(e, {form, mode, field, record}){
          console.log("bindFieldEvent click3", field.name);
        });
      });
    }

事件函数执行次序

一般来说事件函数执行次序是依照调用 bindFormEventbindFieldEvent 函数为表单或字段绑定事件函数的先后顺序来的。

但是当同时为表单和字段绑定了事件函数时,会优先按事件绑定次序执行所有的字段级事件函数,再按事件绑定次序执行所有的表单级事件函数,也就是说使用 bindFieldEvent 绑定的事件函数会先于使用 bindFormEvent 绑定的事件函数执行。

解除绑定

理论上所有的事件绑定在组件销毁时都应该被解除绑定以避免事件函数被重复执行或造成内存泄漏。

调用 bindFormEventbindFieldEvent 函数绑定的所有事件会在组件销毁时自行解除相关事件绑定,不需要再在 onDestroy 函数中写代码解绑。

但是在 onRendered 函数中通过其他方式绑定的事件函数,比如window.addEventListener,还是需要写代码手动解绑的,详情请参阅前面的 onDestroy 小节。

示例

示例1,以下示例代码实现了点击表单中编辑状态下的名为code的字段时弹出一个代码编辑器窗口。

# xxx.object.yml
form:
  onRendered: !!js/function |
    function(args){
      args.bindFormEvent("click", function(e, {form, mode, field, record}){
        if(mode === "read" || !field || field.readonly || field.name !== "code"){
          return;
        }
        window.open("http://www.somecodeeidtor.com");
      });
    }

# xxx.object.yml
form:
  onRendered: !!js/function |
    function(args){
      args.bindFieldEvent("click", ["code"], function(e, {form, mode, field, record}){
        if(mode === "read" || field.readonly){
          return;
        }
        window.open("http://www.somecodeeidtor.com");
      });
    }

示例2,以下示例代码模拟实现了点击表单中编辑状态下的名为经度或纬度的字段时弹出一个iframe窗口,显示地图应用用于选择经纬度并把选中值回传到表单的经度和纬度字段中。

# lbs.object.yml
form:
  onRendered: !!js/function |
    function(args){
      if(!window.LBSClient){
          window.LBSClient = {};
      }
      LBSClient.positionValue = {};
      /*lbsRootUrl指向的是启动一个远程开发环境模拟实现的地图应用地址*/
      let lbsRootUrl = "https://5000-steedos-steedosprojectt-b0e4nt9jhwg.ws-us45.gitpod.io";
      LBSClient.onMessage = function(e){
          let positionValue = LBSClient.positionValue;
          if (e.origin !== lbsRootUrl){
              return;
          }
          if(e.data && e.data.tag === "positionChanged"){
              positionValue.long = e.data.long;
              positionValue.lat = e.data.lat;
          }
      }
      LBSClient.fieldClickHandler = function(e, {form, mode, field}){
          // 点击经纬度字段的编辑框时弹出iframe获取经纬度值
          // iframe指向的地址需要执行 parent.postMessage({long, lat, tag:"positionChanged"},"http://www.xxx.com") 发送message到当前窗口
          // postMessage只认域名或ip地址,不认localhost,如果本地开发环境使用域名需要先在host中作好域名映射 127.0.0.1  www.xxx.com
          if(mode === "read" || field.readonly){
              return;
          }
          let positionValue = LBSClient.positionValue;
          delete positionValue.long;
          delete positionValue.lat;
          window.removeEventListener("message", LBSClient.onMessage);
          window.addEventListener("message", LBSClient.onMessage);
          let longFieldValue = form.getFieldValue("long__c");
          let latFieldValue = form.getFieldValue("lat__c");
          window.SteedosUI.showModal(stores.ComponentRegistry.components.Iframe,{
              name: "frame_lbs", 
              title: `LBS位置`,
              src: lbsRootUrl + "/api/lbs?long=" + longFieldValue + "&lat=" + latFieldValue,
              onFinish: ()=>{
                  let positionValue = LBSClient.positionValue;
                  if(positionValue && positionValue.long && positionValue.lat){
                      form.setFieldsValue({long__c: positionValue.long, lat__c: positionValue.lat});
                      return true;
                  }
                  else{
                      window.alert("请先输入经纬度值!");
                  }
                  return false;
              },
              modalProps:{
                  onCancel: ()=>{
                      window.removeEventListener("message", LBSClient.onMessage);
                  }
              }
          });
      }
      args.bindFieldEvent("click", ["long__c", "lat__c"], LBSClient.fieldClickHandler);
    }
  onDestroy: !!js/function |
    function(args){
      window.removeEventListener("message", LBSClient.onMessage);
    }

上面的示例需要在弹出的iframe指向的地图应用程序中增加 postMessage 逻辑在用户选择好地图位置后回传经纬度值到父窗口,以下是模拟实现该过程的示例代码。

main/default/routes/lbs.router.js

const express = require("express");
const router = express.Router();

router.get('/api/lbs', async function (req, res) {
    let long = req.query.long;
    let lat = req.query.lat;
    res.writeHead(200, {
        'Content-Type': 'text/html'
    });
    res.write(
        `<!DOCTYPE html>
        <html>
            <head>
                <meta charset="utf-8">
                <meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=yes">
                <title>Steedos</title>
                <link rel="stylesheet" type="text/css" href="/assets/styles/steedos-tailwind.min.css">
                <script type="text/javascript" src="/lib/jquery/jquery-1.11.2.min.js"></script>
                <script type="text/javascript">
                    const inputValueChangeHandler = function(e){
                        let long = $("input[name=long]").val();
                        let lat = $("input[name=lat]").val();
                        if(long && lat && parent){
                            let rootUrl = "http://192.168.2.192:5700";//这是上面lbs.object.yml文件定义的对象所在的rootURL地址。
                            parent.postMessage({long, lat, tag:"positionChanged"}, rootUrl);
                        }
                    }
                    $(function(){
                        let long = "${long}";
                        let lat = "${lat}";
                        if(long){
                            $("input[name=long]").val(long);
                        }
                        if(lat){
                            $("input[name=lat]").val(lat);
                        }
                        $("input[name=long],input[name=lat]").on("change", inputValueChangeHandler);
                    });
                </script>
            </head>
            <body class="p-8">
                <table class="table-auto">
                    <thead>
                        <tr>
                            <th>经度</th>
                            <th>纬度</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr>
                            <td>
                                <input type="text" name="long" class="mt-1 px-3 py-2 bg-white border shadow-sm border-slate-300 placeholder-slate-400 focus:outline-none focus:border-sky-500 focus:ring-sky-500 block w-full rounded-md sm:text-sm focus:ring-1" placeholder="请输入经度" />
                            </td>
                            <td>
                                <input type="text" name="lat" class="mt-1 px-3 py-2 bg-white border shadow-sm border-slate-300 placeholder-slate-400 focus:outline-none focus:border-sky-500 focus:ring-sky-500 block w-full rounded-md sm:text-sm focus:ring-1" placeholder="请输入纬度" />
                            </td>
                        </tr>
                    </tbody>
                </table>
            </body>
        </html>
        `
    );
    return res.end();
});
exports.default = router;

以上地图应用示例代码会运行在上面示例2中onRendered函数中定义的lbsRootUrl变量指向的地址中,我们可以启动一个远程开发环境并把上述代码复制过去来模拟实现一个地图应用。

表单级联

在项目开发中,我们经常会需要在表单上实现多个字段之间的级联效果,比如常见的省市选项级联等,下面我们描述下如果在华炎魔方中实现类似需求。

下拉级联

当需要实现选项固定的字段的下拉级联效果时,我们推荐把字段配置为select类型,并在optionsFunction属性中编写级联逻辑代码,以下是一个简单的省市级联效果字段配置示例。

select类型即是选择框字段类型,该字段类型说明请参考文档 字段类型索引 中的 选择框字段类型

需要注意city字段需要配置depend_on属性指向province,表示当province字段值变更时级联触发city字段选项重新计算并且会清除city字段值。

province:
    type: select
    label:group: 省市级联
    options:
      - label: 北京
          value: 'bj'
      - label: 上海
          value: 'sh'
      - label: 江苏
          value: 'js'
city:
  type: select
  label:group: 省市级联
  depend_on:
    - province
  optionsFunction: !<tag:yaml.org,2002:js/function> |-
    function (values){
      const cityData = {
        bj: [{ label: '东城区', value: 'bj-1' }, { label: '西城区', value: 'bj-2' }],
        sh: [{ label: '松江', value: 'sh-1' }, { label: '浦东', value: 'sh-2' }],
        js: [{ label: '南京', value: 'js-1' }, { label: '杭州', value: 'js-2' }]
      };
      return cityData[values.province];
    }

选项过滤

很多情况下字段选项并不是固定的,而是需要请求后台接口来列出相关选项的,这时我们会把字段类型配置为 “相关表” 或 “主表子表”,其使用说明请参考 字段类型索引

在华炎魔方中可以为这两种类型的字段配置filtersfiltersFunction属性来限定选项的过滤条件,在这两个属性中配置业务代码都可以实现多个字段间选项级联效果。

过滤函数

假设有一个产品对象,我们希望新建产品记录时,用户在选择了产品类别后,可以在选择所属品牌时,只列出之前选好的产品类别下的品牌供用户选择,我们只要按下面的代码来配置产品类别和品牌字段即可:

category:
  label: 产品类别
  type: lookup
  reference_to: categories
brand:
  label: 品牌
  type: lookup
  reference_to: brands
  depend_on:
    - category
  filtersFunction: !<tag:yaml.org,2002:js/function> |-
    function (filters, values){
      return [["category","=",values.category]]
    }