Skip to content

bsFormBuilder

一个基于 Bootstrap (v4.x) + JQuery 的、拖拽的表单构建工具。

bsFormBuilder

  • 1、基于 Bootstrap (v4.x) + JQuery,简单易用
  • 2、拖动的 html 组件,支持通过 Json 自定义扩展
  • 3、组件的属性面板,支持通过 Json 自定义扩展
  • 4、支持导出 html 和 json,然后自己通过 json 来构建 UI 页面
  • 5、支持导入 json 到 bsFormBuilder,用来进行二次编辑
  • 6、丰富的 API,方便二次开发和扩展
  • 7、支持 “模板” 功能,可以选择已有模板进行二次开发
  • 8、内置轻量的 html 渲染引擎,速度极快,极好用~~
<div id="builder"></div>
<script>
$('#builder').bsFormBuilder({...});
</script>

在使用前,需要导入 bootstrap 和 jquery 的相关文件。

<link href="path/bootstrap.min.css" rel="stylesheet">
<link href="path/bootstrap-icons.css" rel="stylesheet">
<script src="path/jquery.min.js"></script>
<script src="path/bootstrap.bundle.min.js"></script>
<!-- 导入 bs-form-builder 依赖-->
<link href="path/bs-form-builder.min.css" rel="stylesheet">
<script src="path/bs-form-builder.min.all.js"></script>

通过 $('#builder').bsFormBuilder({options...}); 进行初始化,bsFormBuilder 方法可以传入 options 配置,options 内容如下:

{
//模式: "view" 预览模式, "builder" 构建工具模式,默认值为 builder
mode: "builder",
templateEngine: null, //支持自定义模板渲染引擎,默认使用 fasty
bsFormContainerSelector: ".bsFormContainer", // 设计容器
bsFormContainerFilterSelector: ".bsFormFilter", // 设计容器里,不允许拖动的组件 class
bsFormContainerSortableGroup: "shared", // 配置主容器里的 group 名称, 其实就是Sortable的组
bsFormContainerPlaceHolderSelector: ".bsFormContainer-placeholder", // 设计容器里的提示内容
bsFormPropsSelector: ".bsFormProps", // 面板内容
bsFormPropsTitleSelector: ".bsFormPropsTitle", // 面板标题
bsFormPropsFilter:null, // 属性过滤器,用于对特殊的组件进行属性过滤
bsFormPropsItemAppended:null, //监听 props html 内容被追加的时候会被调用可以是函数类型(builder, prop, data) =》 {},用于拖动组件到容器中的初始化
customBuilderStructure: false, // 自定义容器面板,
onDataChange:null, //监听数据更新(更新之前)
onDataChanged:null, //监听数据更新(更新之后)
renderEmptyDrags: null,//当左边的拖动按钮分类找不到任何组件时,调用该方法
useComponents:[], //使用哪些组件
unUseComponents:[], //排除哪些组件(不使用哪些组件)
components: '#(CPATH)/admin/template/block/components', //可以传个数组或者url get请求返回的格式要和components的格式一样
//支持自定义渲染方法,或者服务端渲染,就是接受一个请求后台返回一个html字符串渲染到页面上,
customRender: '#(CPATH)/admin/template/block/render',
optionsDatasources: null, // 定义数据源,就是给option定义的
//初始化数据,就是容器中元素的位置等..信息,不停的拖动一些位置信息会有变化,通过
//$('#builder').bsFormBuilder().data("bsFormBuilder").exportToJson()将数据保存到后台,下次刷新页面的时候就可以保存状态了
//重点: datas: '#(CPATH)/admin/template/block/datas',可以是url 返回对应格式的json数据
datas: [
{
"id":"c4044aefc2",
"tag":"select",
"label":"下拉菜单",
"name":"select_1",
"options":[{"text":"选项1","value":"value1"},{"text":"选项2","value":"value2"}],
"optionsTitle":"选项"
},
{
"id":"c4044aefc3",
"tag":"checkbox",
"label":"复选框",
"name":"select_2",
"options":[{"text":"选项1","value":"value1"},{"text":"选项2","value":"value2"}],
"optionsTitle":"选项"
}
],
//操作按钮列表
actionButtons:[],
//操作按钮模板
actionButtonTemplate:'',
//组件扩展配置,配置的内容可以覆盖掉系统的配置
components: {},
//每个组件的默认属性
defaultProps: [],
//属性渲染的 html 模板配置
propTemplates: {},
//初始化回调方法
onInit: function(bsFormBuilder){},
}
  • 1、通过 $('#builder').bsFormBuilder('methodName',arguments...); 方法调用。

  • 2、或者可以通过 $('#builder').bsFormBuilder().data('bsFormBuilder') 来获取 bsFormBuilder 实例,然后直接调用其方法。

bsFormBuilder 支持的方法如下:

  • init: 初始化,bsFormBuilder 自动调用
  • render(data, withActive):通过 data 数据,来渲染出一个 html 内容
  • renderDefault(data): 系统内置的默认渲染方法,当 component 未定义自己的 render 方法的时候,使用该方法进行渲染。
  • deepCopy(target, withNewElementIdAndId):深度拷贝工具类
  • createComponentData(component):通过 component 来创建 data 数据
  • genRandomId():生成一个随机的 id
  • makeFormItemActive(elementId):设置选择状态
  • deleteFormItem(elementId):删除一个 formItem
  • copyFormItem(elementId):复制一个 formItem
  • getDataByElementId(elementId):通过一个节点 id 获取 data 数据
  • removeDataByElementId(elementId):通过节点 id 移除 data 数据
  • getParentArrayByElementId(elementId):通过节点 id 获取其所在的 数组
  • refreshDataIndex($parentElement):刷新 data 的 index 属性
  • refreshPropsPanel():渲染(刷新)属性面板
  • renderPropTemplate(prop, data, template):渲染属性模板
  • exportToJson()导出 json 数据(重点常用)
  • exportToHtml():导出 html 数据
  • getDatas():获取 datas 数据,并可以对其进行修改
  • addDataToRoot(data):添加一个 data 到根节点
  • addDatasToRoot(array):添加一个 data 数组到根节点
  • addDataToContainer(data, containerElementId, index):添加一个 data 到一个子container
  • addDatasToContainer(array, containerElementId, index):添加一个 data 数组到一个子container
  • updateDataAttr(data, attr, value):更新一个 data 的属性,并同步到 html 显示
  • refreshDataElement(data):刷新 data 数据到 html
  • isViewMode():是否是视图模式
  • isBuilderMode():是否是构建模式(构建工具)
  • clear():清空设计的所有内容,然后可以重新设计
  • destroy():销毁整个组件

1、刷新右侧的属性面板:refreshPropsPanel

当动态添加右侧自定义输入组件的时候可以在这里初始化

2、clear():清空设计的所有内容,然后可以重新设计

3、destroy():销毁整个组件

在 bsFormBuilder 中,组件是通过一个 json 内容来定义的,一个完整的组件的 json 内容 如下:

components: [
{
"name": "灵活栅格",
//全局唯一的id
"tag": "sgrid",
//拖动的元素属性,名称 图标\位置索引..., 这里的type 是表示将拖动元素渲染到那个区域
<--
<div class="tab-content">
<div class="tab-pane fade show active" id="component" role="tabpanel" aria-labelledby="component-tab" >
<div class="bsFormDrags-title">模板自带</div>
<div class="bsFormDrags d-flex align-items-center" data-type="custom">{"type": "custom"会展示到这}</div>
<div class="bsFormDrags-title">表单组件</div>
<div class="bsFormDrags d-flex align-items-center" data-type="base"></div>
<div class="bsFormDrags-title">辅助组件</div>
<div class="bsFormDrags d-flex align-items-center" data-type="assist"></div>
<div class="bsFormDrags-title">布局组件</div>
<div class="bsFormDrags d-flex align-items-center" data-type="container"></div>
</div>
<div class="tab-pane fade" id="template" role="tabpanel" aria-labelledby="module-tab">
<div id="bs-template-item-list" class="bs-template-item-list"></div>
</div>
</div>
-->
"drag": {
"title": "灵活栅格",
"type": "custom",
"index": 100,
"icon": "bi bi-grid-1x2"
},
{<!--自定义选项的数量-->}
optionsCounter: 2,
{<!--是否带options配置,可以将 props 和 option分开来记,属性是组件带的,选项是自己编辑的,是两个区域-->}
withOptions: true,
{<!--这里是配置右侧模板选项的,可以根据输入的值自定义数据-->}
optionsTitle: '栅格配置',
{<!--datasource/custom 如果是custom 使用的是defaultOptions的数据,如果是datasource使用的就是optionsDatasources: []、定义的数据-->}
optionsTypes: 'custom', //options 类型值支持自定义一种方式
opiontsAdd: function () {
return {
text: "栅格" + (++this.optionsCounter),
value: 12
}
},
defaultOptions: [
{
text: "栅格1",
value: 6
},
{
text: "栅格2",
value: 6
}
],
"template": '<div class="bsFormItem">' +
' <div class="form-group clearfix">' +
' <div class="row pdlr-15">' +
' {{~for (var i = 0;i<options.length;i++)}}' +
' <div class="col-{{options[i].value}} bsItemContainer">{{$children[i]}}</div>' +
' {{~end}}' +
' </div>' +
' </div>' +
'</div>',
"onPropChange": function (bsFormBuilder, data, propName, value) {
if (propName !== "options" || !data.children) {
return false;
}
//修改顺序的时候,保证顺序下的 data 跟着修改
var idDataMapping = {};
let oldOptions = data.options;
let newOptions = value;
for (let i = 0; i < oldOptions.length; i++) {
let oldOption = oldOptions[i];
idDataMapping[oldOption.elementId] = data.children[i];
}
for (let i = 0; i < newOptions.length; i++) {
let newOption = newOptions[i];
data.children[i] = idDataMapping[newOption.elementId];
}
}
}
],
  • name:组件名称

  • tag: 组件 tag,全局唯一,若用户定义的组件 tag 与系统一样,则会覆盖系统的组件定义。

    重点:覆盖的只是相同的字段

    {
    "name": "灵活栅格",
    "drag": {
    "type": "layout",
    "index": 0,
    "icon": "bi bi-grid-1x2",
    "title": "灵活栅格"
    },
    "tag": "sgrid"
    },
    //灵活栅格
    {
    "name": "灵活栅格",
    "tag": "sgrid",
    "drag": {
    "title": "灵活栅格",
    "type": "container",
    "index": 100,
    "icon": "bi bi-grid-1x2"
    },
    //这后面的字段还在不会覆盖
    optionsCounter: 2,
    withOptions: true,
    optionsTitle: '栅格配置',
    optionsTypes: 'custom', //就是自己定义options的json数据(注意这里是数据,并不是数据格式)的意思,还有一个是datasource 就是使用后台返回数据
    opiontsAdd: function () {
    return {
    text: "栅格" + (++this.optionsCounter),
    value: 12
    }
    },
    //这里的数据格式,默认只能是{text,value}如果需要修改格式的话,需要改源码
    defaultOptions: [
    {
    text: "栅格1",
    value: 6
    },
    {
    text: "栅格2",
    value: 6
    }
    ],
    "template": '<div class="bsFormItem">' +
    ' <div class="form-group clearfix">' +
    ' <div class="row pdlr-15">' +
    ' {{~for (var i = 0;i<options.length;i++)}}' +
    ' <div class="col-{{options[i].value}} bsItemContainer">{{$children[i]}}</div>' +
    ' {{~end}}' +
    ' </div>' +
    ' </div>' +
    '</div>',
    "onPropChange": function (bsFormBuilder, data, propName, value) {
    if (propName !== "options" || !data.children) {
    return false;
    }
    //修改顺序的时候,保证顺序下的 data 跟着修改
    var idDataMapping = {};
    let oldOptions = data.options;
    let newOptions = value;
    for (let i = 0; i < oldOptions.length; i++) {
    let oldOption = oldOptions[i];
    idDataMapping[oldOption.elementId] = data.children[i];
    }
    for (let i = 0; i < newOptions.length; i++) {
    let newOption = newOptions[i];
    data.children[i] = idDataMapping[newOption.elementId];
    }
    }
    },
  • drag:左侧显示的模块图标

    "drag": {
    //模块标题
    "title": "日期和时间",
    //该模块在那个分类
    "type": "base",
    //位置索引
    "index": 100,
    //https://icons.getbootstrap.com 对应的类名图标
    "icon": "bi bi-calendar2-day"
    },
  • props:组件支持的属性配置

  • propsfilter:系统属性过滤配置,若为配置则显示系统存在的 props 定义

  • withOptions:该属性是否带有 options 配置

  • defaultOptions:options 的默认配置值

  • optionsTypes:options 支持的类型,目前系统内置两种类型: 自定义(custom) 和 数据源(datasource)

  • optionsTitle:options 属性面板的标题

  • template:模板,可以是一个 string 字符串,也可以是一个返回一个 string 的 function(component, data)。

  • onAdd:当组件被添加到 html 的时候回调,或者被拖动的时候,注意:当组件从一个子容器被拖动到另一个子容器,也会调用此方法。

  • onPropChange:当属性被修改的时候,回调。

注意:默认情况下,无需配置 onAdd、onPropChange 方法。除非您已经了解的其作用。

bsFormBuilder 已经内置了 4 个属性:tagidnamelabel,任何组件都包含有这 4 个属性(除非定义了 propsfilter),然而,一个复杂的组件除了这 4 个属性以外,还应该有其他的属性,比如 textarea 应该有行数 rows

所以,textarea 组件的定义如下:

//定义好右侧要展示的模板
var defaultPropTemplates = {
//number是类型,组件会选择所需要的模板
number: function () {
return '<div class="form-group clearfix">' +
' <div class="form-label-left">' +
' {{~ if(required)}}' +
' <span class="red required">*</span>' +
' {{~end}}' +
' <label for="{{id}}">{{label}}</label>' +
' </div>' +
' <div class="flex-auto">' +
' <input id="{{id}}" {{~if (disabled)}}disabled{{~end}} type="text" data-attr="{{name}}" placeholder="{{placeholder}}" class="onkeyup form-control" value="{{value}}">' +
' </div>' +
' </div>';
},
//组件
{
"name": "多行输入框",
"tag": "textarea",
//组件需要的属性
"props": [
{
//这里name是自定属性字段变量名,在template中 {{rwos}} 获取变量
name: "rows",
//当前属性变量的赋值,要用那种模板,通过propTemplates选项定义
type: "number",
label: "行数",
placeholder: "请输入行数...",
defaultValue: 3,
disabled: false,
required: true,
},
// 可以设置多个字段
{
name: "rows",
//这里要指定右侧展示属性的模板类型,通过propTemplates选项定义
type: "number",
label: "行数",
placeholder: "请输入行数...",
defaultValue: 3,
disabled: false,
required: true,
}
],
"template":"...."
}

props 是一个组件的属性配置,系统内置了 4 个属性,我们可以通过这个来定义组件自己的属性。

例如,textarea 需要定义行号属性,因此,需要添加如下的配置:

{
name: "rows",
type: "input",
label: "行数",
placeholder: "请输入行数...",
defaultValue: 5,
disabled: false,
required: true,
}

textarea 定义了名称为 rows 的属性,template 必须通过 {{rows}} 来接收该属性的设置:

<textarea rows="{{rows}}">{{value}}</textarea>
  • name: 属性名称
  • type: 属性渲染类型,支持有:input/select/number/switch/checkbox/radio/none,可以扩展其他属性类型,或者复写这些属性的默认行为。
  • label: 属性在面板里的label
  • placeholder: 属性里 placeholder 内容
  • defaultValue: 属性的默认值
  • disabled: 属性面板里是否启用
  • required: 属性面板里是否必填
- 输出:{{attr}}
- for循环:{{~ for(let item of array)}} -{{item.name}}- {{~end}}
- if循环:{{~ if( x === "string")}} -{{x}}- {{~end}}
- if-elseif-else循环:{{~ if( x === "string")}} - {{~elseif(x === "other")} - {{~else}} - {{~end}}
  • $builder : bsFormBuilder 实例
  • $component:component 实例
  • $data:当前 component 的数据
  • $children:html 数组,若当前是一个容器,那么该容器下的 html 内容。

理论上,属性面板支持 input、select、number、switch、checkbox、radio 这 6 种属性类型,已经够用了,不过 bsFormBuilder 依然支持通过在初始化的时候,通过初始化函数来扩展自己的属性面板设置类型。

属性扩展如下:

var options = {
//这里的属性模板其实就是右侧展示的 有可能和option分不太清,个
propTemplates: {
otherType:function (){
return '<div> </div>'
},
target: function () {
return '<div class="form-group clearfix">' +
' <div class="form-label-left">' +
' <legend class="col-form-label pt-0">打开方式</legend>' +
' </div>' +
' <div class="flex-auto">' +
' <select class="custom-select onchange" {{~if (disabled)}}disabled{{~end}} data-attr="{{name}}">' +
' <option value="">当前窗口</option>' +
' <option value="_blank" {{value == "_blank" ? "selected" : ""}}>新窗口(_blank)</option>' +
' </select>' +
' </div>' +
' </div>';
},
image: function () {
return '<div class="form-group clearfix">' +
' <div class="form-label-left">' +
' {{~ if(required)}}' +
' <span class="red required">*</span>' +
' {{~end}}' +
' <label for="{{id}}">{{label}}</label>' +
' </div>' +
' <div class="flex-auto">' +
' <div class="jpress-image-browser">' +
' <input type="hidden" class="onchange" id="{{id}}" data-attr="{{name}}" value="{{value}}" />' +
' {{~ if (value)}}' +
' <img src="{{value}}" />' +
' {{~else}}' +
' <img src="#(CPATH)/static/commons/img/nothumbnail.jpg" />' +
' {{~end}}' +
' <div class="image-action">' +
' <a class="image-delete"> <i class="fa fa-trash"></i></a>' +
' <a class="image-edit"> <i class="fa fa-edit"></i></a>' +
' </div>' +
' </div>' +
' </div>' +
' </div>';
},
video: function () {
return '<div class="form-group clearfix">' +
' <div class="form-label-left">' +
' {{~ if(required)}}' +
' <span class="red required">*</span>' +
' {{~end}}' +
' <label for="{{id}}">{{label}}</label>' +
' </div>' +
' <div class="flex-auto">' +
' <div class="btn-group ">' +
' {{~ if($data.vid && $data.poster)}}' +
' <a id="group-all" href="javascript:;" class="btn btn-default chooseVideo active"> <i class="bi bi-camera-video"></i> 已选择</a>' +
' <a id="group-28d" href="javascript:;" class="btn btn-default deleteVideo">删除</a>' +
' {{~ else}}' +
' <a id="group-all" href="javascript:;" class="btn btn-default chooseVideo"> <i class="bi bi-camera-video"></i> 选择视频</a>' +
' {{~ end}}' +
' </div>' +
' </div>' +
' </div>';
},
},
defaultProps:[
{
name: "属性名称",
type: "otherType",
label: "属性label",
placeholder: "请输入行数..."
}
]
}
$('#builder').bsFormBuilder(options)
var options = {
propTemplates: {
otherType:function (){
return '<div> </div>'
}
},
components:{
component1 :{
"name": "自定义组件1",
"tag": "component1",
"props": [
{
name: "username",
type: "otherType",
label: "用户名",
}
],
"template":`<div>{{username}}</div>`
},
component2 :{
"name": "自定义组件2",
"tag": "component2",
"props": [
{
name: "属性名称2",
type: "otherType",
label: "属性名称2",
}
],
"template":"...."
}
}
}
$('#builder').bsFormBuilder(options)

在很多场景下,一些组件(比如复选框、下拉菜单等)的内容不是写死的,也不是自定义的,而是来之某个 API 接口,或者说来源于某个 “数据源”。此时,我们要做一下配置:

  • 简单来说,当某些 模块的选项需要动态定制的时候,就需要options了
  • 注意:options 只有一个模板类型,要使用多个的话,需要修改源码
  1. 要修改模板类型,将 optionsTypes 作为类型,只后options获取模板的方式

    //找到refreshPropsPanel 方法,在获取options模板的地方修改
    //let template = this._getPropTemplateByType("options");
    let template = this._getPropTemplateByType(this.currentData.component.optionsTypes ?? "options");
  2. 修改 options 的格式

    //找到同步方法,
    //*注意:当自定义格式的时候,input修改之后,不自动更新 props中的options选项,就要手动调用同步方法
    /**
    * 同步 currentData 的 options
    * @private
    */
    _syncCurrentDataOptionsFromPropSetting: function () {
    var options = [];
    var optionItems = this.$propsPanel.children(".options").children(".option-item");
    optionItems.each(function (index, item) {
    //之后修改保存数据的格式
    var title = $(item).find(".option-input.title").val();
    var desc = $(item).find(".option-input.desc").val();
    var href = $(item).find(".option-input.href").val();
    var elementId = $(item).attr("id");
    options.push({title, desc, href, elementId});
    });
    this.updateDataAttr(this.currentData, "options", options);
    },

1、在初始化 bsFormBuilder 的时候,定义好数据源列表,例如:

var options = {
optionsDatasources:[
{
text: "数据源1", value: "ds1",
options: [
{text: "aaa1", value: "value"}
]
},
{
text: "数据源2", value: 'ds2',
options: [
{text: "bbb1", value: "value1"},
{text: "bbb2", value: "value2"}
]
},
{
text: "数据源3", value: "ds3",
options: function (){
return ["..."]
}
},
{
text: "数据源4", value: "ds4",
options: "http://www.***.com/***.json"
}
]
}
$('#builder').bsFormBuilder(options);

数据结构说明:

  • 1、optionsDatasources 里有多个数据源,可以配置为 array、function、url (String类型)
  • 2、数据源有 3 个字段: text / value / options
  • 3、每个数据源通过 options 字段来定义数据集合
  • 4、数据(option)是由 value 和 text 组成的

PS:

  • 1、optionsDatasources 可以配置为一个 url 地址(要求返回 json,json 内容必须有 datasources 字段来描述数据源数据)。
  • 2、数据源里的 options 字段,可以是一个数据集合,也可以是一个 function,或者一个 url 地址 (要求返回 json,json 内容必须有 options 字段来描述数据)。

当配置 optionsDatasources 字段后,属性面板会多出一个 “选项类型” 的下拉菜单,用户让用户选择自定义选项,还是使用数据源。

1、下载代码

Terminal window
git clone https://gitee.com/fuhai/bsFormBuilder.git

2、安装依赖

Terminal window
npm install

PS:在安装依赖的过程中,可能会出现网络错误,请配置好网络环境,或者多安装几次…

3、构建编译生成 dist 文件

Terminal window
npm run build