[設計器 vjdesign] 快速實現動態表單配置系統

vjdesign 是一個可視化界面設計器,jformer 是一個動態表單呈現組件,都具備可擴展性實現豐富功能,如今使用vjdesign + jformer 快速開發一套動態表單配置系統javascript

系統的特性以下:css

  • 提供幾種經常使用的 elementui 組件,可在設計器拖動實現佈局和設置組件屬性
  • 可設置 axios 數據源獲取表單數據
  • 表單支持設置驗證條件
  • 表單可保存和預覽

頁面已經集成了 elementui 經過配置可增長支持的組件,若是有真實接收數據的服務可實現表單提交html

完整示例

這裏先給出實際運行效果vue

codesandbox 可能有點慢,須要多等一下子java

示例實現
效果展現ios

設計器實現

設計器經過配置實現了組件支持和屬性編輯,並可保存佈局和打開預覽頁,在這篇文章裏實現了 elementui 表單組件的簡化配置,這裏直接拿來用git

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 庫做爲數據源實現服務數據的獲取與提交github

extends.js 的實現npm

/** * 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 渲染處理,讓設計時也呈現出佈局效果element-ui

設計器 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

相關文章
相關標籤/搜索