如何实现微信小程序动态表单

废话少说直接上需求背景和实现思路

1.需求背景

我们希望小程序中某个页面中的用户输入数据表单上可以在后台的cms系统或者是可以通过接口进行动态控制的 ;也就是说我们只要通过在管理系统进行勾选以后,编辑,点击生成页面所对应的表单部分就会出现我们动态配置以后的表单(包括表单的个数,每个表单的label,类型,id,默认值等)

2.起初的实现思路

最早我拿到这个需求的时候,如果希望实现这种后台配置好,前端小程序动态解析生成表单的过程,主要有这三种方式:

1.后台配置的表单元素是html富文本,小程序前端通过三方控件wxParse进行动态解析

2.将前端的表单抽离成最小单元组件并以umd形式的组件,放到服务端,小程序拉取服务端umd的组件进行动态解析

3.将前端的表单抽离成最小单元组件不放在远程服务器,只放在前端,后台通过一套虚拟语法结构(AST)对表单类型,id,规则进行描述,小程序前端动态解析

以上三种方式各自的问题

方式1问题:

wxparse 确实可以进行对富文本解析成表单,但是无法将富文本内部的行为,数据进行共享出来(方案pass掉)

方式2问题:

将前端的表单抽离成最小单元组件并以umd形式的组件,放到服务端,目前小程序并不支持umd形式的打包,并且小程序无法加载远程组件

咳咳 那就只剩下第三种了!!!

本来想用1或者2做的高大上点,但是最终只能这种方式,容我伤心1秒钟

切入主题 制作动态表单的具体实现

1.方案须知

最终我们选择方案:

将前端的表单抽离成最小单元组件不放在远程服务器,只放在前端,后台通过一套虚拟语法结构(AST)数据结构对表单个数,类型,id,规则进行描述,小程序前端动态解析

2. 设计动态表单语法结构(AST)

2.1首先我们来看 给定要求的设计图

image.png

2.1 接上 如果我们希望是此类的表单结构,那我们在设计表单虚拟语法结构,

的时候应该如下图所示:

1.整体的虚拟语法结构是一个数组
2.数组包含两层,第一层表示 对一级表单的名称描述,ID
3.第二层表示 对一级一级表单下面具体表单个数,类型,规则进行描述


image.png

所以我们可以抽象出这样的结构

const formConfig = [
  {
    fieldId: '101', //对应一级表单层级
    fieldName: '基本信息',
    formInfo: [    //每个层级下面 具体表单元素
      {
        label: '名字',//标题
        type: 'text', //表单类型 text,upload,picker,time
        id: 'input-name-form',   //表单id
        placeholder: '输入您的姓名',//设置文本框默认提示
        data: [], //填充表单的数据 例如下拉框
        role: {
           //验证规则正则表单式 
          //1.reg正则表达式 
          //2.notnull 非空验证 
          //3.null 不验证
          type: 'reg',
          value: '',//正则表达式
        },
        force: true,//是否必输入
      },
      {
        label: '身份证',//标题
        type: 'text', //表单类型 text,upload,picker,time
        id: 'input-id-form',   //表单id
        placeholder: '输入您的姓名',//设置文本框默认提示
        data: [], //填充表单的数据 例如下拉框
        role: {
          //验证规则正则表单式 
          //1.reg正则表达式 
          //2.notnull 非空验证 
          //3.null 不验证
          type: 'reg',
          value: '',//正则表达式
        },
        force: true,//是否必输入
      },
      {
        label: '性别',//标题
        type: 'picker', //表单类型 text,upload,picker,time
        id: 'input-sex-form',   //表单id
        placeholder: '输入您的姓名',//设置文本框默认提示
        data: [
          { id: 1, name: '男' },
          { id: 2, name: '女' },
        ], //填充表单的数据 例如下拉框
        role: {
          type: 'reg',
          value: '',//正则表达式
        },
        force: true,//是否必输入
      }
    ]
  },
  {
    fieldId: '102',
    fieldName: '参赛信息',
    formInfo: [
      {
        label: '参赛编号',//标题
        type: 'text', //表单类型 text,upload,picker,time
        id: 'match-id-form',   //表单id
        placeholder: '输入您的姓名',//设置文本框默认提示
        data: [], //填充表单的数据 例如下拉框
        role: {
          type: 'reg',
          value: '',//正则表达式
        },
        force: false,//是否必输入
      },
      {
        label: '组别',//标题
        type: 'picker', //表单类型 text,upload,picker,time
        id: 'group-id-from',   //表单id
        placeholder: '输入您的姓名',//设置文本框默认提示
        data: [
          { id: 1, name: '第一组' },
          { id: 2, name: '第二组' },
          { id: 3, name: '第三组' },
        ], //填充表单的数据 例如下拉框
        role: {
          type: 'reg',
          value: '',//正则表达式
        },
        force: false,//是否必输入
      },
      {
        label: '参赛时间',//标题
        type: 'time', //表单类型 text,upload,picker,time
        id: 'join-time-form',   //表单id
        placeholder: '选择时间',//设置picker未选择默认提示
        data: [], //填充表单的数据 例如下拉框
        role: {
          type: 'reg',
          value: '',//正则表达式
        },
        force: false,//是否必输入
        timeType:'date',//当类型为time的时候 指定具体的时间控件类型  date是年月日选择器 还是周  time还是天的日期(时分) timeType
        endTime: '',//设置 表示有效日期范围的开始,字符串格式为"YYYY-MM-DD"
        starTime: ''//设置 表示有效日期范围的开始,字符串格式为"YYYY-MM-DD"
      },
      {
        label: '上传证书',//标题
        type: 'upload', //表单类型 text,upload,picker,time
        id: 'upload-zhn-form',   //表单id
        placeholder: '输入您的姓名',//设置文本框默认提示
        data: [], //填充表单的数据 例如下拉框
        role: {
          type: 'reg',
          value: '',//正则表达式
        },
        force: false,//是否必输入
      },
      {
        label: '所在区域',//标题
        type: 'region', //表单类型 text,upload,picker,time
        id: 'region-area-form',   //表单id
        placeholder: '当前选择',//设置文本框默认提示
        data: [], //填充表单的数据 例如下拉框
        role: {
          type: 'reg',
          value: '',//正则表达式
        },
        force: false,//是否必输入
      }
    ]
  },
]

export default formConfig

对抽象的ast结构中字段的解释
** fieldId 对应一级表单层级**
** fieldName 基本信息 **
** formInfo 每个层级下面 具体表单元素**
** label 每个表单元素标题**
** type 表单类型 text(文本类型组件),upload( 上传组件),picker(选择组件),time(时间组件),region(省市县组件) **
** id 表单id 这里为什么需要id呢?**
** placeholder 设置文本框默认提示 **
** data 填充表单的数据 例如下拉框**
** role 验证规则正则表单式 对象形式 **
** type 验证类型 1.reg正则表达式 2.notnull 非空验证 3.null 不验证 **
** value type为正则的时候 进行执行 value中携带的正则表达式**
** force 表单是否必输入 **

3.表单抽离成最小单元组件

3.1所以根据上面的的抽象以后我们可以大体上将组件按照最小化的方式进行抽出来,叫做最小元组件

1.text-input文本框输入组件
2.text-picker选框组件(性别,分组选框)
3.text-time 时间组件
4.image-upload 文件上传组件
5.region-picker 省市县

image.png

3.2 当组件抽离出来以后,组件的默认数据的获取可以通过ast中获得,我们可以在动态渲染中通过type进行条件控制,但是组件内部用户数据的如何如何在用户点击按钮时候拿到? 每个具体表单元素如何动态渲染 ?一起看第四,第五点

4.元组件中数据如何共享出来

微信官方可用的技术方案须知,ps:基于 Component 的 behaviors中的selectComponent,当我们希望拿到自定义组件中的一些数据时候,可以基于this.selectComponent('#自定义组件的id'),所以这也是为什么在抽象AST结构的时候,需要加一个对应的id,并且每个form元素的id都不是唯一的

实现该过程

//在自定义组件中
Component({
data: {
     input_text:'', //第一步:定义接受文本框数据
 },
methods:{
    //第二步:改变data的值 对应文本框上面绑定的事件
   enterValue:function(e){
        console.log(e.detail.value);
       this.setData({
         input_text: e.detail.value
       });
    }
},
//第三步: 通过声明组件间共享数据的behaviors属性达到数据共享
behaviors: ['wx://component-export'],
  //第四步:将你要暴露的数据导出
  export() {
    return { textvalue: this.data.input_text }
  }
});
//获取这个组件中值的时候,在外层组件的事件中
this.selectComponent('#id').textvalue

微信官方behaviors文档

5.每个具体组件的封装和动态渲染

1.组件的封装 举例image-upload组件,因为首次未曾选择图片上传,upload的中只有左侧label和右侧按钮,通过组件内部一个inputType是否上传类型来判断如果已经上传那就改用文本框显示上传成功的图片绝对路径,再次点击,则重新打开图片选择器

<view class='reports-form-box form-inner'>
          <view class='reports-form-input input-all-width'>
            <view class='form-inputname'>{{forminfo.label}}<text wx:if="{{forminfo.force}}" class="force_text">*</text></view>
            <block wx:if="{{inputType==='upload'}}"><button bindtap='uploadImage' class='form-inputtext form-upload'></button></block>
            <block wx:if="{{inputType==='uploaded'}}">
               <input  bindtap='uploadImage' value='{{input_text}}' class='form-inputtext'></input>
            </block>

          </view>
</view>
import util from '../../utils/util.js';
Component({
  /**
   * 组件的属性列表
   */
  options: {
   //开启共享css模式
    addGlobalClass: true,
  },
  properties: {
    label: {
      type: String,
      value: '',
    },
    formid: {
      type: String,
      value: '',
    },
    forminfo: {
      type: Object,
      value: {}
    }
  },

  /**
   * 组件的初始数据
   */
  data: {
     wx_back_image:'',
     inputType:'upload',
     input_text:'', //获取到上传成功以后的输入地址
  },
  behaviors: ['wx://component-export'],
  export() {
    return { input_text: this.data.input_text}
  },
  /**
   * 组件的方法列表
   */
  methods: {
    uploadImage:function(){
       wx.chooseImage({
         count: 1,
         sizeType: ['original','compressed'],
         sourceType: ['album', 'camera'],
         success: (res)=>{
           console.log(res);
           const { tempFilePaths} =res;
           console.log('wx image path::::',tempFilePaths[0]);
           this.setData({
             wx_back_image: tempFilePaths[0]
           });
           //util.wx_upload_image 方法是一个模拟微信上传成功的接口 只为测试
           util.wx_upload_image(tempFilePaths[0]).then((resp)=>{
               console.log(resp);
               this.setData({
                 inputType: 'uploaded',
                 input_text: resp,
               });
           });
         },
       })
    },
  }
})

2.组件的动态渲染 那我们可以在遍历第一层表单之后,在遍历每一个第一层表单内部具体的form元素的时候,通过type来动态加载我们封装好的各个元组件

<view class='reports-container'>

   <view class='reports-box' 
    wx:for="{{formList}}"
    wx:for-index="idx"  
    wx:for-item="fieldItem"
    wx:key="{{fieldItem.fieldId}}">
     <view class='reports-header'>
        <view class='reports-header-line'></view>
        <view class='reports-header-name'>{{fieldItem.fieldName}}</view>
     </view>
     <view class='reports-form'>
       <block wx:for="{{fieldItem.formInfo}}" 
                  wx:for-index="idx"  
                  wx:for-item="items"
                  wx:key="{{items.id}}">
                <!--动态渲染元组件的过程-->
                <block wx:if="{{items.type==='text'}}">
                   <!--每个元组件都需要传递id和当前组件全部的信息 便于元组件内部解析-->
                   <text-input id="{{items.id}}" forminfo="{{items}}"></text-input>
                </block>
                <block wx:if="{{items.type==='picker'}}">
                    <text-picker id="{{items.id}}" forminfo="{{items}}"></text-picker>
                </block>

                <block wx:if="{{items.type==='time'}}">
                    <text-time id="{{items.id}}" forminfo="{{items}}"></text-time>
                </block>

                <block wx:if="{{items.type==='region'}}">
                    <region-picker id="{{items.id}}" forminfo="{{items}}"></region-picker>
                </block>

                 <block wx:if="{{items.type==='upload'}}">
                    <image-upload id="{{items.id}}" forminfo="{{items}}"></image-upload>
                </block>

                

                

        </block>
     </view>

 

  </view>

   <view class='reports-form form-center'>
   <!-- 并提交订单 -->
    <view bindtap='getInputValue' class='reports-form-btn'>提交</view>
  </view>
 
</view>

6.测试并使用

//导入组件ast数据结构 开发过程中有后端返回过来
import formConfig from '../../utils/formconfig.js';

Page({

  /**
   * 页面的初始数据
   */
  data: {
    formList: formConfig
  },
  getInputValue:function(){
    // const v = this.selectComponent('#my-input');
    // console.log(v);
    // const ids=this.data.formIds;
    // for(var key in ids){
    //   console.log(ids[key], key, `#${key}`);
    //    console.log(this.selectComponent(`#${key}`));
    // }
    let result =[];
    formConfig.forEach((item,i)=>{
         result.push(item.formInfo)
    })
    //console.log(result);
    var forms=result.reduce((a,b)=>{
      return a.concat(b)
    })
    //console.log(forms);
    forms.forEach((items,i)=>{
      //console.log(items.id);
      const v = this.selectComponent('#' + items.id);
      console.log(items.id,':::',v);
    });
  }
})

测试结果

image.png

image.png

项目github地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • 1.几种基本数据类型?复杂数据类型?值类型和引用数据类型?堆栈数据结构? 基本数据类型:Undefined、Nul...
    极乐君阅读 5,496评论 0 106
  • 买煎饼果子的时候,被煎饼大哥的手法惊艳了,愣是把摊煎饼看出了舞蹈的韵味。没想到,这看似普通的流程,竟有律动之美。 ...
    所遇皆欢喜阅读 600评论 0 2
  • 一、自律来自于自尊 有的家长抱怨:“孩子没有自律性:做事拖拉、磨蹭;没有上进心、懒散”等。常问:怎样能让孩子有自律...
    吴九龙阅读 284评论 0 0
  • 今日与财务部对新财年的核算原则,首先建立营销部门的阿米巴核算模型,不能让员工找到为自己干的感觉,不能让员工准确衡量...
    一世惊鸿阅读 144评论 0 0
  • 文/逗号 是不是要改变方式?这几个月磨练了我的意志和耐性,偶尔想起来,根本不像现实中的情况。 又或许,这真的是磨练...
    青禾吖阅读 264评论 1 4