├── .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 | [](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 |  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 |