Angular集成bpmn.js 基础实现及扩展
最近工作项目中有用到bpmn做一个前端工作流编辑器,零零碎碎用了比较多的知识点,空闲下来了整理一下大致包含了bpmn的简单引入、添加属性面板、汉化、添加自定义属性面板、节点修改属性、左侧工具栏图标的增加修改、contexpad的修改、流程图完整性的校验等。示例代码存放地址https://github.com/Tamy0816/rt-angular-bpmn.git
1、angular引入bpmn.js
安装bpmn.js依赖包
npm install bpmn-js --save
添加bpmn样式
在angular.json的styles中添加样式文件
"node_modules/bpmn-js/dist/assets/diagram-js.css",
"node_modules/bpmn-js/dist/assets/bpmn-font/css/bpmn.css",
实现编辑器组件
组件模板 app.component.ts
import { Component, ViewChild, ElementRef, OnInit } from '@angular/core';
import BpmnModeler from 'bpmn-js/lib/Modeler';
const defaultXML = `<?xml version="1.0" encoding="UTF-8"?>
<bpmn2:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"
xmlns:di="http://www.omg.org/spec/DD/20100524/DI"
xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL BPMN20.xsd" id="sample-diagram"
targetNamespace="http://bpmn.io/schema/bpmn">
<bpmn2:process id="Process_1" isExecutable="false">
<bpmn2:startEvent id="StartEvent_1"/>
</bpmn2:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds height="36.0" width="36.0" x="412.0" y="240.0"/>
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn2:definitions>`
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
public modeler: any;
ngOnInit() {
this.modeler = new BpmnModeler({
container: '#el',
});
this.modeler.importXML(defaultXML);
}
}
组件模板 app.component.html
<div id="el" style="width:100%; height: 1000px">
</div>
运行效果如图:
2、添加便捷工具
通过添加如下图小工具栏可以让画图更加便捷,可以撤销操作、放大缩小、下载xml格式文件或者图片文件
可以将小工具栏单独作为一个组件引入,设置样式绝对定位
添加EditingTools文件夹,文件结构如下:
editotTools.component.ts
import { Component, Output, EventEmitter } from 'node_modules/@angular/core';
@Component({
selector: 'app-editing-tools',
templateUrl: './editingTools.component.html',
styleUrls: ['./editingTools.component.css'],
})
export class EditingToolsComponent {
@Output() onUndo = new EventEmitter();
@Output() onRedo = new EventEmitter();
@Output() onZoomReset = new EventEmitter();
@Output() onZoomIn = new EventEmitter();
@Output() onZoomOut = new EventEmitter();
@Output() onSave = new EventEmitter();
@Output() onDownloadXml = new EventEmitter();
@Output() onDownloadSvg = new EventEmitter();
_onUndo() {
this.onUndo.emit();
}
_onRedo() {
this.onRedo.emit();
}
_onZoomReset() {
this.onZoomReset.emit();
}
_onZoomIn() {
this.onZoomIn.emit();
}
_onZoomOut() {
this.onZoomOut.emit();
}
_onSave() {
this.onSave.emit();
}
_onDownloadXml() {
this.onDownloadXml.emit();
}
_onDownloadSvg() {
this.onDownloadSvg.emit();
}
}
editorTools.component.html
<div class="editingTools">
<ul class="controlList">
<li class="control">
<button type="button" title="undo" (click)="_onUndo()">
<i class="undo" ></i>
</button>
</li>
<li class="control line">
<button type="button" title="redo" (click)="_onRedo()">
<i class="redo" ></i>
</button>
</li>
<li class="control">
<button type="button" title="reset zoom" (click)="_onZoomReset()">
<i class="zoom" ></i>
</button>
</li>
<li class="control">
<button type="button" title="zoom in" (click)="_onZoomIn()">
<i class="zoomIn" ></i>
</button>
</li>
<li class="control line">
<button type="button" title="zoom out" (click)="_onZoomOut()">
<i class="zoomOut" ></i>
</button>
</li>
<li class="control">
<button type="button" title="save" (click)="_onSave()">
<i class="save" ></i>
</button>
</li>
<li class="control">
<button type="button" title="download bpmn diagram" (click)="_onDownloadXml()">
<i class="download" ></i>
</button>
</li>
<li class="control">
<button type="button" title="download as svg image" (click)="_onDownloadSvg()">
<i class="image" ></i>
</button>
</li>
</ul>
</div>
父组件添加子组件模板 app.component.html
<div id="el" style="width:100%; height: 1000px">
</div>
<app-editing-tools (onDownloadSvg)=handleDownloadSvg() (onDownloadXml)=handleDownloadXml() (onRedo)=handleRedo()
(onSave)=handleSave() (onUndo)=handleUndo() (onZoomIn)=handleZoom(0.1)
(onZoomOut)=handleZoom(-0.1) (onZoomReset)=handleZoom()></app-editing-tools>
父组件实现对应方法 app.component.ts
/**
* 下载xml/svg
* @param type 类型 svg / xml
* @param data 数据
* @param name 文件名称
*/
download = (type, data, name) => {
let dataTrack = '';
const a = document.createElement('a');
switch (type) {
case 'xml':
dataTrack = 'bpmn';
break;
case 'svg':
dataTrack = 'svg';
break;
default:
break;
}
name = name || `diagram.${dataTrack}`;
a.setAttribute('href', `data:application/bpmn20-xml;charset=UTF-8,${encodeURIComponent(data)}`);
a.setAttribute('target', '_blank');
a.setAttribute('dataTrack', `diagram:download-${dataTrack}`);
a.setAttribute('download', name);
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
// 前进
handleRedo = () => {
this.modeler.get('commandStack').redo();
};
// 后退
handleUndo = () => {
this.modeler.get('commandStack').undo();
};
// 下载SVG格式
handleDownloadSvg = () => {
this.modeler.saveSVG({ format: true }, (err, data) => {
this.download('svg', data, 'svg');
});
};
// 下载XML格式
handleDownloadXml = () => {
this.modeler.saveXML({ format: true }, (err, data) => {
this.download('xml', data, 'xml');
});
};
// 流程图放大缩小
handleZoom = (radio) => {
const newScale = !radio
? 1.0 // 不输入radio则还原
: this.state.scale + radio <= 0.2 // 最小缩小倍数
? 0.2
: this.state.scale + radio;
this.modeler.get('canvas').zoom(newScale);
this.state.scale = newScale;
};
// 保存
handleSave() {
this.modeler.saveXML({format: true}, async (err, xml) => {
console.log(xml);
});
}
3、添加bpmn自带属性面板
bpmn自带的属性面板如下图
添加属性面板步骤:
添加bpmn-js-properties-panel组件
npm install bpmn-js-properties-panel --save
bpmn.js适配的是流程引擎Camunda,所以如果需要更加完整的属性输入框,也需安装camunda-bpmn-moddle,用于camunda数据对象
npm install camunda-bpmn-moddle --save
以上安装好之后在angular.json的styles中添加样式
"node_modules/bpmn-js-properties-panel/dist/assets/bpmn-js-properties-panel.css"
在app.component.html中添加属性面板展示区域
<app-editing-tools (onDownloadSvg)=handleDownloadSvg() (onDownloadXml)=handleDownloadXml() (onRedo)=handleRedo()
(onSave)=handleSave() (onUndo)=handleUndo() (onZoomIn)=handleZoom(0.1)
(onZoomOut)=handleZoom(-0.1) (onZoomReset)=handleZoom()></app-editing-tools>
<div class="row">
<div id="el" style="float:left;width:70%; height: 1000px">
</div>
<div id="js-properties-panel" style="float:right;width:25%; height: 1000px"></div>
</div>
在组件文件夹下新建CamundaModdleDescriptor.ts文件
,将node_modules\camunda-bpmn-moddle\resources\camunda.json文件内容复制粘贴过来,前面加上export CamundaModdleDescriptor = 供组件导入使用
在app.component.ts组件中引入CamundaModdleDescriptor.ts文件,在new BpmnModeler中添加moddleExtensions
运行后右侧属性面板 中即可显示Camunda流程引擎相关的内容了
4、bpmn.js页面汉化
官网示例:https://github.com/bpmn-io/bpmn-js-examples/tree/master/i18n/app,可以先clone代码到本地
再将示例中的customTranslate文件夹复制到项目文件夹下,然后在app.component.ts中引入,
import customTranslate from './customTranslate/customTranslate';
ngOnInit() {
const customTranslateModule = {
translate: ['value', customTranslate]
};
this.modeler = new BpmnModeler({
container: '#el',
propertiesPanel: {
parent: '#js-properties-panel'
},
additionalModules: [
propertiesProvider,
propertiesPanelModule,
customTranslateModule
],
moddleExtensions: {
camunda: CamundaModdleDescriptor
}
});
this.modeler.importXML(defaultXML);
}
再重启后就是汉化后的界面了
5、修改左侧工具栏(palatte)
在项目文件夹下面新建一个custom-palette文件夹,新建CustomPalette.js 和 index.js 文件, 可以参考node-modules下面的bpmn-js的palette如下图
文件内容如下:
CustomPalette.js这个文件里面的内容我们就可以根据自己需要添加或者删除一些工具图标,在官网地址https://www.npmjs.com/package/bpmn-font这个工程下面可以看到全部的bpmn-font,如下图:
CustomPalette.js代码如下:
import { assign } from 'min-dash';
/**
* A palette provider for BPMN 2.0 elements.
*/
export default function PaletteProvider(
palette, create, elementFactory,
spaceTool, lassoTool, handTool,
globalConnect, translate) {
this._palette = palette;
this._create = create;
this._elementFactory = elementFactory;
this._spaceTool = spaceTool;
this._lassoTool = lassoTool;
this._handTool = handTool;
this._globalConnect = globalConnect;
this._translate = translate;
palette.registerProvider(this);
}
PaletteProvider.$inject = [
'palette',
'create',
'elementFactory',
'spaceTool',
'lassoTool',
'handTool',
'globalConnect',
'translate'
];
PaletteProvider.prototype.getPaletteEntries = function(element) {
var actions = {},
create = this._create,
elementFactory = this._elementFactory,
spaceTool = this._spaceTool,
lassoTool = this._lassoTool,
handTool = this._handTool,
globalConnect = this._globalConnect,
translate = this._translate;
function createAction(type, group, className, title, options) {
function createListener(event) {
var shape = elementFactory.createShape(assign({ type: type }, options));
if (options) {
shape.businessObject.di.isExpanded = options.isExpanded;
}
create.start(event, shape);
}
var shortType = type.replace(/^bpmn:/, '');
return {
group: group,
className: className,
title: title || translate('Create {type}', { type: shortType }),
action: {
dragstart: createListener,
click: createListener
}
};
}
function createParticipant(event) {
create.start(event, elementFactory.createParticipantShape());
}
assign(actions, {
'hand-tool': {
group: 'tools',
className: 'bpmn-icon-hand-tool',
title: translate('拖拽'),
action: {
click: function(event) {
handTool.activateHand(event);
}
}
},
'lasso-tool': {
group: 'tools',
className: 'bpmn-icon-lasso-tool',
title: translate('选择'),
action: {
click: function(event) {
lassoTool.activateSelection(event);
}
}
},
// 删除不需要的工具
// 'space-tool': {
// group: 'tools',
// className: 'bpmn-icon-space-tool',
// title: translate('Activate the create/remove space tool'),
// action: {
// click: function(event) {
// spaceTool.activateSelection(event);
// }
// }
// },
'global-connect-tool': {
group: 'tools',
className: 'bpmn-icon-connection-multi',
title: translate('连接线'),
action: {
click: function(event) {
globalConnect.toggle(event);
}
}
},
'tool-separator': {
group: 'tools',
separator: true
},
'create.start-event': createAction(
'bpmn:StartEvent', 'event', 'bpmn-icon-start-event-none',
translate('开始节点')
),
'create.end-event': createAction(
'bpmn:EndEvent', 'event', 'bpmn-icon-end-event-none',
translate('结束节点')
),
'create.exclusive-gateway': createAction(
'bpmn:ExclusiveGateway', 'gateway', 'bpmn-icon-gateway-xor',
translate('网关')
),
// 添加工具图标
'create.userTask': createAction(
'bpmn:UserTask', 'activity', 'bpmn-icon-user-task',
translate('用户任务')
),
});
return actions;
};
index.js内容如下
import CustomPalette from './CustomPalette';
export default {
__init__: [
'paletteProvider'
],
paletteProvider: ['type', CustomPalette]
};
然后在app.component.ts中引入paletteProvider,代码如下:
import paletteProvider from './custom-palette';
ngOnInit() {
const customTranslateModule = {
translate: ['value', customTranslate]
};
this.modeler = new BpmnModeler({
container: '#el',
propertiesPanel: {
parent: '#js-properties-panel'
},
additionalModules: [
propertiesProvider,
propertiesPanelModule,
customTranslateModule,
paletteProvider
],
moddleExtensions: {
camunda: CamundaModdleDescriptor
}
});
this.modeler.importXML(defaultXML);
}
去掉了一些不需要的工具图标之后我们的页面如下图,页面更加简约清爽了
6、修改context-pad
context-pad是我们在点击节点的时候弹出的一个方便选择下一节点的工具,当我们按照自己的需求修改了左侧palette工具栏之后,我们可能也会想一并修改这个context-pad的显示内容
修改方式与palette的修改方式大同小异,现在项目工程下文件夹下新建context-pad文件夹,添加ContextPadProvider.js、index.js文件,参考node-modules中的bpmn-js下面的context-pad文件夹,可以完全拷贝过来,
ContextPadProvider.js中顶部import的路径需要修改成绝对路径
import {
assign,
forEach,
isArray
} from 'min-dash';
import {
is
} from 'bpmn-js/lib/util/ModelUtil';
import {
isExpanded,
isEventSubProcess
} from 'bpmn-js/lib/util/DiUtil';
import {
isAny
} from 'bpmn-js/lib/features/modeling/util/ModelingUtil';
import {
getChildLanes
} from 'bpmn-js/lib/features/modeling/util/LaneUtil';
import {
hasPrimaryModifier
} from 'diagram-js/lib/util/Mouse';
/**
* A provider for BPMN 2.0 elements context pad
*/
export default function ContextPadProvider(
config, injector, eventBus,
contextPad, modeling, elementFactory,
connect, create, popupMenu,
canvas, rules, translate) {
config = config || {};
contextPad.registerProvider(this);
this._contextPad = contextPad;
this._modeling = modeling;
this._elementFactory = elementFactory;
this._connect = connect;
this._create = create;
this._popupMenu = popupMenu;
this._canvas = canvas;
this._rules = rules;
this._translate = translate;
if (config.autoPlace !== false) {
this._autoPlace = injector.get('autoPlace', false);
}
eventBus.on('create.end', 250, function(event) {
var shape = event.context.shape;
if (!hasPrimaryModifier(event)) {
return;
}
var entries = contextPad.getEntries(shape);
if (entries.replace) {
entries.replace.action.click(event, shape);
}
});
}
ContextPadProvider.$inject = [
'config.contextPad',
'injector',
'eventBus',
'contextPad',
'modeling',
'elementFactory',
'connect',
'create',
'popupMenu',
'canvas',
'rules',
'translate'
];
ContextPadProvider.prototype.getContextPadEntries = function(element) {
var contextPad = this._contextPad,
modeling = this._modeling,
elementFactory = this._elementFactory,
connect = this._connect,
create = this._create,
popupMenu = this._popupMenu,
canvas = this._canvas,
rules = this._rules,
autoPlace = this._autoPlace,
translate = this._translate;
var actions = {};
if (element.type === 'label') {
return actions;
}
var businessObject = element.businessObject;
function startConnect(event, element) {
connect.start(event, element);
}
function removeElement(e) {
modeling.removeElements([ element ]);
}
function getReplaceMenuPosition(element) {
var Y_OFFSET = 5;
var diagramContainer = canvas.getContainer(),
pad = contextPad.getPad(element).html;
var diagramRect = diagramContainer.getBoundingClientRect(),
padRect = pad.getBoundingClientRect();
var top = padRect.top - diagramRect.top;
var left = padRect.left - diagramRect.left;
var pos = {
x: left,
y: top + padRect.height + Y_OFFSET
};
return pos;
}
/**
* Create an append action
*
* @param {String} type
* @param {String} className
* @param {String} [title]
* @param {Object} [options]
*
* @return {Object} descriptor
*/
function appendAction(type, className, title, options) {
if (typeof title !== 'string') {
options = title;
title = translate('Append {type}', { type: type.replace(/^bpmn:/, '') });
}
function appendStart(event, element) {
var shape = elementFactory.createShape(assign({ type: type }, options));
create.start(event, shape, {
source: element
});
}
var append = autoPlace ? function(event, element) {
var shape = elementFactory.createShape(assign({ type: type }, options));
autoPlace.append(element, shape);
} : appendStart;
return {
group: 'model',
className: className,
title: title,
action: {
dragstart: appendStart,
click: append
}
};
}
function splitLaneHandler(count) {
return function(event, element) {
// actual split
modeling.splitLane(element, count);
// refresh context pad after split to
// get rid of split icons
contextPad.open(element, true);
};
}
if (isAny(businessObject, [ 'bpmn:Lane', 'bpmn:Participant' ]) && isExpanded(businessObject)) {
var childLanes = getChildLanes(element);
assign(actions, {
'lane-insert-above': {
group: 'lane-insert-above',
className: 'bpmn-icon-lane-insert-above',
title: translate('Add Lane above'),
action: {
click: function(event, element) {
modeling.addLane(element, 'top');
}
}
}
});
if (childLanes.length < 2) {
if (element.height >= 120) {
assign(actions, {
'lane-divide-two': {
group: 'lane-divide',
className: 'bpmn-icon-lane-divide-two',
title: translate('Divide into two Lanes'),
action: {
click: splitLaneHandler(2)
}
}
});
}
if (element.height >= 180) {
assign(actions, {
'lane-divide-three': {
group: 'lane-divide',
className: 'bpmn-icon-lane-divide-three',
title: translate('Divide into three Lanes'),
action: {
click: splitLaneHandler(3)
}
}
});
}
}
assign(actions, {
'lane-insert-below': {
group: 'lane-insert-below',
className: 'bpmn-icon-lane-insert-below',
title: translate('Add Lane below'),
action: {
click: function(event, element) {
modeling.addLane(element, 'bottom');
}
}
}
});
}
if (is(businessObject, 'bpmn:FlowNode')) {
if (is(businessObject, 'bpmn:EventBasedGateway')) {
assign(actions, {
'append.receive-task': appendAction(
'bpmn:ReceiveTask',
'bpmn-icon-receive-task'
),
'append.message-intermediate-event': appendAction(
'bpmn:IntermediateCatchEvent',
'bpmn-icon-intermediate-event-catch-message',
translate('Append MessageIntermediateCatchEvent'),
{ eventDefinitionType: 'bpmn:MessageEventDefinition' }
),
'append.timer-intermediate-event': appendAction(
'bpmn:IntermediateCatchEvent',
'bpmn-icon-intermediate-event-catch-timer',
translate('Append TimerIntermediateCatchEvent'),
{ eventDefinitionType: 'bpmn:TimerEventDefinition' }
),
'append.condition-intermediate-event': appendAction(
'bpmn:IntermediateCatchEvent',
'bpmn-icon-intermediate-event-catch-condition',
translate('Append ConditionIntermediateCatchEvent'),
{ eventDefinitionType: 'bpmn:ConditionalEventDefinition' }
),
'append.signal-intermediate-event': appendAction(
'bpmn:IntermediateCatchEvent',
'bpmn-icon-intermediate-event-catch-signal',
translate('Append SignalIntermediateCatchEvent'),
{ eventDefinitionType: 'bpmn:SignalEventDefinition' }
)
});
} else
if (isEventType(businessObject, 'bpmn:BoundaryEvent', 'bpmn:CompensateEventDefinition')) {
assign(actions, {
'append.compensation-activity':
appendAction(
'bpmn:Task',
'bpmn-icon-task',
translate('Append compensation activity'),
{
isForCompensation: true
}
)
});
} else
if (!is(businessObject, 'bpmn:EndEvent') &&
!businessObject.isForCompensation &&
!isEventType(businessObject, 'bpmn:IntermediateThrowEvent', 'bpmn:LinkEventDefinition') &&
!isEventSubProcess(businessObject)) {
assign(actions, {
'append.end-event': appendAction(
'bpmn:EndEvent',
'bpmn-icon-end-event-none',
translate('Append EndEvent')
),
// 根据项目需要添加了userTask内容
'append.append-user-task': appendAction(
'bpmn:UserTask',
'bpmn-icon-user-task',
translate('Append UserTask')
),
'append.gateway': appendAction(
'bpmn:ExclusiveGateway',
'bpmn-icon-gateway-xor',
translate('Append Gateway')
),
// 删除了一些不需要的内容
// 'append.append-task': appendAction(
// 'bpmn:Task',
// 'bpmn-icon-task',
// translate('Append Task')
// ),
// 'append.intermediate-event': appendAction(
// 'bpmn:IntermediateThrowEvent',
// 'bpmn-icon-intermediate-event-none',
// translate('Append Intermediate/Boundary Event')
// )
});
}
}
if (!popupMenu.isEmpty(element, 'bpmn-replace')) {
// Replace menu entry
assign(actions, {
'replace': {
group: 'edit',
className: 'bpmn-icon-screw-wrench',
title: translate('Change type'),
action: {
click: function(event, element) {
var position = assign(getReplaceMenuPosition(element), {
cursor: { x: event.x, y: event.y }
});
popupMenu.open(element, 'bpmn-replace', position);
}
}
}
});
}
if (isAny(businessObject, [
'bpmn:FlowNode',
'bpmn:InteractionNode',
'bpmn:DataObjectReference',
'bpmn:DataStoreReference'
])) {
// 删除了一些不需要的内容
// assign(actions, {
// 'append.text-annotation': appendAction('bpmn:TextAnnotation', 'bpmn-icon-text-annotation'),
// 'connect': {
// group: 'connect',
// className: 'bpmn-icon-connection-multi',
// title: translate('Connect using ' +
// (businessObject.isForCompensation ? '' : 'Sequence/MessageFlow or ') +
// 'Association'),
// action: {
// click: startConnect,
// dragstart: startConnect
// }
// }
// });
assign(actions, {
'connect': {
group: 'connect',
className: 'bpmn-icon-connection-multi',
title: translate('Connect using DataInputAssociation'),
action: {
click: startConnect,
dragstart: startConnect
}
}
});
}
if (isAny(businessObject, [ 'bpmn:DataObjectReference', 'bpmn:DataStoreReference' ])) {
assign(actions, {
'connect': {
group: 'connect',
className: 'bpmn-icon-connection-multi',
title: translate('Connect using DataInputAssociation'),
action: {
click: startConnect,
dragstart: startConnect
}
}
});
}
// delete element entry, only show if allowed by rules
var deleteAllowed = rules.allowed('elements.delete', { elements: [ element ] });
if (isArray(deleteAllowed)) {
// was the element returned as a deletion candidate?
deleteAllowed = deleteAllowed[0] === element;
}
if (deleteAllowed) {
assign(actions, {
'delete': {
group: 'edit',
className: 'bpmn-icon-trash',
title: translate('Remove'),
action: {
click: removeElement
}
}
});
}
return actions;
};
function isEventType(eventBo, type, definition) {
var isType = eventBo.$instanceOf(type);
var isDefinition = false;
var definitions = eventBo.eventDefinitions || [];
forEach(definitions, function(def) {
if (def.$type === definition) {
isDefinition = true;
}
});
return isType && isDefinition;
}
然后就可以在app.component.ts中引入代码如下:
import contextPadProvider from './context-pad';
修改后的context-pad如下图,添加了userTask控件,删掉了一些不需要的
7、节点添加自定义属性
有些时候根据项目需求可能需要节点额外再添加一些属性,我们可以根据bpmn自带的updateProperties方法做到添加节点属性
先在页面添加一个区域添加一些表单,这里例子只写一个select下拉框了,给select添加change事件,在change事件中将选择的值更新到节点的属性上面去
app.component.html 代码如下
实现效果如下:
app.component.ts 实现selected方法,主要用到了updateProperties方法,我们可以用这个方法任意添加节点属性
selected(value) {
const modeling = this.modeler.get('modeling');
modeling.updateProperties(this.element, {
'custom-property': value
});
}
我们可以在保存打印的xml中看到custom-property已经被添加上去了
8、流程图完整性的校验
我们可以在流程图提交的时候,校验xml文件,发现一些比较基础的绘制错误告诉给用户,比如流程图没有开始或者结束节点,先就举这个例子,其它可以参考项目需要进行添加。我是用的将xml文件转为json格式做的校验。
先安装插件 xml2js,要使用xml2js,还需安装timers
npm install xml2js
npm install timers
在保存时添加如下方法做校验,当流程图缺少开始节点或者结束节点的时候抛错:
handleSave() {
console.log('save');
this.modeler.saveXML({ format: true }, async (err, xml) => {
console.log(xml);
const parseString = xml2js.parseString;
parseString(xml, (err, result) => {
const bpmnData = result['bpmn2:definitions']['bpmn2:process'][0];
if (!bpmnData['bpmn2:startEvent'] || !bpmnData['bpmn2:endEvent']) {
alert('一个流程图必须有一个开始和结束节点');
}
});
});
}