[设计器 vjdesign] 快速实现动态表单配置系统

608 阅读6分钟

vjdesign 是一个可视化界面设计器,jformer 是一个动态表单呈现组件,都具有可扩展性实现丰富功能,现在使用vjdesign + jformer 快速开发一套动态表单配置系统

系统的特性如下:

  • 提供几种常用的 elementui 组件,可在设计器拖动实现布局和设置组件属性
  • 可设置 axios 数据源获取表单数据
  • 表单支持设置验证条件
  • 表单可保存和预览

页面已经集成了 elementui 通过配置可增加支持的组件,如果有真实接收数据的服务可实现表单提交

完整示例

这里先给出实际运行效果

codesandbox 可能有点慢,需要多等一会儿

示例实现
效果展示

设计器实现

设计器通过配置实现了组件支持和属性编辑,并可保存布局和打开预览页,在这篇文章里实现了 elementui 表单组件的简化配置,这里直接拿来用

html 页

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Static Template</title>
    <script src="https://cdn.jsdelivr.net/npm/vue"></script>
    <script src="https://cdn.jsdelivr.net/npm/vjdesign"></script>
    <script src="https://cdn.jsdelivr.net/npm/element-ui/lib/index.js"></script>
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/vjdesign/dist/vjdesign.css"
    />
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/element-ui/lib/theme-chalk/index.css"
    />
    <style>
      html {
        font-size: 14px;
      }

      #app {
        position: absolute;
        top: 0;
        bottom: 0;
        left: 0;
        right: 0;
      }

      .v-jdesign {
        position: absolute;
        top: 50px;
        bottom: 0;
        height: auto;
      }

      .navi {
        position: absolute;
        top: 0;
        right: 0;
        left: 0;
        height: 49px;
        border-bottom: 1px solid silver;
        line-height: 49px;
        padding: 0 20px;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <div class="navi">
        <el-button type="primary" @click="onSave">保存</el-button>
        <el-button @click="onSaveAndPreview">保存并预览</el-button>
      </div>
      <v-jdesign v-model="config" v-bind:profile="profile"></v-jdesign>
    </div>

    <script src="./js/extends.js"></script>
    <script>
      window.vjdesign.default.form.use(({ provider }) => {
        provider(elementFormProps).withIndex(1);
        provider(colProps).withIndex(2);
      });

      new Vue({
        data() {
          return { config: {}, profile: {} };
        },
        methods: {
          onSave() {
            localStorage.setItem("config", JSON.stringify(this.config));
          },
          onSaveAndPreview() {
            localStorage.setItem("config", JSON.stringify(this.config));
            window.location.href = "./preview.html";
          }
        },
        async mounted() {
          const loading = this.$loading();
          try {
            this.profile = await (await fetch("./profile.json")).json();
            try {
              this.config = JSON.parse(localStorage.getItem("config") || "{}");
            } catch {
              this.config = {};
            }
          } finally {
            loading.close();
          }
        }
      }).$mount("#app");
    </script>
  </body>
</html>

扩展实现

elementFormProps 和 colProps 用于在表单渲染时支持简化的配置,axiosDatasource 实现了一个数据源扩展,让表单支持使用 axios 库作为数据源实现服务数据的获取与提交

extends.js 的实现

/**
 * providers
 */

// 实现一个渲染处理 provider 实现在组件上定义 elForm
// 就可以自动在组件外层加上 el-form-item
const elementFormProps = (field) => {
  if (!field.elForm) {
    return;
  }

  // 复制组件原始配置
  const originField = { ...field };

  // 将组件名改成 el-form-item
  field.component = "el-form-item";

  // 将原组件的复制赋给组件下级实现改变界面结构
  field.children = [originField];

  // 将 elForm 属性作为 el-form-item 的属性
  field.fieldOptions = {
    props: field.elForm
  };

  // 如果组件关联了数据属性则将数据属性作为 el-form-item 的数据属性
  if (originField.model) {
    field.fieldOptions.props.prop = Array.isArray(originField.model)
      ? originField.model[0]
      : originField.model;
  }

  // 删除 elForm 定义避免下级组件渲染处理时无限循环
  delete field.elForm;
  delete originField.elForm;
  delete originField.colProps;
};

const colProps = (field) => {
  if (!field.colProps) {
    return;
  }

  const originField = { ...field };
  field.component = "el-col";
  field.children = [originField];
  field.fieldOptions = {
    props: field.colProps
  };

  delete field.colProps;
  delete originField.colProps;
};

/**
 * datasources
 */

const axios = window.axios ? window.axios.create() : {};

// 自定义通过 axios 请求数据
const axiosDatasource = (resolveOptions) => {
  const { autoload = false } = resolveOptions();

  const instance = {
    request: () => {
      const {
        url,
        method = "GET",
        headers = [],
        params,
        data,
        timeout,
        onResponse = () => {},
        onError = () => {}
      } = resolveOptions();

      return axios
        .request({
          url,
          method,
          headers: headers.reduce((pre, cur) => {
            pre[cur.key] = cur.value;
            return pre;
          }, {}),
          params,
          data,
          timeout
        })
        .then((response) => {
          instance.data = response.data;
          onResponse(response);
        })
        .catch((error) => {
          onError(error);
        });
    },
    data: null
  };

  if (autoload) {
    instance.request();
  }

  return instance;
};

设计器里因为没有数据源的行为因此设计器界面里不需要注册 axiosDatasource,在设计器里只需要注册 elementFormProps 和 colProps 渲染处理,让设计时也呈现出布局效果

设计器 html 页中注册渲染处理

window.vjdesign.default.form.use(({ provider }) => {
  provider(elementFormProps).withIndex(1);
  provider(colProps).withIndex(2);
});

设计器配置

设计器配置里设置了设计器支持的数据源、组件和组件的属性

{
  "datasource": [
    {
      "type": "axios",
      "label": "axios",
      "properties": [
        { "name": "url", "label": "URL" },
        {
          "name": "method",
          "label": "方法",
          "editor": {
            "name": "select",
            "options": {
              "items": [
                { "value": "GET", "label": "GET" },
                { "value": "POST", "label": "POST" },
                { "value": "PUT", "label": "PUT" },
                { "value": "DELETE", "label": "DELETE" }
              ]
            }
          }
        },
        {
          "name": "headers",
          "label": "头信息 (head)",
          "editor": "array",
          "properties": [
            { "name": "key", "label": "键", "transform": false },
            { "name": "value", "label": "值" }
          ]
        },
        { "name": "params", "label": "参数 (querystring)" },
        { "name": "data", "label": "数据 (body)" },
        { "name": "timeout", "label": "请求超时", "editor": "number" },
        { "name": "autoload", "label": "自动请求", "editor": "checkbox" },
        { "name": "onResponse", "label": "请求返回事件", "transform": ["@"] },
        { "name": "onError", "label": "请求失败事件", "transform": ["@"] }
      ]
    }
  ],
  "components": [
    {
      "name": "el-button",
      "label": "按钮",
      "group": "ElementUI",
      "designer": { "name": "default", "copyClass": false },
      "properties": [
        "elForm.label",
        { "name": "text", "label": "文本", "group": "组件" },
        {
          "name": "fieldOptions.props.type",
          "label": "类型",
          "group": "组件",
          "editor": {
            "name": "select",
            "options": {
              "items": [
                { "value": "primary", "label": "主要" },
                { "value": "success", "label": "成功" },
                { "value": "info", "label": "信息" },
                { "value": "warning", "label": "警告" },
                { "value": "danger", "label": "危险" },
                { "value": "text", "label": "文本" }
              ]
            }
          }
        },
        "fieldOptions.on.click"
      ]
    },
    {
      "name": "el-row",
      "label": "行",
      "group": "ElementUI",
      "designer": "classContainer",
      "properties": [
        {
          "name": "fieldOptions.props.gutter",
          "label": "间隔",
          "group": "组件",
          "editor": "number"
        }
      ]
    },
    {
      "name": "el-form",
      "label": "表单",
      "group": "ElementUI",
      "designer": "container",
      "properties": [
        {
          "name": "fieldOptions.props.model",
          "label": "表单数据",
          "group": "数据"
        },
        {
          "name": "fieldOptions.props.labelWidth",
          "label": "前缀宽度",
          "group": "组件"
        }
      ]
    },
    {
      "name": "el-input",
      "label": "输入框",
      "group": "ElementUI",
      "properties": ["model", "elForm.label", "elForm.rules", "colProps.span"]
    },
    {
      "name": "el-select",
      "label": "选择框",
      "group": "ElementUI",
      "properties": [
        "model",
        "elForm.label",
        "elForm.rules",
        "colProps.span",
        {
          "name": "children",
          "label": "选项",
          "group": "组件",
          "editor": "array",
          "properties": [
            {
              "name": "component",
              "label": "类型",
              "editor": {
                "name": "select",
                "options": {
                  "items": [{ "value": "el-option", "label": "选择项" }]
                }
              }
            },
            { "name": "fieldOptions.props.value", "label": "选项值" },
            { "name": "fieldOptions.props.label", "label": "选项名" }
          ]
        }
      ]
    }
  ],
  "properties": [
    { "name": "elForm.label", "label": "前缀", "group": "表单" },
    {
      "name": "elForm.rules",
      "label": "验证规则",
      "group": "表单",
      "properties": [
        {
          "name": "type",
          "label": "类型",
          "transform": false,
          "editor": {
            "name": "select",
            "options": {
              "items": [
                { "value": "string", "label": "字符串" },
                { "value": "number", "label": "数字" },
                { "value": "boolean", "label": "布尔" },
                { "value": "array", "label": "数组" }
              ]
            }
          }
        },
        { "name": "required", "label": "是否必填", "editor": "switch" },
        { "name": "message", "label": "错误提示" },
        { "name": "min", "label": "最小值", "editor": "number" },
        { "name": "max", "label": "最大值", "editor": "number" },
        { "name": "pattern", "label": "正则" }
      ],
      "editor": "array"
    },
    {
      "name": "colProps.span",
      "label": "列宽",
      "group": "布局",
      "editor": "number"
    },
    {
      "name": "fieldOptions.on.click",
      "label": "点击",
      "transform": ["@"],
      "group": "事件"
    }
  ]
}

axios 数据源是实现了使用 axios 库进行HTTP请求,这里只需要定义数据源对应的属性

在 extends.js 数据源实现中,定义了数据源都会用到哪些属性

const { autoload = false } = resolveOptions();

const {
  url,
  method = "GET",
  headers = [],
  params,
  data,
  timeout,
  onResponse = () => {},
  onError = () => {}
} = resolveOptions();

预览界面

预览界面用于查看表单预览效果

html 页

html 页中需要引用 axios 库和 extends.js 并在 jformer 中注册 elementFormProps、colProps、axiosDatasource 扩展

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>JFormer 表单</title>
    <script src="https://cdn.jsdelivr.net/npm/vue"></script>
    <script src="https://cdn.jsdelivr.net/npm/jformer"></script>
    <script src="https://cdn.jsdelivr.net/npm/element-ui/lib/index.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/axios"></script>
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/element-ui/lib/theme-chalk/index.css"
    />
  </head>
  <body>
    <div id="app">
      <j-former v-bind:config="config"></j-former>
    </div>
    <script src="./js/extends.js"></script>
    <script>
      // extends 文件里定义了 elementFormProps、colProps、axiosDatasource
      window.jformer.default.use(({ provider, datasource }) => {
        // 注册 provider
        provider(elementFormProps).withIndex(1);
        provider(colProps).withIndex(2);

        // 注册数据源
        datasource("axios", axiosDatasource);
      });

      new Vue({
        data() {
          return {
            config: {}
          };
        },
        async mounted() {
          try {
            this.config = JSON.parse(localStorage.getItem("config") || {});
          } catch {
            this.config = {};
          }
        }
      }).$mount("#app");
    </script>
  </body>
</html>

关于设计器的使用

示例表单

打开设计器视图左侧的 元数据 项,点 编辑 将下列配置复制到元数据内可查看示例

  "datasource": {
    "表单数据": {
      "type": "axios",
      "url": "./data/formdata.json",
      "method": "GET",
      "autoload": true,
      "onResponse": {
        "$type": "design",
        "$value": "@*:arguments[0].data"
      }
    }
  },
  "listeners": [],
  "fields": [{
    "component": "el-form",
    "children": [{
      "component": "el-row",
      "children": [{
        "component": "el-input",
        "elForm": {
          "label": "输入1",
          "rules": [{
            "required": true,
            "message": "必填项"
          }]
        },
        "colProps": {
          "span": 12
        },
        "model": ["text1"]
      }, {
        "component": "el-input",
        "elForm": {
          "label": "输入2",
          "rules": [{
            "message": "不得少于输入1的长度",
            "min": {
              "$type": "design",
              "$value": "$:model.text1.length"
            }
          }]
        },
        "colProps": {
          "span": 12
        },
        "model": ["text2"]
      }, {
        "component": "el-select",
        "model": ["selected"],
        "elForm": {
          "label": "选择1"
        },
        "colProps": {
          "span": 12
        },
        "children": [{
          "component": "el-option",
          "fieldOptions": {
            "props": {
              "value": {
                "$type": "design",
                "$value": "$:0"
              },
              "label": "选项1"
            }
          }
        }, {
          "component": "el-option",
          "fieldOptions": {
            "props": {
              "value": {
                "$type": "design",
                "$value": "$:1"
              },
              "label": "选项2"
            }
          }
        }, {
          "component": "el-option",
          "fieldOptions": {
            "props": {
              "value": {
                "$type": "design",
                "$value": "$:2"
              },
              "label": "选项3"
            }
          }
        }]
      }],
      "fieldOptions": {
        "props": {
          "gutter": 20
        }
      }
    }, {
      "component": "el-button",
      "text": "提交",
      "elForm": {
        "label": " "
      },
      "fieldOptions": {
        "props": {
          "type": "primary"
        },
        "on": {
          "click": {
            "$type": "design",
            "$value": "@:refs.form.validate()"
          }
        }
      }
    }],
    "fieldOptions": {
      "props": {
        "labelWidth": "120px",
        "model": {
          "$type": "design",
          "$value": "$:model"
        }
      },
      "ref": "form"
    }
  }],
  "model": {}
}

axios 数据源获取远程数据

设计器配置里定义相关属性,除了几个常规属性外,在请求成功后的请求返回事件中可通过设置转换来将请求的数据更新到 model 里,在转换表达式 数据 属性里设置 * 就是用新数据更新整个 model 对象

其中 model 是表单内部数据,可用于关联组件输入输出交互的数据,转换类型是 行为 时,数据 属性值就是要更新 model 里的属性,实现 里的 arguments 代表该事件触发后传递的参数数组,这里第一个参数是 axios 发起请求后返回的 response 对象,其中 data 就是请求结果的数据

表单数据和表单验证

elementui 中要想实现表单验证需要设置 el-form 的 model 属性,这里的表单数据就是 model 属性,使用转换表达式关联整个 model 对象

每个表单项 el-form-item 都要设置 prop 属性来实现此表单项要验证哪个属性,这里已经通过 elementFormProp 渲染处理实现了只设置组件的数据属性就可以自动设置 prop 属性

因为表单数据关联了整个 model 对象,获取表单的数据请求结果更新了 model 数据,因此,这里的组件数据属性 设置为 model 里的属性名

选择组件的选项值

设计器配置文件里已经定义了响应属性,界面上可通过选项属性设置选项值,在设置值的时候如果获取的数据值是数字,则选项值需要用转换表达式来输入一个数字作为选项值

表单的验证及提交

vue 中可通过设置组件 ref 属性来获取组件实例,这里的引用名就是设置组件的 ref,之后可在转换表达式中通过 refs获取到这个组件的实例

在提交按钮的点击事件上进行表单验证,elementui 的 el-form 组件 validate 方法执行表单验证,转换 实现 里设置 refs.form.validate()

如果需要实现表单数据提交,需要有接收数据的服务,并添加一个用于表单提交的数据源,提交的数据用转换关联 model.data 属性,数据源扩展 axiosDatasource 中定义了一个 request 方法用于发起请求,点击事件里可写成 refs.form.validate().then(()=>datasource.<数据源名称>.request())

相关链接

设计器: Github Gitee
动态表单组件: Github Gitee