[动态表单 jfomer] 通过 json-schema 动态生成表单

3,724 阅读1分钟

概述

jformer 是一个动态表单呈现组件,仅通过 json 数据就可以动态生成界面,jformer 具有扩展性,能够自定义渲染处理方式,在渲染时控制输出的界面呈现形式

json-schema 是一种描述数据的格式,通常用于数据格式验证、表单验证等方面,现在利用 jformer 的扩展特性实现将json-schema 数据渲染成表单

实现

先定义一个 html 页面,引用 jformer,示例采用 elementui 组件库

<!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>Json Schema 示例</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>
    <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>
      new Vue({
        data() {
          return {
            config: {
              fields: []
            }
          }
        }
      }).$mount('#app')
    </script>
  </body>
</html>

准备一个 json-schema 数据

{
  type: 'object',
  required: ['lastName', 'firstName'],
  properties: {
    lastName: { type: 'string', description: '姓' },
    firstName: { type: 'string', description: '名' },
    age: { type: 'number', description: '年龄' },
    subobj: {
      type: 'object',
      properties: {
        text1: { type: 'string', description: '子属性1' },
        text2: { type: 'string', description: '子属性2' }
      }
    }
  }
}

要想实现通过 json-schema 数据动态生成表单,需要定义一个渲染处理,在 jformer 准备渲染界面之前改变当前节点的数据结构,将 json-schema 数据转换成表单数据格式

// 定义一个函数,实现输出表单项
const buildFieldItem = field => {
  field.component = 'el-form-item'
  field.fieldOptions = {
    props: { label: field.description || field.name }
  }
}

// 定义一组字段处理方法,根据不同数据类型输出相应输入组件
const typeproviders = {
  // 当数据类型是 object 时,字段输出为表单
  object: field => {
    field.component = 'el-form' // 定义输出的组件
    field.fieldOptions = { props: { labelWidth: '150px' } }
    
    // 当数据类型时 object 时,属性 properties 可作为表单项
    // 将 properties 转换成一组属性的描述,赋给 children 属性
    // 在渲染 children 的时候,会单独根据 child 的数据类型输出特定表单项
    field.children = Object.keys(field.properties).map(key => {
      const name = field.parent ? `${field.parent}.${key}` : key
      return {
        ...field.properties[key],
        name,
        parent: name // 设置 parent 属性,当 child 也是对象时实现级联嵌套
      }
    })
  },
  // 当数据类型是字符串时,输出输入组件
  string: field => {
    // 每个输入组件都嵌套在表单项内
    buildFieldItem(field)
    field.children = [{ component: 'el-input', model: [field.name] }]
  },
  // 当数据类型是字符串时,输出数字输入组件
  number: field => {
    buildFieldItem(field)
    field.children = [
      { component: 'el-input-number', model: [field.name] }
    ]
  }
}

// 在 jformer 中注册渲染处理
window.jformer.default.use(({ provider }) => {
  provider(() => {
    return field => {
      ;(typeproviders[field.type] || new Function())(field)
    }
  })
})

关于表单验证,这里举一个是否必填的简单例子,json-schema 中定义属性是否必填是通过 object 类型的节点的 required 属性定义的,这里只需要在处理 object 节点时候将 properties 中属性名和 required 中属性对应上生成 rules 就可实现

必填验证处理

const objectprovider = field => {
  field.component = 'el-form'
  field.fieldOptions = {
    props: { labelWidth: '150px' }
  }
  field.children = Object.keys(field.properties).map(key => {
    const name = field.parent ? `${field.parent}.${key}` : key
    return {
      ...field.properties[key],
      name,
      // 生成 children 的时候同时生成 rules
      rules: [
        {
          required: !!(field.required || []).find(item => item === key),
          message: '必填项'
        }
      ],
      parent: name
    }
  })
}

const buildFieldItem = field => {
  field.component = 'el-form-item'
  field.fieldOptions = {
    props: {
      label: field.description || field.name,
      prop: field.name,
      rules: field.rules // 在处理表单项时候 rules 赋过来
    }
  }
}

完整示例如下,将以下定义存成 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>Json Schema 示例</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>
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/element-ui/lib/theme-chalk/index.css"
    />
  </head>
  <body>
    <div id="app">
      <j-former v-model="model" v-bind:config="config"></j-former>
      <p>数据: {{JSON.stringify(model)}}</p>
    </div>

    <script>
      const buildFieldItem = field => {
        field.component = 'el-form-item'
        field.fieldOptions = {
          props: {
            label: field.description || field.name,
            prop: field.name,
            rules: field.rules
          }
        }
      }

      const typeproviders = {
        object: field => {
          field.component = 'el-form'
          field.fieldOptions = {
            props: { labelWidth: '150px', model: field.model }
          }
          field.children = Object.keys(field.properties).map(key => {
            const name = field.parent ? `${field.parent}.${key}` : key
            return {
              ...field.properties[key],
              name,
              rules: [
                {
                  required: !!(field.required || []).find(item => item === key),
                  message: '必填项'
                }
              ],
              parent: name
            }
          })
        },
        string: field => {
          buildFieldItem(field)
          field.children = [{ component: 'el-input', model: [field.name] }]
        },
        number: field => {
          buildFieldItem(field)
          field.children = [
            { component: 'el-input-number', model: [field.name] }
          ]
        }
      }

      window.jformer.default.use(({ provider }) => {
        provider(() => {
          return field => {
            ;(typeproviders[field.type] || new Function())(field)
          }
        })
      })

      new Vue({
        data() {
          return {
            model: {},
            config: {
              fields: [
                {
                  type: 'object',
                  model: '$:model',
                  required: ['lastName', 'firstName'],
                  properties: {
                    lastName: { type: 'string', description: '姓' },
                    firstName: { type: 'string', description: '名' },
                    age: { type: 'number', description: '年龄' },
                    subobj: {
                      type: 'object',
                      properties: {
                        text1: { type: 'string', description: '子属性1' },
                        text2: { type: 'string', description: '子属性2' }
                      }
                    }
                  }
                }
              ]
            }
          }
        }
      }).$mount('#app')
    </script>
  </body>
</html>

相关链接

设计器:Github Gitee

动态表单:Github Gitee

数据源、监听、转换表达式具体定义参考 文档