preface

ο£Ώ

Q: What is bPMn.js? πŸ€” ️

Bpmn. js is a BPMN2.0 rendering toolkit and Web modeler that allows you to draw flowcharts in the front end.

ο£Ώ

Q: Why did I write this series? πŸ€” ️

Due to the business needs of the company, bPMN. js is used in the project. However, as the developer of BPMN. js is a foreign friend, there are few textbooks and no detailed documents on this aspect in China. So a lot of ways to use a lot of pits have to find their own. After mulling it over, I decided to write a series of textbooks on it to help more bPMn.js users or developers who want to find a good way to draw flowcharts. It is also a kind of consolidation of their own.

Because it is a series of articles, so the update may be more frequent, if you accidentally brush and not what you need also please understand 😊.

Don’t ask for praise πŸ‘ don’t ask for heart ❀️. I only hope to have a little help for you.

Package Component

Before going into this section, I want you to know the previous sections: Customize palette, Customize Renderer, Customize contextPad, edit delete node.

This section will summarize the previous sections and provide a usable BPMN component solution.

By reading you can learn:

  • Create a line node
  • Custom modeler
  • Encapsulate BPMN as a component

Create a line node

First let’s look at how a offline node is created.

I used CustomPalette. Js as an example 🌰, remember createTask from earlier, creating a line is similar to this:

// CustomPalette.js
PaletteProvider.$inject = [
    ...
    'globalConnect'
]
PaletteProvider.prototype.getPaletteEntries = function(element) {
    const { globalConnect } = this
    
    function createConnect () {
        return {
          group: 'tools'.className: 'icon-custom icon-custom-flow'.title: 'New line'.action: {
            click: function (event) {
              globalConnect.toggle(event)
            }
          }
        }
    }
    
    return {
        'create.lindaidai-task': {... },'global-connect-tool': createConnect()
    }
}
Copy the code

So I can draw a line:

bpmnModeler.png

Custom modeler

After the example above, it is not hard to see that in each key function, the desired customization is passed through the function return value.

And the return values are pretty much the same, like group, className, etc., so can we integrate them and save a lot of code?

We can build a function like this:

// CustomPalette.js
function createAction (type, group, className, title, options) {
    function createListener (event) {
      var shape = elementFactory.createShape(assign({ type }, options))
      create.start(event, shape)
    }

    return {
      group,
      className,
      title: 'new' + title,
      action: {
        dragstart: createListener,
        click: createListener
      }
    }
}
Copy the code

It accepts the different attributes of all elements and returns a custom element.

But the line creation might be a little different:

// CustomPalette.js
function createConnect (type, group, className, title, options) {
   return {
     group,
     className,
     title: 'new' + title,
     action: {
       click: function (event) {
         globalConnect.toggle(event)
       }
     }
   }
 }
Copy the code

So I’ve divided the functions that create elements into two categories: createAction and createConnect.

Next we just need to build an array like this:

// utils/util.js
const flowAction = { / / line
   type: 'global-connect-tool'.action: ['bpmn:SequenceFlow'.'tools'.'icon-custom icon-custom-flow'.'Connection']}const customShapeAction = [ // shape
   {
       type: 'create.start-event'.action: ['bpmn:StartEvent'.'event'.'icon-custom icon-custom-start'.'Start node'] {},type: 'create.end-event'.action: ['bpmn:EndEvent'.'event'.'icon-custom icon-custom-end'.'End node'] {},type: 'create.task'.action: ['bpmn:Task'.'activity'.'icon-custom icon-custom-task'.'Normal mission'] {},type: 'create.businessRule-task'.action: ['bpmn:BusinessRuleTask'.'activity'.'icon-custom icon-custom-businessRule'.'businessRule task'] {},type: 'create.exclusive-gateway'.action: ['bpmn:ExclusiveGateway'.'activity'.'icon-custom icon-custom-exclusive-gateway'.'gateway'] {},type: 'create.dataObjectReference'.action: ['bpmn:DataObjectReference'.'activity'.'icon-custom icon-custom-data'.'variables']}]const customFlowAction = [
   flowAction
]

export { customShapeAction, customFlowAction }
Copy the code

Also build a method to loop through the elements above πŸ‘† :

// utils/util.js
/** * loop creates a series of elements *@param {Array} Actions element collection *@param {Object} Fn handles the function */
export function batchCreateCustom(actions, fn) {
   const customs = {}
   actions.forEach(item= > {
       customs[item['type']] = fn(... item['action'])})return customs
}
Copy the code

writeCustomPalette.jscode

You can then reference them in CustomPalette. Js:

// CustomPalette.js
import { customShapeAction, customFlowAction, batchCreateCustom } from '. /.. /.. /utils/util'
PaletteProvider.prototype.getPaletteEntries = function(element) {
   var actions = {}
   const {
       create,
       elementFactory,
       globalConnect
   } = this;

   function createConnect(type, group, className, title, options) {
       return {
           group,
           className,
           title: 'new' + title,
           action: {
               click: function(event) {
                   globalConnect.toggle(event)
               }
           }
       }
   }

   function createAction(type, group, className, title, options) {
       function createListener(event) {
           var shape = elementFactory.createShape(Object.assign({ type }, options))
           create.start(event, shape)
       }

       return {
           group,
           className,
           title: 'new' + title,
           action: {
               dragstart: createListener,
               click: createListener
           }
       }
   }
   Object.assign(actions, { ... batchCreateCustom(customFlowAction, createConnect),/ / line. batchCreateCustom(customShapeAction, createAction) })return actions
}
Copy the code

It seems that the code is not a lot of simplification 😊.

Let’s see what the page looks like:

bpmnModeler2.png

The toolbar on the left has been replaced with the image we want.

writeCustomRenderer.jscode

Then it’s time to code the Renderer. Before we do that, again, we can do some configuration items.

Because we notice that the svgCreate(‘image’, {}) method is used to render custom elements.

Inside it is also the address url and style configuration attR of an image received.

Then we can extract the url prefix:

 // utils/util.js
const STATICPATH = 'https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/' // Static file path
const customConfig = { // Customize the configuration of elements
    'bpmn:StartEvent': {
        'field': 'start'.'title': 'Start node'.'attr': { x: 0.y: 0.width: 40.height: 40}},'bpmn:EndEvent': {
        'field': 'end'.'title': 'End node'.'attr': { x: 0.y: 0.width: 40.height: 40}},'bpmn:SequenceFlow': {
        'field': 'flow'.'title': 'Connection',},'bpmn:Task': {
        'field': 'rules'.'title': 'Normal mission'.'attr': { x: 0.y: 0.width: 48.height: 48}},'bpmn:BusinessRuleTask': {
        'field': 'variable'.'title': 'businessRule task'.'attr': { x: 0.y: 0.width: 48.height: 48}},'bpmn:ExclusiveGateway': {
        'field': 'decision'.'title': 'gateway'.'attr': { x: 0.y: 0.width: 48.height: 48}},'bpmn:DataObjectReference': {
        'field': 'score'.'title': 'variables'.'attr': { x: 0.y: 0.width: 48.height: 48}}}const hasLabelElements = ['bpmn:StartEvent'.'bpmn:EndEvent'.'bpmn:ExclusiveGateway'.'bpmn:DataObjectReference'] // Start with the element type of the label

export { STATICPATH, customConfig, hasLabelElements }
Copy the code

Then you just need to check when writing drawShape:

// CustomRenderer.js
import inherits from 'inherits'
import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer'
import {
    append as svgAppend,
    create as svgCreate
} from 'tiny-svg'
import { customElements, customConfig, STATICPATH, hasLabelElements } from '.. /.. /utils/util'
/** * A renderer that knows how to render custom elements. */
export default function CustomRenderer(eventBus, styles, bpmnRenderer) {
    BaseRenderer.call(this, eventBus, 2000)
    var computeStyle = styles.computeStyle

    this.drawElements = function(parentNode, element) {
        console.log(element)
        const type = element.type // Get the type
        if(type ! = ='label') {
            if (customElements.includes(type)) { // or customConfig[type]
                return drawCustomElements(parentNode, element)
            }
            const shape = bpmnRenderer.drawShape(parentNode, element)
            return shape
        } else {
            element
        }
    }
}

inherits(CustomRenderer, BaseRenderer)

CustomRenderer.$inject = ['eventBus'.'styles'.'bpmnRenderer']

CustomRenderer.prototype.canRender = function(element) {
    // ignore labels
    return true
        // return ! element.labelTarget;
}

CustomRenderer.prototype.drawShape = function(parentNode, element) {
    return this.drawElements(parentNode, element)
}

CustomRenderer.prototype.getShapePath = function(shape) {
    // console.log(shape)
}

function drawCustomElements(parentNode, element) {
    const { type } = element
    const { field, attr } = customConfig[type]
    const url = `${STATICPATH}${field}.png`
    const customIcon = svgCreate('image', {
        ...attr,
        href: url
    })
    element['width'] = attr.width // I changed the width and height of the element directly
    element['height'] = attr.height
    svgAppend(parentNode, customIcon)
        // Determine if there is a name attribute to render the label
    if(! hasLabelElements.includes(type) && element.businessObject.name) {const text = svgCreate('text', {
            x: attr.x,
            y: attr.y + attr.height + 20."font-size": "14"."fill": "# 000"
        })
        text.innerHTML = element.businessObject.name
        svgAppend(parentNode, text)
    }
    return customIcon
}
Copy the code

The key is that in the drawCustomElements function, a string concatenation of the URL is utilized.

This way, the custom elements can be rendered.

The effect is as follows:

bpmnModeler3.png

writeCustomContextProvider.jscode

Now that the Palette and renderer are ready, let’s see how the contextPad is written.

It is written similar to the palette, with one caveat:

Different types of nodes appearcontextPadThe content may be different.

Such as:

  • StartEventThere will be aEdit, Delete, Task, BusinessRuleTask, ExclusiveGatewayAnd so on;
  • EndEventCan only appearEdit, delete;
  • SequenceFlowCan only appearEdit, delete.

That is, we need to return a different contextPad depending on the node type.

When writing the return value of the getContextPadEntries function, it is possible to return different results based on element.type:

import { isAny } from 'bpmn-js/lib/features/modeling/util/ModelingUtil'
ContextPadProvider.prototype.getContextPadEntries = function(element) {...// The code omitted here can be seen in the project github source code
    
    // An element that can only be generated by clicking on an element in the list
    if (isAny(businessObject, ['bpmn:StartEvent'.'bpmn:Task'.'bpmn:BusinessRuleTask'.'bpmn:ExclusiveGateway'.'bpmn:DataObjectReference']) {Object.assign(actions, { ... batchCreateCustom(customShapeAction, createAction), ... batchCreateCustom(customFlowAction, createConnect),/ / cable
            'edit': editElement(),
            'delete': deleteElement()
        })
    }
    // End node and line only delete and edit
    if (isAny(businessObject, ['bpmn:EndEvent'.'bpmn:SequenceFlow'.'bpmn:DataOutputAssociation']) {Object.assign(actions, {
            'edit': editElement(),
            'delete': deleteElement()
        })
    }
    return actions
}
Copy the code

IsAny is used to determine whether a type belongs to a later array, similar to includes.

Thus our contextPad is enriched 😊.

bomnModeler4.png

Encapsulate BPMN as a component

With the foundation of custom Modeler, we can encapsulate BPMN as a component that we can reference where we need to apply it.

For a better demonstration, I created a new project, bPMN-custom-Modeler, which has the same dependencies and configuration as bPMN-vue – Custom, but in this new project I plan to use custom Modeler to override the original. And encapsulate a BPMN component for the page to use.

preparation

Under the components folder of your project, create a new folder called BPMN, which will hold the packaged BPMN components.

Then we can also prepare an empty XML as the default display in the component (that is, what should be displayed if there are no graphics coming in). Here I define a newDiagram. Js.

Create a view file in the root directory to hold some page files. Here I will create a custom-modeler. Vue to reference the wrapped BPMN component to see the effect.

The component’sprops

If we want to encapsulate this as a component, we need to pass props to this component. It can be an entire XML string or the address of a BPMN file.

I use passing in the BPMN file address as an example for encapsulation. Of course, you can decide according to your business needs.

That is, when referencing this component, I expect it to say something like:

/* views/custom-modeler.vue */
<template>
    <bpmn :xmlUrl="xmlUrl" @change="changeBpmn"></bpmn>
</template>

<script>
import { Bpmn } from '. /.. /components/bpmn'
export default {
    components: {
        Bpmn
    },
    data () {
      return {
        xmlUrl: 'https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmnMock.bpmn'}},methods: {
        changeBpmn ($event) {}
    }
}
</script>
Copy the code

As long as you reference the BPMN component and then pass a URL, the corresponding graphic content can be displayed on the page.

So, in bPMN. vue, we should define props like this:

// Bpmn.vue
props: {
    xmlUrl: {
      type: String.default: ' '}}Copy the code

Authoring componenthmtlcode

The HTML code in the component is very easy, basically giving the canvas a container to hold and defining two buttons for downloading:

<! -- Bpmn.vue -->
<template>
  <div class="containers">
    <div class="canvas" ref="canvas"></div>
    <div id="js-properties-panel" class="panel"></div>
    <ul class="buttons">
      <li>
          <a ref="saveDiagram" href="javascript:" title="Save as BPMN">Save as BPMN</a>
      </li>
      <li>
          <a ref="saveSvg" href="javascript:" title="Save as SVG">Save as SVG</a>
      </li>
    </ul>
  </div>
</template>
Copy the code

Authoring componentjscode

In JS, I will be the previous several sections of “the most detailed bPMN.js textbook – HTTP request” and “the most detailed BPMN.js textbook – HTTP events” in the function are integrated in.

Basically:

  • When you initialize it, the input isxmlUrlDetermine if it is not null then request data, otherwise assign a default value;
  • After successful initialization, add in the successful functionmodeler,elementListener event of;
  • Initialize the downloadXML, SVG,The link button.

Such as:

// Bpmn.vue
async createNewDiagram () {
  const that = this
  let bpmnXmlStr = ' '
  if (this.xmlUrl === ' ') { // Check whether it exists
      bpmnXmlStr = this.defaultXmlStr
      this.transformCanvas(bpmnXmlStr)
  } else {
      let res = await axios({
          method: 'get'.timeout: 120000.url: that.xmlUrl,
          headers: { 'Content-Type': 'multipart/form-data'}})console.log(res)
      bpmnXmlStr = res['data']
      this.transformCanvas(bpmnXmlStr)
  }
},
transformCanvas(bpmnXmlStr) {
  // Convert the string to a graph for display
  this.bpmnModeler.importXML(bpmnXmlStr, (err) => {
    if (err) {
      console.error(err)
    } else {
      this.success()
    }
    // Let the graph adapt to the screen
    var canvas = this.bpmnModeler.get('canvas')
    canvas.zoom('fit-viewport')
  })
},
success () {
  this.addBpmnListener()
  this.addModelerListener()
  this.addEventBusListener()
},
addBpmnListener () {},
addModelerListener () {},
addEventBusListener () {}
Copy the code

LinDaiDai/ bPMN-custom-Modeler/bPMn.vue

After the language

Project case Git address: LinDaiDai/ bPMN-custom-modeler please give a Star🌟, thanks 😊

For the full catalogue of the series, please check here: “The most detailed BPMN.js textbook in the whole Web”.

Series related recommendations:

“The most detailed BPMN.JS textbook in the Whole Web – Basic Chapter”

The most detailed BPMN.js textbook in the whole web – HTTP Request

The most detailed BPMN.JS textbook in the whole Web – Events

The most detailed BPMN.js textbook on the Whole web – Renderer

The most detailed BPMN.js textbook -contextPad

“The most detailed BPMN. js textbook in the whole web – Editing and Deleting nodes”