
# 1 用户场景
业务场景越来越多,开发人员不满足于自定义控件写一些静态组件或者页面,因此自定义控件需要获取后端返回来的数据,并且能够实现前后端交互全流程。
# 2 功能介绍
使用`java`插件或者`KDE`脚本插件,给自定义控件传输业务数据,`java`插件的写法和`KDE`脚本大同小异。该例子会使用`KDE`脚本插件
# 3 控件对象
设计时对象
```java
kd.bos.ext.metadata.form.control.CustomControlAp
```
运行时对象
```java
kd.bos.ext.form.control.CustomControl
```
动态创建自定义控件时,配置项和控件方案只能在设计时配置,运行时自定义控件可以在插件里调用setData方法给自定义控件传输业务数据。
# 4 操作步骤
## 4.1 插件赋值
### 4.1.1 新建KS脚本
* 开发平台找到对应自定义控件单据所在的**应用**,点击打开`KDE`编辑器

* 在编辑器中找到对应的单据页面,鼠标右键选择菜单"**新建插件脚本**"
* 这里我们将新建的插件脚本文件命名为"**sendganttdata.ks**"

### 4.1.2 开发KS脚本
将下面代码复制到上面创建的插件脚本中,并修改对应的值
```js
var plugin = new FormPlugin({
afterBindData : function(e){
// getControl中传入的是自定义控件的标识(key),可在设计器里面查看,并不是方案id(schemaId)
var customcontrol = this.getView().getControl('helloworld');
// 这里是你要给前端发送的数据,
var data = {
text: '你好,自定义控件'
}
//调用该自定义控件的setData方法赋值
customcontrol.setData(data)
}
});
```

### 4.1.3 检查返回数据
返回"**设计器**"里面的对应单据页面,点击"**预览**"按钮查看页面
确认新建的插件已经"启用",可通过单据页面的"**插件**"属性查看是否已启用


上图可以看到"Network"面板里面就是刚才在`sendganttdata.ks`里面定义返回来的数据
### 4.1.4 控件获取数据
修改自定义控件`index.js`里的`initFunc`函数,可以从`props`中获取得到后端传过来数据`text`,示例代码如下:
```js
var initFunc = function(model, props) {
// KDApi.loadFile可以通过路径加载js或css文件,并且在html文件头生成script或者link标签,第一个参数是路径,第二个参数是model,第三个参数是加载完成后执行的回调函数
KDApi.loadFile('./css/helloworld.css', model, function() {
// 通过路径去获取html字符串,第一个参数是路径,第二个参数是model,第三个参数是HTML模板中变量的值
var text = props.data && props.data.text || []
KDApi.getTemplateStringByFilePath('./html/helloworld.html', model, {
text: text
}).then(function(result) {
model.dom.innerHTML = result
})
})
}
```

通过上面例子,我们完成了自定义控件插件赋值的方法。
但是在上面的例子中,自定义控件可以获取后端返回来的数据,解决了纯静态类的自定义控件需求,但是如果自定义控件更加复杂,需要有点击事件,编辑等需求,自定义控件要和后端插件实现主动通信。
因此,自定义控件和插件需要如何编写?接下来为你介绍。
## 4.2 前后端交互
在这一小节里,会通过前端的点击事件给后端发请求,后端插件接收到这个请求后弹出一个提示框,这样即可完成前后端完整的交互。
### 4.2.1 前端发送请求
* 在`index.js`里,声明`initEvent`函数,监听点击事件,并执行`model.invoke`方法给后端发送请求,代码示例如下:
```js
var initEvent = function(model, props){
//内置了jquery对象,可直接使用$
$('.helloworld', model.dom).click(function(){
// model.invoke,用于给后端发送请求,第一个参数是事件名,可自定义;第二个参数是发送给后端的数据,可以是任意类型
model.invoke('click', 'Hello World!')
})
}
```
### 4.2.2 DOM绑定事件
* 在`index.js`里的`initFunc`函数中,通过`initEvent`给`DOM`绑定操作事件
* PC端支持使用jQuery库绑定事件,initEvent方法实现请参考4.2.1章节
```js
var initFunc = function(model, props) {
// KDApi.loadFile可以通过路径加载js或css文件,并且在html文件头生成script或者link标签,第一个参数是路径,第二个参数是model,第三个参数是加载完成后执行的回调函数
KDApi.loadFile('./css/helloworld.css', model, function() {
// 后端插件通过setData传给前端的数据,前端可以通过props.data去获取
var text = props.data && props.data.text || ''
// 通过路径去获取html字符串,第一个参数是路径,第二个参数是model,第三个参数是HTML模板中变量的值
KDApi.getTemplateStringByFilePath('./html/helloworld.html', model, {
text: text
}).then(function(result) {
model.dom.innerHTML = result
// 绑定DOM事件
initEvent(model, props)
})
})
}
```
### 4.2.3 后端处理请求
* 在脚本插件的`customEvent`方法里(即插件中重写customEvent方法),处理前端发送过来的请求
```js
var plugin = new FormPlugin({
afterBindData : function(e){
// getControl中传入的是控件的标识(key)不是方案id(schemaId)
var customcontrol = this.getView().getControl('helloworld');
var data = {
text: '你好,自定义控件'
}
customcontrol.setData(data)
},
customEvent: function(e) {
// 设计器上自定义控件的标识
var key = e.getKey()
// 前端通过model.invoke传给后端的数据
var args = e.getEventArgs()
// 前端通过model.invoke定义的事件名
var eventName = e.getEventName()
this.getView().showMessage('key: ' + key + ';eventName: ' + eventName + ';args: ' + args)
}
});
```
### 4.2.4 保存预览
保存脚本后,在设计器中点击"**预览**"按钮,效果如下:

至此,一个完整的前后端交互的例子完成了!
# 5 代码示例
## 5.1 前端示例
自定义控件默认是集成了jquery,简单控件可直接使用其进行开发即可,如果你希望通过使用其他框架来实现如React、Vue等,也是支持的,以下将以热门框架React、Vue为例,在例子中使用前端打包工具webpack,开发完后,编译,将控件目录下的lang文件夹、css文件夹、index.js、index.js.map打成zip包上传即可。
### 5.1.1 vue
在此例子中打包入口为main.js,输出index.js和index.css,所以之前在index.js里写的代码可以挪到main.js中
```js
// 在main.js上通过import引入Vue库和自己写的Vue组件库
import Vue from 'vue'
import Steps from './components/steps.vue'
import eventBus from '../../../../../../util/eventBus'
/**
* Vue实例在setHtml方法中声明,初始化执行init的时候就能够创建
* 声明Vue实例对象时,在其挂载完毕的生命周期里声明一个订阅,用于订阅update方法中发布的消息,从而更新实例数据
* update方法中,发布一个消息,让Vue实例接收消息,更新数据
* 在Vue实例的destroyed中,结束订阅
* 注意loadFile中index.css的引入路径,因为webpack打包后将其放在了css文件夹里,所以路径是./css/index.css
*/
(function (KDApi) {
function MyComponent (model) {
this._setModel(model)
}
MyComponent.prototype = {
_setModel: function (model) {
this.model = model
},
init: function (props) {
console.log('-----init', this.model.style, props)
setHtml(this.model, props)
},
update: function (props) {
console.log('-----update', this.model, props)
eventBus.pub(this.model, 'update', props)
},
destoryed: function () {
console.log('-----destoryed', this.model)
}
}
const setHtml = (model, props) => {
KDApi.loadFile('./css/index.css', model, () => {
const { invoke, dom } = model
const { data } = props
const activeI = data ? data.avtiveIndex : -1
new Vue({
el: dom,
template: '<Steps :invoke="invoke" :activeInd="activeIndex" :model="model" :getLangMsg="getLangMsg" />',
components: {
Steps
},
data: {
activeIndex: activeI,
model: model
},
mounted () {
const self = this
this.updateSub = eventBus.sub(model, 'update', (props) => {
const { data } = props
self.activeIndex = data ? data.activeIndex : -1
})
},
destroyed () {
eventBus.unsub(this.updateSub)
},
methods: {
invoke: (eventName, args) => {
invoke(eventName, args)
},
getLangMsg: (key) => {
return KDApi.getLangMsg(model, key)
}
}
})
})
}
// 注册自定义组件
KDApi.register('stepsVue', MyComponent, {
isMulLang: true
})
})(window.KDApi)
```
### 5.1.2 react
在此例子中打包入口为main.js,输出index.js和index.css,所以之前在index.js里写的代码可以挪到main.js中
```js
import React from 'react'
import ReactDOM from 'react-dom'
import TodoList from './components/TodoList'
import eventBus from '../../../../../../util/eventBus'
/**
* 在setHtml中声明Root类,使用ReactDOM.render将其渲染在model.dom中
* 在Root类的componentDidMount里,声明一个订阅,用于接收后端更新发过来的消息,从而去更新组件
* 在update里,发布一个消息,当后端插件给自定义控件传递新数据时,就能将消息发布给Root
* 在Root类的componentWillUnmount里,取消订阅
* 在destoryed里,使用ReactDOM.unmountComponentAtNode卸载Root
* 注意loadFile中index.css的引入路径,因为webpack打包后将其放在了css文件夹里,所以路径是./css/index.css
*/
(function (KDApi) {
function MyComponent (model) {
this._setModel(model)
}
MyComponent.prototype = {
_setModel: function (model) {
this.model = model
},
init: function (props) {
console.log('-----init', this.model.style, props)
setHtml(this.model, props)
},
update: function (props) {
console.log('-----update', this.model, props)
eventBus.pub(this.model, 'update', props)
},
destoryed: function () {
console.log('-----destoryed', this.model)
ReactDOM.unmountComponentAtNode(this.model.dom)
}
}
var setHtml = function (model, primaryProps) {
KDApi.loadFile('./css/index.css', model, () => {
class Root extends React.Component {
constructor(props) {
super(props)
this.state = {
customProps: props.customProps,
model: props.model
}
}
componentDidMount () {
const { model } = this.state
this.updateSub = eventBus.sub(model, 'update', (updateProps) => {
this.setState({ customProps: updateProps})
})
}
shouldComponentUpdate () {
}
componentWillUnmount () {
eventBus.unsub(this.updateSub)
}
render () {
const { customProps, model } = this.state
return (
<TodoList model={model} customProps={customProps} />
)
}
}
ReactDOM.render(<Root model={model} customProps={primaryProps} />, model.dom)
})
}
// 注册自定义组件
KDApi.register('todolistreact', MyComponent)
})(window.KDApi)
```
### 5.1.3 jquery
实现一个控件方案id为"dellabel"的自定义控件
```
// index.js
(function (KDApi, $) {
// 构造函数
function MyComponent (model) {
this._setModel(model)
}
var themeColor
MyComponent.prototype = {
// 绑定model
_setModel: function (model) {
this.model = model
},
// 生命周期:初始化
init: function (props) {
console.log('-----init', this.model, pro