preface

Since Activiti and BPMN. js were required for the project before, I learned and operated a lot. During the learning and using process, I often went to Baidu and found that articles on Activiti7 and BPMN. js were relatively few, or the description was not detailed enough and the version was relatively old. So let’s document some of the steps and actions I took to integrate BPMn.js.

I will record some Activiti7 operations in the back of Spring Boot integration next time. This time, I will take a look at how Vue integrates BPMN. js to realize online drawing, Xml export, SVG, online saving and other operations.

If you have any good suggestions or questions, you can send an email to [email protected].

Github complete code BPMN – Demo author blog Winily

Preparatory work

First of all needless to say there must be a Vue project first, first build a Vue project, install dependencies.

// Create a Vue project Vue create BPMN -demo // Install project dependencies, I prefer yarn, yarn // or use NPM NPM installCopy the code

With the project done, all we need to do is install the BPMN-JS dependencies and integrate the code. Start by installing a dependency

// yarn install yarn add bpmn-js // NPM install NPM install bpmn-jsCopy the code

First of all, open the project main.js and import the font library and style file about BPMN-JS. If you do not import these style files, the left control menu of BPMN-JS will not be displayed

import 'bpmn-js/dist/assets/diagram-js.css'
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css'
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css'
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css'
Copy the code

Create the Bpmn modeler

Once the preparation is done, it is time to use BPMN-js in Vue. Here I will write the code directly in app.vue for convenience, and then write it into the component as required.

First of all, bPMN-JS modeler is actually implemented through Canvas, we create a div in the template for Bpmn to create the modeler.

<template>
    <div id="app">
        <div class="container">
            <! Npmn-js implements drawing on canvas and sets ref to get element from vue.
            <div class="bpmn-canvas" ref="canvas"></div>
        </div>
    </div>
</template>
Copy the code

With the Html part written, let’s implement the JS part. To create the modeler, we use the BpmnModeler object. We create the modeler primarily by creating this object.

// Introduce the Bpmn modeler object here
import BpmnModeler from "bpmn-js/lib/Modeler";

export default {
    data() {
        return {
            bpmnModeler: null.canvas: null.// This part of the specific code I put below
            initTemplate: ` slightly ` 
        };
    },
    methods: {
        init() {
            // Get the canvas element
            this.canvas = this.$refs.canvas;
            // Create a Bpmn object
            this.bpmnModeler = new BpmnModeler({
                // Set BPMN's drawing container to the canvas element fetched at the door
                container: this.canvas
            });

            // Initialize the modeler content
            this.initDiagram(this.initTemplate);
        },
        initDiagram(bpmn) {
            // Import XML into the BPMN-JS modeler
            this.bpmnModeler.importXML(bpmn, err => {
                if (err) {
                    this.$Message.error("Error opening model, please confirm that model conforms to Bpmn2.0 specification"); }}); }},// The bPMN-JS modeler is created by calling the init function after the component is loaded
    mounted() {
        this.init(); }};Copy the code

This part of THE XML is the Bpmn flowchart template code, this template contains a start node in the inside, according to your needs to create or modify the template, of course I do not recommend manual modification of the XML code. The way to create a template I recommend is to create a desired model in the BPMN-JS modeler, then export an XML or Bpmn file and copy the code from the file to use.

One of the things THAT I noticed in the Activiti7 back end of the XML template that I used was the definitions tag at the XML header where I introduced a lot of namespaces, If your flowchart needs to be deployed in Activiti, you must import the Activiti namespace. The following template is an Xml template for Activiti. Failure to introduce or conform to Activiti’s canonical namespace will result in model deployment failure. It is recommended that the model be modified based on the following template if it will eventually be deployed to Activiti.

<?xml version="1.0" encoding="UTF-8"? >
<definitions 
  xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" 
  xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" 
  xmlns:camunda="http://camunda.org/schema/1.0/bpmn" 
  xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
  xmlns:activiti="http://activiti.org/bpmn" 
  id="m1577635100724" 
  name="" 
  targetNamespace="http://www.activiti.org/testm1577635100724"
>
  <process id="process" processType="None" isClosed="false" isExecutable="true">
    <extensionElements>
      <camunda:properties>
        <camunda:property name="a" value="1" />
      </camunda:properties>
    </extensionElements>
    <startEvent id="_2" name="start" />
  </process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_leave">
    <bpmndi:BPMNPlane id="BPMNPlane_leave" bpmnElement="leave">
      <bpmndi:BPMNShape id="BPMNShape__2" bpmnElement="_2">
        <omgdc:Bounds x="144" y="368" width="32" height="32" />
        <bpmndi:BPMNLabel>
          <omgdc:Bounds x="149" y="400" width="23" height="14" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNShape>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</definitions>
Copy the code

Now that we have created the modeler, let’s see what it looks like.

Add the bPMn-js-properties-panel plugin

Bpmn-js does not support the setting of Activities custom values, so it needs to introduce additional plug-ins for integration. Bpmn-js-properties-panel and Camunda-bPMn-Moddle should be installed first

Bpmn-js-properties-panel provides a property editor for the modeler, and Camunda-bPMn-Moddle extends what the property editor can edit. These attributes, such as Assignee of Activitie, rely on camunda-bPMn-moddle to provide editing.

Yarn add bpmn-js-properties-panel yarn add camunda-bpmn-moddle // NPM install NPM install bPMn-js-properties-panel  npm install camunda-bpmn-moddleCopy the code

After installing the dependency, introduce the bPMn-js-properties-panel style in main.js otherwise the toolbar will not display

// Style the left toolbar and edit the node
import 'bpmn-js-properties-panel/dist/assets/bpmn-js-properties-panel.css'
Copy the code

Create a div in app. vue to give the toolbar a location to display. I’ll just put it under the canvas

<! -- Where the toolbar displays -->
<div class="bpmn-js-properties-panel" id="js-properties-panel"></div>
Copy the code

Then import the toolbar and configure toolbar support

// Toolbar related
import propertiesProviderModule from "bpmn-js-properties-panel/lib/provider/camunda";
import propertiesPanelModule from "bpmn-js-properties-panel";
import camundaModdleDescriptor from "camunda-bpmn-moddle/resources/camunda";
Copy the code

The modified init function

init() {
  // Get the canvas element
  this.canvas = this.$refs.canvas;

  // Create a Bpmn object
  this.bpmnModeler = new BpmnModeler({
    // Set BPMN's drawing container to the canvas element fetched at the door
    container: this.canvas,

    // Add toolbar support
    propertiesPanel: {
      parent: "#js-properties-panel"
    },
    additionalModules: [propertiesProviderModule, propertiesPanelModule],
    moddleExtensions: {
      camunda: camundaModdleDescriptor
    }
  });

  this.createNewDiagram(this.bpmnTemplate);
}
Copy the code

The effect

Implement new, import, export operations

To do this, we write buttons and styles and attach them to click events. I’m using the Element component

One thing to note here is that if you are using the process editor to edit the process and the process needs to be deployed to Activiti to run, there is a problem. Using the camunda-bPMn-Moddle setting, For example, setting Assignee to ‘triple’ results in CAMunda: Assignee = ‘triple’ resulting in XML, which does not meet Activiti’s specification requirements, In Activiti, Activiti :assignee=” ga3 “is required, otherwise it will not be recognized normally, So one of my solutions to this problem is to simply replace Camunda with Activiti with the re when the XML text is retrieved from the saveXML function so that the process can run normally.

<div class="action">
  <! To open a file, I use Element's file upload component. The hook gets the file before uploading and then reads the file contents.
  <el-upload class="upload-demo" :before-upload="openBpmn">
    <el-button icon="el-icon-folder-opened"></el-button>
  </el-upload>
  <el-button class="new" icon="el-icon-circle-plus" @click="newDiagram"></el-button>
  <el-button icon="el-icon-download" @click="downloadBpmn"></el-button>
  <el-button icon="el-icon-picture" @click="downloadSvg"></el-button>
</div>
Copy the code

And it looks something like this

Next, implement the operation

Import file operation ()

I used Element’s file upload component, used a before-upload hook to fetch the file object and then retrieved the file contents from the file object and fed them to the Bpmn modeler.

openBpmn(file) {
  const reader = new FileReader();
  // Read the text information in the File object in utF-8 format
  reader.readAsText(file, "utf-8");
  reader.onload = (a)= > {
    // Import the text information into the Bpmn modeler after reading
    this.createNewDiagram(reader.result);
  };
  return false;
}
Copy the code

New Diagram Operation

Creating a new image is as simple as reloading the original template

newDiagram() {
  this.createNewDiagram(this.bpmnTemplate);
},
Copy the code

Exporting files

For the export operation, I set a hidden A link here, because what I do is to get the file content, set the file name and file format, and then generate the download link to the A label and trigger the click event of the A label to trigger the download.

<a hidden ref="downloadLink"></a>
Copy the code

Download related operations

download({ name = "diagram.bpmn", data }) {
  // Here is the hidden link set earlier
  const downloadLink = this.$refs.downloadLink;
  // Convert the data to urIs and download the ones to use
  const encodedData = encodeURIComponent(data);
  if (data) {
    // Pass the data to the link
    downloadLink.href =
      "data:application/bpmn20-xml; charset=UTF-8," + encodedData;
    // Set the file name
    downloadLink.download = name;
    // Trigger the click event to start the downloaddownloadLink.click(); }},Copy the code

For convenience, I wrote a function to get the file name, so I’ll just take the model ID as the file name.

getFilename(xml) {
  let start = xml.indexOf("process");
  let filename = xml.substr(start, xml.indexOf(">"));
  filename = filename.substr(filename.indexOf("id") + 4);
  filename = filename.substr(0, filename.indexOf('"'));
  return filename;
},
Copy the code

1. Export Bpmn operations

The operation of exporting Bpmn files is relatively simple. In fact, Bpmn files are no different from XML files except the suffix is not quite the same. In fact, Bpmn files are stored in THE XML tag structure. So we’re going to export and download the Bpmn file just by getting the XML of the model from the BPMN-JS modeler.

downloadBpmn() {
  this.bpmnModeler.saveXML({ format: true }, (err, xml) => {
    if(! err) {// Get the file name
      const name = `The ${this.getFilename(xml)}.bpmn`;
      // Pass the file name and data to the download function
      this.download({ name: name, data: xml }); }}); },Copy the code

2. Export SVG operations

Exporting SVG is a little bit more cumbersome, as I didn’t find any function in the BPMN-JS modeler that could provide the export, so I had to extract the SVG tags from the Canvas canvas and download them myself

downloadSvg() {
  this.bpmnModeler.saveXML({ format: true }, (err, xml) => {
    if(! err) {// Get the file name
      const name = `The ${this.getFilename(xml)}.svg`;
      
      // Extract the SVG graphic label from the modeler canvas
      let context = "";
      const djsGroupAll = this.$refs.canvas.querySelectorAll(".djs-group");
      for (let item of djsGroupAll) {
        context += item.innerHTML;
      }
      // Get basic SVG data, length, width and height
      const viewport = this.$refs.canvas
        .querySelector(".viewport")
        .getBBox();

      // Concatenate labels and data into a fully normal SVG graph
      const svg = `
        <svg
          xmlns="http://www.w3.org/2000/svg"
          xmlns:xlink="http://www.w3.org/1999/xlink"
          width="${viewport.width}"
          height="${viewport.height}"
          viewBox="${viewport.x} ${viewport.y} ${viewport.width} ${viewport.height}"Version =" 1.1 ">${context}
        </svg>
      `;
      // Pass the file name and data to the download function
      this.download({ name: name, data: svg }); }}); },Copy the code

Final effect and complete code

Effect of screenshots

The complete code

  1. main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'

// BPMN dependencies
import 'bpmn-js/dist/assets/diagram-js.css' 
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css'
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css'
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css'

// Style the left toolbar and edit the node
import 'bpmn-js-properties-panel/dist/assets/bpmn-js-properties-panel.css'

import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';

Vue.use(ElementUI);

Vue.config.productionTip = false

new Vue({
  router,
  render: h= > h(App)
}).$mount('#app')
Copy the code
  1. App.vue
<template>
  <div id="app">
    <div class="container">
      <! Npmn-js implements drawing on canvas and sets ref to get element from vue.
      <div class="bpmn-canvas" ref="canvas"></div>
      <! -- Where the toolbar displays -->
      <div class="bpmn-js-properties-panel" id="js-properties-panel"></div>


      <! Write the operation button inside this -->
      <div class="action">
        <el-upload action class="upload-demo" :before-upload="openBpmn">
          <el-button icon="el-icon-folder-opened"></el-button>
        </el-upload>
        <el-button class="new" icon="el-icon-circle-plus" @click="newDiagram"></el-button>
        <el-button icon="el-icon-download" @click="downloadBpmn"></el-button>
        <el-button icon="el-icon-picture" @click="downloadSvg"></el-button>

        <a hidden ref="downloadLink"></a>
      </div>
    </div>
  </div>
</template>

<script>
import BpmnModeler from "bpmn-js/lib/Modeler";
// Toolbar related
import propertiesProviderModule from "bpmn-js-properties-panel/lib/provider/camunda";
import propertiesPanelModule from "bpmn-js-properties-panel";
import camundaModdleDescriptor from "camunda-bpmn-moddle/resources/camunda";

export default {
  data() {
    return {
      bpmnModeler: null,
      canvas: null, bpmnTemplate: ` <? xml version="1.0" encoding="UTF-8"? > <definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" 
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
              xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" 
              xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" 
              xmlns:camunda="http://camunda.org/schema/1.0/bpmn" 
              xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
              xmlns:activiti="http://activiti.org/bpmn" 
              id="m1577635100724" 
              name="" 
              targetNamespace="http://www.activiti.org/testm1577635100724"
            >
            <process id="process" processType="None" isClosed="false" isExecutable="true">
              <extensionElements>
                <camunda:properties>
                  <camunda:property name="a" value="1" />
                </camunda:properties>
              </extensionElements>
              <startEvent id="_2" name="start" />
            </process>
            <bpmndi:BPMNDiagram id="BPMNDiagram_leave">
              <bpmndi:BPMNPlane id="BPMNPlane_leave" bpmnElement="leave">
                <bpmndi:BPMNShape id="BPMNShape__2" bpmnElement="_2">
                  <omgdc:Bounds x="144" y="368" width="32" height="32" />
                  <bpmndi:BPMNLabel>
                    <omgdc:Bounds x="149" y="400" width="23" height="14" />
                  </bpmndi:BPMNLabel>
                </bpmndi:BPMNShape>
              </bpmndi:BPMNPlane>
            </bpmndi:BPMNDiagram>
          </definitions>
      `
    };
  },
  methods: {
    newDiagram() {
      this.createNewDiagram(this.bpmnTemplate);
    },

    downloadBpmn() {
      this.bpmnModeler.saveXML({ format: true }, (err, xml) => {
        if(! err) {// Get the file name
          const name = `${this.getFilename(xml)}.bpmn`;
          // Pass the file name and data to the download method
          this.download({ name: name, data: xml }); }}); }, downloadSvg() {this.bpmnModeler.saveXML({ format: true }, (err, xml) => {
        if(! err) {// Get the file name
          const name = `${this.getFilename(xml)}.svg`;

          // Extract the SVG graphic label from the modeler canvas
          let context = "";
          const djsGroupAll = this.$refs.canvas.querySelectorAll(".djs-group");
          for (let item of djsGroupAll) {
            context += item.innerHTML;
          }
          // Get basic SVG data, length, width and height
          const viewport = this.$refs.canvas
            .querySelector(".viewport")
            .getBBox();

          // Concatenate labels and data into a fully normal SVG graph
          const svg = `
            <svg
              xmlns="http://www.w3.org/2000/svg"
              xmlns:xlink="http://www.w3.org/1999/xlink"
              width="${viewport.width}"
              height="${viewport.height}"
              viewBox="${viewport.x} ${viewport.y} ${viewport.width} ${viewport.height}"
              version="1.1"
              >
              ${context}
            </svg>
          `;
          // Pass the file name and data to the download method
          this.download({ name: name, data: svg }); }}); }, openBpmn(file) {const reader = new FileReader();
      // Read the text information in the File object in utF-8 format
      reader.readAsText(file, "utf-8");
      reader.onload = () => {
        // Import the text information into the Bpmn modeler after reading
        this.createNewDiagram(reader.result);
      };
      return false;
    },

    getFilename(xml) {
      let start = xml.indexOf("process");
      let filename = xml.substr(start, xml.indexOf(">"));
      filename = filename.substr(filename.indexOf("id") + 4);
      filename = filename.substr(0, filename.indexOf('"'));
      return filename;
    },

    download({ name = "diagram.bpmn", data }) {
      // Here is the hidden link set earlier
      const downloadLink = this.$refs.downloadLink;
      // Convert input to URIs and download the required ones
      const encodedData = encodeURIComponent(data);

      if (data) {
        // Pass the data to the link
        downloadLink.href =
          "data:application/bpmn20-xml; charset=UTF-8," + encodedData;
        // Set the file name
        downloadLink.download = name;
        // Trigger the click event to start the download
        downloadLink.click();
      }
    },

    async init() {
      // Get the canvas element
      this.canvas = this.$refs.canvas;

      // Create a Bpmn object
      this.bpmnModeler = new BpmnModeler({
        // Set BPMN's drawing container to the canvas element fetched at the door
        container: this.canvas,

        // Add toolbar support
        propertiesPanel: {
          parent: "#js-properties-panel"
        },
        additionalModules: [propertiesProviderModule, propertiesPanelModule],
        moddleExtensions: {
          camunda: camundaModdleDescriptor
        }
      });

      await this.createNewDiagram(this.bpmnTemplate);
    },
    async createNewDiagram(bpmn) {
      // Convert string to graph;
      this.bpmnModeler.importXML(bpmn, err => {
        if (err) {
          this.$Message.error("Error opening model, please confirm that model conforms to Bpmn2.0 specification"); }}); } }, mounted() {this.init(); }};</script>

<style>

.bpmn-canvas {
  width: 100%;
  height: 100vh;
}

.action {
  position: fixed;
  bottom: 10px;
  left: 10px;
  display: flex;
}
.upload-demo {
  margin-right: 10px;
}

.bpmn-js-properties-panel {
  position: absolute;
  top: 0;
  right: 0px;
  width: 300px;
}
</style>
Copy the code