├── .babelrc ├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── CI.yml ├── .gitignore ├── .npmrc ├── README.md ├── app ├── app.js ├── css │ └── app.css ├── custom │ ├── CustomRenderer.js │ └── index.js └── index.html ├── docs └── screenshot.png ├── package.json ├── resources └── diagram.bpmn └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "env" ] 3 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | public/ -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "plugin:bpmn-io/es6", 3 | "env": { 4 | "browser": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [ push, pull_request ] 3 | jobs: 4 | Build: 5 | 6 | strategy: 7 | matrix: 8 | os: [ ubuntu-latest ] 9 | node-version: [ 14 ] 10 | 11 | runs-on: ${{ matrix.os }} 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | - name: Use Node.js 17 | uses: actions/setup-node@v2 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: Cache Node.js modules 21 | uses: actions/cache@v2 22 | with: 23 | # npm cache files are stored in `~/.npm` on Linux/macOS 24 | path: ~/.npm 25 | key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }} 26 | restore-keys: | 27 | ${{ runner.OS }}-node- 28 | ${{ runner.OS }}- 29 | - name: Install dependencies 30 | run: npm install 31 | - name: Build 32 | run: npm run all -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | public/ -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > This example is part of our [:notebook: custom elements guide](https://github.com/bpmn-io/bpmn-js-examples/tree/master/custom-elements). Checkout the final result [here](https://github.com/bpmn-io/bpmn-js-example-custom-elements). 2 | 3 | 4 | # bpmn-js Example: Custom Rendering 5 | 6 | [![CI](https://github.com/bpmn-io/bpmn-js-example-custom-rendering/workflows/CI/badge.svg)](https://github.com/bpmn-io/bpmn-js-example-custom-rendering/actions?query=workflow%3ACI) 7 | 8 | An example of creating custom rendering for [bpmn-js](https://github.com/bpmn-io/bpmn-js). Custom rendering allows you to render any shape or connection the way you want. 9 | 10 | 11 | ## About 12 | 13 | This example renders `bpmn:Task` and `bpmn:Event` elements differently. 14 | 15 | ![Screenshot](docs/screenshot.png) 16 | 17 | ### Creating a Custom Renderer 18 | 19 | In order to render `bpmn:Task` and `bpmn:Event` elements differently we'll create a custom renderer. By extending [BaseRenderer](https://github.com/bpmn-io/diagram-js/blob/master/lib/draw/BaseRenderer.js) we make sure our renderer will be called whenever a shape or connection is to be rendered. Note that we also specify a priority higher than the default priority of 1000 so our renderer will be called first. 20 | 21 | ```javascript 22 | const HIGH_PRIORITY = 1500; 23 | 24 | export default class CustomRenderer extends BaseRenderer { 25 | constructor(eventBus) { 26 | super(eventBus, HIGH_PRIORITY); 27 | 28 | ... 29 | } 30 | ... 31 | } 32 | ``` 33 | 34 | Whenever our renderer is called we need to decide whether we want to render an element or if the [default renderer](https://github.com/bpmn-io/bpmn-js/blob/master/lib/draw/BpmnRenderer.js) should render it. We're only interested in rendering `bpmn:Task` and `bpmn:Event` elements: 35 | 36 | ```javascript 37 | canRender(element) { 38 | 39 | // only render tasks and events (ignore labels) 40 | return isAny(element, [ 'bpmn:Task', 'bpmn:Event' ]) && !element.labelTarget; 41 | } 42 | ``` 43 | 44 | Once we've decided to render an element depending on the element's type our renderers `drawShape` or `drawConnection` will be called. Since we only render shapes, we don't need to implement `drawConnection`. We don't want to render tasks and events entirely different, so we'll let the default renderer do the heavy lifting of rendering the shape and then change it afterward: 45 | 46 | ```javascript 47 | drawShape(parentNode, element) { 48 | const shape = this.bpmnRenderer.drawShape(parentNode, element); 49 | 50 | if (is(element, 'bpmn:Task')) { 51 | const rect = drawRect(parentNode, 100, 80, TASK_BORDER_RADIUS, '#52B415'); 52 | 53 | prependTo(rect, parentNode); 54 | 55 | svgRemove(shape); 56 | 57 | return shape; 58 | } 59 | 60 | const rect = drawRect(parentNode, 30, 20, TASK_BORDER_RADIUS, '#cc0000'); 61 | 62 | svgAttr(rect, { 63 | transform: 'translate(-20, -10)' 64 | }); 65 | 66 | return shape; 67 | } 68 | ``` 69 | 70 | If the element is a `bpmn:Task` we render the task first and then replace its rectangle with our own rectangle. Therefore, we don't have to render labels and markers ourselves. 71 | 72 | In the case of `bpmn:Event` elements we let the default renderer render the element first before we render an additional rectangle on top of it. 73 | 74 | You can also decide to take care of the rendering entirely on your own without using the default renderer at all. 75 | 76 | Finally, since we are rendering shapes we need to implement a `getShapePath` method which will be called whenever a connection is to be cropped: 77 | 78 | ```javascript 79 | getShapePath(shape) { 80 | if (is(shape, 'bpmn:Task')) { 81 | return getRoundRectPath(shape, TASK_BORDER_RADIUS); 82 | } 83 | 84 | return this.bpmnRenderer.getShapePath(shape); 85 | } 86 | ``` 87 | 88 | See the entire renderer [here](app/custom/CustomRenderer.js). 89 | 90 | Next, let's add our custom renderer to bpmn-js. 91 | 92 | ### Adding the Custom Renderer to bpmn-js 93 | 94 | When creating a new instance of bpmn-js we need to add our custom renderer using the `additionalModules` property: 95 | 96 | ```javascript 97 | import BpmnModeler from 'bpmn-js/lib/Modeler'; 98 | 99 | import customRendererModule from './custom'; 100 | 101 | const bpmnModeler = new BpmnModeler({ 102 | additionalModules: [ 103 | customRendererModule 104 | ] 105 | }); 106 | ``` 107 | 108 | Our custom renderer will now render all task and event shapes. 109 | 110 | ## Run the Example 111 | 112 | You need a [NodeJS](http://nodejs.org) development stack with [npm](https://npmjs.org) installed to build the project. 113 | 114 | To install all project dependencies execute 115 | 116 | ```sh 117 | npm install 118 | ``` 119 | 120 | To start the example execute 121 | 122 | ```sh 123 | npm start 124 | ``` 125 | 126 | To build the example into the `public` folder execute 127 | 128 | ```sh 129 | npm run all 130 | ``` 131 | 132 | 133 | ## License 134 | 135 | MIT 136 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | import BpmnModeler from 'bpmn-js/lib/Modeler'; 2 | 3 | import customRendererModule from './custom'; 4 | 5 | import diagramXML from '../resources/diagram.bpmn'; 6 | 7 | const containerEl = document.getElementById('container'); 8 | 9 | // create modeler 10 | const bpmnModeler = new BpmnModeler({ 11 | container: containerEl, 12 | additionalModules: [ 13 | customRendererModule 14 | ] 15 | }); 16 | 17 | // import XML 18 | bpmnModeler.importXML(diagramXML).catch((err) => { 19 | if (err) { 20 | console.error(err); 21 | } 22 | }); -------------------------------------------------------------------------------- /app/css/app.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | margin: 0; 4 | outline: none; 5 | padding: 0; 6 | } 7 | 8 | html, body, #container { 9 | height: 100%; 10 | } -------------------------------------------------------------------------------- /app/custom/CustomRenderer.js: -------------------------------------------------------------------------------- 1 | import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer'; 2 | 3 | import { 4 | append as svgAppend, 5 | attr as svgAttr, 6 | create as svgCreate, 7 | remove as svgRemove 8 | } from 'tiny-svg'; 9 | 10 | import { 11 | getRoundRectPath 12 | } from 'bpmn-js/lib/draw/BpmnRenderUtil'; 13 | 14 | import { is } from 'bpmn-js/lib/util/ModelUtil'; 15 | import { isAny } from 'bpmn-js/lib/features/modeling/util/ModelingUtil'; 16 | 17 | const HIGH_PRIORITY = 1500, 18 | TASK_BORDER_RADIUS = 2; 19 | 20 | 21 | export default class CustomRenderer extends BaseRenderer { 22 | constructor(eventBus, bpmnRenderer) { 23 | super(eventBus, HIGH_PRIORITY); 24 | 25 | this.bpmnRenderer = bpmnRenderer; 26 | } 27 | 28 | canRender(element) { 29 | 30 | // only render tasks and events (ignore labels) 31 | return isAny(element, [ 'bpmn:Task', 'bpmn:Event' ]) && !element.labelTarget; 32 | } 33 | 34 | drawShape(parentNode, element) { 35 | const shape = this.bpmnRenderer.drawShape(parentNode, element); 36 | 37 | if (is(element, 'bpmn:Task')) { 38 | const rect = drawRect(parentNode, 100, 80, TASK_BORDER_RADIUS, '#52B415'); 39 | 40 | prependTo(rect, parentNode); 41 | 42 | svgRemove(shape); 43 | 44 | return shape; 45 | } 46 | 47 | const rect = drawRect(parentNode, 30, 20, TASK_BORDER_RADIUS, '#cc0000'); 48 | 49 | svgAttr(rect, { 50 | transform: 'translate(-20, -10)' 51 | }); 52 | 53 | return shape; 54 | } 55 | 56 | getShapePath(shape) { 57 | if (is(shape, 'bpmn:Task')) { 58 | return getRoundRectPath(shape, TASK_BORDER_RADIUS); 59 | } 60 | 61 | return this.bpmnRenderer.getShapePath(shape); 62 | } 63 | } 64 | 65 | CustomRenderer.$inject = [ 'eventBus', 'bpmnRenderer' ]; 66 | 67 | // helpers ////////// 68 | 69 | // copied from https://github.com/bpmn-io/bpmn-js/blob/master/lib/draw/BpmnRenderer.js 70 | function drawRect(parentNode, width, height, borderRadius, strokeColor) { 71 | const rect = svgCreate('rect'); 72 | 73 | svgAttr(rect, { 74 | width: width, 75 | height: height, 76 | rx: borderRadius, 77 | ry: borderRadius, 78 | stroke: strokeColor || '#000', 79 | strokeWidth: 2, 80 | fill: '#fff' 81 | }); 82 | 83 | svgAppend(parentNode, rect); 84 | 85 | return rect; 86 | } 87 | 88 | // copied from https://github.com/bpmn-io/diagram-js/blob/master/lib/core/GraphicsFactory.js 89 | function prependTo(newNode, parentNode, siblingNode) { 90 | parentNode.insertBefore(newNode, siblingNode || parentNode.firstChild); 91 | } -------------------------------------------------------------------------------- /app/custom/index.js: -------------------------------------------------------------------------------- 1 | import CustomRenderer from './CustomRenderer'; 2 | 3 | export default { 4 | __init__: [ 'customRenderer' ], 5 | customRenderer: [ 'type', CustomRenderer ] 6 | }; -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | bpmn-js-example-model-extension 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpmn-io/bpmn-js-example-custom-rendering/843d7241fa2b95a14d73e0f2fcf80d549fb628e0/docs/screenshot.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bpmn-js-example-custom-rendering", 3 | "version": "0.0.0", 4 | "description": "An example of creating custom rendering for bpmn-js", 5 | "scripts": { 6 | "all": "run-s lint build", 7 | "build": "webpack --mode production", 8 | "build:watch": "webpack --watch", 9 | "serve": "sirv public --dev", 10 | "dev": "run-p build:watch serve", 11 | "lint": "eslint .", 12 | "start": "run-s build serve" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/bpmn-io/bpmn-js-example-custom-rendering" 17 | }, 18 | "keywords": [ 19 | "bpmnjs-example" 20 | ], 21 | "author": { 22 | "name": "Philipp Fromme", 23 | "url": "https://github.com/philippfromme" 24 | }, 25 | "contributors": [ 26 | { 27 | "name": "bpmn.io contributors", 28 | "url": "https://github.com/bpmn-io" 29 | } 30 | ], 31 | "license": "MIT", 32 | "devDependencies": { 33 | "copy-webpack-plugin": "^8.1.0", 34 | "eslint": "^7.22.0", 35 | "eslint-plugin-bpmn-io": "^0.12.0", 36 | "npm-run-all": "^4.1.5", 37 | "raw-loader": "^4.0.2", 38 | "sirv-cli": "^1.0.11", 39 | "webpack": "^5.28.0", 40 | "webpack-cli": "^4.5.0", 41 | "webpack-dev-server": "^3.11.2" 42 | }, 43 | "dependencies": { 44 | "bpmn-js": "^8.2.2", 45 | "diagram-js": "^7.2.3", 46 | "tiny-svg": "^2.2.2" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /resources/diagram.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SequenceFlow_1 6 | 7 | 8 | 9 | SequenceFlow_1 10 | SequenceFlow_2 11 | SequenceFlow_5 12 | 13 | 14 | 15 | 16 | SequenceFlow_2 17 | 18 | 19 | 20 | SequenceFlow_5 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 2 | 3 | module.exports = { 4 | entry: { 5 | bundle: ['./app/app.js'] 6 | }, 7 | output: { 8 | path: __dirname + '/public', 9 | filename: 'app.js' 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.bpmn$/, 15 | use: 'raw-loader' 16 | } 17 | ] 18 | }, 19 | plugins: [ 20 | new CopyWebpackPlugin({ 21 | patterns: [ 22 | { from: 'assets/**', to: 'vendor/bpmn-js', context: 'node_modules/bpmn-js/dist/' }, 23 | { from: '**/*.{html,css}', context: 'app/' } 24 | ] 25 | }) 26 | ], 27 | mode: 'development', 28 | devtool: 'source-map' 29 | }; --------------------------------------------------------------------------------