废话少说直接上需求背景和实现思路
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首先我们来看 给定要求的设计图
2.1 接上 如果我们希望是此类的表单结构,那我们在设计表单虚拟语法结构,
的时候应该如下图所示:
1.整体的虚拟语法结构是一个数组
2.数组包含两层,第一层表示 对一级表单的名称描述,ID
3.第二层表示 对一级一级表单下面具体表单个数,类型,规则进行描述
所以我们可以抽象出这样的结构
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 省市县
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
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);
});
}
})