Help Needed: Rendering Custom Elements on BPMN.io Canvas via CDN

Hello everyone,
I’m just starting out, and I would really appreciate your help.
I’m trying to use BPMN.io via a CDN, and I want to create custom elements.

At the moment, I’ve added the element to the palette, and I can also display a preview using drag-and-drop. However, I’m unable to render the custom element on the canvas.

I’ll share my code below for review. If possible, please guide me through this.

Test online: https://codesandbox.io/p/sandbox/4k4l4t

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Custom Elements</title>
    <link
      rel="stylesheet"
      href="https://unpkg.com/bpmn-js@17.11.1/dist/assets/diagram-js.css"
    />
    <link
      rel="stylesheet"
      href="https://unpkg.com/bpmn-js@17.11.1/dist/assets/bpmn-font/css/bpmn.css"
    />
    <style>
      .bpmn-task.trapezoid {
        background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 50'%3E%3Cpolygon points='15,50 85,50 65,0 35,0' fill='%23000'%3E%3C/polygon%3E%3C/svg%3E");
        background-repeat: no-repeat;
        background-position: center;
        background-size: contain;
      }

      .bpmn-task.rectangle {
        background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 50'%3E%3Crect width='100' height='50' fill='%23000'/%3E%3C/svg%3E");
        background-repeat: no-repeat;
        background-position: center;
        background-size: contain;
      }

      .bpmn-task.ellipse {
        background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 50'%3E%3Cellipse cx='50' cy='25' rx='50' ry='25' fill='%23000'/%3E%3C/svg%3E");
        background-repeat: no-repeat;
        background-position: center;
        background-size: contain;
      }

      .bpmn-task.triangle {
        background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 50'%3E%3Cpolygon points='50,0 0,50 100,50' fill='%23000'%3E%3C/polygon%3E%3C/svg%3E");
        background-repeat: no-repeat;
        background-position: center;
        background-size: contain;
      }

      #canvas {
        width: 100%;
        height: 600px;
        border: 1px solid lightgray;
      }
    </style>
    <script src="https://unpkg.com/bpmn-js@17.11.1/dist/bpmn-modeler.production.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/snapsvg@0.5.0/dist/snap.svg-min.js"></script>
  </head>
  <body>
    <!-- Canvas Container -->
    <div id="canvas"></div>

    <script>
      // Custom Renderer
      class CustomRenderer {
        constructor(eventBus, bpmnRenderer) {
          this.bpmnRenderer = bpmnRenderer;

          // eventBus.on("element.changed", (event) => {
          //   console.log("element ", event.element, " changed");
          // });

          eventBus.on("render.shape", (event) => {
            const { gfx, element } = event;

            if (element.type === "custom:rectangle") {
              return this.drawRectangle(gfx, element);
            } else if (element.type === "custom:ellipse") {
              return this.drawEllipse(gfx, element);
            } else if (element.type === "custom:triangle") {
              return this.drawTriangle(gfx, element);
            } else if (element.type === "custom:trapezoid") {
              return this.drawTrapezoid(gfx, element);
            }
          });
        }

        drawRectangle(parentGfx, element) {
          const rect = document.createElementNS(
            "http://www.w3.org/2000/svg",
            "rect"
          );
          rect.setAttribute("width", 100);
          rect.setAttribute("height", 50);
          rect.setAttribute("fill", "transparent");
          rect.setAttribute("stroke", "black");
          parentGfx.appendChild(rect);
          return rect;
        }

        drawEllipse(parentGfx, element) {
          const ellipse = document.createElementNS(
            "http://www.w3.org/2000/svg",
            "ellipse"
          );
          ellipse.setAttribute("cx", 50);
          ellipse.setAttribute("cy", 25);
          ellipse.setAttribute("rx", 50);
          ellipse.setAttribute("ry", 25);
          ellipse.setAttribute("fill", "transparent");
          ellipse.setAttribute("stroke", "black");
          parentGfx.appendChild(ellipse);
          return ellipse;
        }

        drawTriangle(parentGfx, element) {
          const polygon = document.createElementNS(
            "http://www.w3.org/2000/svg",
            "polygon"
          );
          polygon.setAttribute("points", "50,0 100,100 0,100");
          polygon.setAttribute("fill", "transparent");
          polygon.setAttribute("stroke", "black");
          parentGfx.appendChild(polygon);
          return polygon;
        }

        drawTrapezoid(parentGfx, element) {
          const polygon = document.createElementNS(
            "http://www.w3.org/2000/svg",
            "polygon"
          );
          polygon.setAttribute("points", "25,0 75,0 100,50 0,50");
          polygon.setAttribute("fill", "transparent");
          polygon.setAttribute("stroke", "black");
          parentGfx.appendChild(polygon);
          return polygon;
        }
      }
      CustomRenderer.$inject = ["eventBus", "bpmnRenderer"];

      // Custom Palette Provider
      class CustomPaletteProvider {
        constructor(bpmnFactory, palette, create, elementFactory) {
          this.bpmnFactory = bpmnFactory;
          this.create = create;
          this.elementFactory = elementFactory;
          palette.registerProvider(this);
        }

        getPaletteEntries() {
          const { bpmnFactory, create, elementFactory } = this;

          const createShapeAction = (type) => (event) => {
            const shape = elementFactory.createShape({
              type: `custom:${type}`,
              businessObject: {
                suitable: type,
              },
            });
            create.start(event, shape);
          };

          return {
            "create.rectangle": {
              group: "customElement",
              className: "bpmn-task rectangle",
              title: "Create rectangle",
              action: {
                dragstart: createShapeAction("rectangle"),
                click: createShapeAction("rectangle"),
              },
            },
            "create.ellipse": {
              group: "customElement",
              className: "bpmn-task ellipse",
              title: "Create ellipse",
              action: {
                dragstart: createShapeAction("ellipse"),
                click: createShapeAction("ellipse"),
              },
            },
            "create.triangle": {
              group: "customElement",
              className: "bpmn-task triangle",
              title: "Create triangle",
              action: {
                dragstart: createShapeAction("triangle"),
                click: createShapeAction("triangle"),
              },
            },
            "create.trapezoid": {
              group: "customElement",
              className: "bpmn-task trapezoid",
              title: "Create trapezoid",
              action: {
                dragstart: createShapeAction("trapezoid"),
                click: createShapeAction("trapezoid"),
              },
            },
          };
        }
      }
      CustomPaletteProvider.$inject = [
        "bpmnFactory",
        "palette",
        "create",
        "elementFactory",
      ];

      var diagramXML = `<?xml version="1.0" encoding="UTF-8"?>
           <bpmn:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                             xmlns:bpmn="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"
                             id="Definitions_1" targetNamespace="http://bpmn.io/schema/bpmn">
             <bpmn:process id="Process_1" isExecutable="false">
               <bpmn:startEvent id="StartEvent_1"/>
             </bpmn:process>
             <bpmndi:BPMNDiagram id="BPMNDiagram_1">
               <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
                 <bpmndi:BPMNShape id="StartEvent_1_di" bpmnElement="StartEvent_1">
                   <dc:Bounds x="173" y="102" width="36" height="36"/>
                 </bpmndi:BPMNShape>
               </bpmndi:BPMNPlane>
             </bpmndi:BPMNDiagram>
           </bpmn:definitions>`;

      // Integrating custom rules and modules
      var bpmnModeler = new BpmnJS({
        container: "#canvas",
        additionalModules: [
          {
            __init__: ["customRenderer", "customPaletteProvider"],
            customRenderer: ["type", CustomRenderer],
            customPaletteProvider: ["type", CustomPaletteProvider],
          },
        ],
      });

      function logXMLOnChange(bpmnModeler) {
        const eventBus = bpmnModeler.get("eventBus");

        eventBus.on("commandStack.changed", function () {
          bpmnModeler.saveXML({ format: true }).then(function (result) {
            console.log("Updated XML after change:", result.xml);
          });
        });
      }

      // Import BPMN diagram
      bpmnModeler
        .importXML(diagramXML)
        .then(function () {
          console.log("Diagram imported successfully!");
          logXMLOnChange(bpmnModeler);
        })
        .catch(function (err) {
          console.error("Error importing diagram:", err);
        });
    </script>
  </body>
</html>

Your elements are missing proper businessObject. Have a look at the implementation in bpmn-js-example-custom-elements.

Next thing is your custom type in elementFactory. For that to work, you need to build a custom elementFactory. Have a look at the example for custom shapes.

Good luck :slight_smile:

1 Like