npm package: @stacc/camunda-web-modeler
Description: A browser-native BPMN and DMN modeler based on bpmn.io.
Latest version: 1.0.4

# Camunda Web Modeler

![Screenshot](./static/screenshot.jpeg)

This is a React component based on [bpmn.io](https://bpmn.io) that allows you to use a fully functional modeler for BPMN and DMN in your browser application. It has lots of configuration options and offers these features:

- Full support for BPMN and DMN
- Embedded XML editor
- Easily import element templates
- Full support for using bpmn.io plugins
- Access to all bpmn.io and additional events to integrate it more easily into your application
- Exposes the complete bpmn.io API
- Includes type definitions for many of the undocumented features and endpoints of bpmn.io
- TypeScript support

# Usage

## Getting Started

1. Add this dependency to your application:

```
npm install @stacc/camunda-web-modeler
```

2. Include it in your application:

```typescript
import {
   BpmnModeler,
   CustomBpmnJsModeler,
   Event,
   isContentSavedEvent
} from "@stacc/camunda-web-modeler";
import React, { useCallback, useMemo, useRef, useState } from 'react';
import './App.css';

const App: React.FC = () => {
   const modelerRef = useRef<CustomBpmnJsModeler>();

   const [xml, setXml] = useState<string>(BPMN);

   const onEvent = useCallback(async (event: Event<any>) => {
      if (isContentSavedEvent(event)) {
         setXml(event.data.xml);
         return;
      }
   }, []);

   /**
    * ====
    * CAUTION: Using useMemo() is important to prevent additional render cycles!
    * ====
    */

   const modelerTabOptions = useMemo(() => ({
      modelerOptions: {
         refs: [modelerRef]
      }
   }), []);

   return (
      <div style={{
         height: "100vh"
      }}>
         <BpmnModeler
            xml={xml}
            onEvent={onEvent}
            modelerTabOptions={modelerTabOptions} />
      </div>
   );
}

export default App;

const BPMN = /* ... */;
```

3. Include your BPMN in the last line and run the application!

## Full example

To see all options available, you can use this example. Remember that it's important to wrap all options and callbacks that are passed into the component using `useMemo()` and `useCallback()`. Else you will have lots of additional render cycles that can lead to bugs that are difficult to debug.

Using the `bpmnJsOptions`, you can pass any options that you would normally pass into bpmn.io. The component will merge these with its own options and use it to create the modeler instance.

```typescript
import {
    BpmnModeler,
    ContentSavedReason,
    CustomBpmnJsModeler,
    Event,
    isBpmnIoEvent,
    isContentSavedEvent,
    isNotificationEvent,
    isPropertiesPanelResizedEvent,
    isUIUpdateRequiredEvent
} from "@stacc/camunda-web-modeler";
import React, { useCallback, useMemo, useRef, useState } from 'react';
import './App.css';

const App: React.FC = () => {
    const modelerRef = useRef<CustomBpmnJsModeler>();

    const [xml, setXml] = useState<string>(BPMN);

    const onXmlChanged = useCallback((
        newXml: string,
        newSvg: string | undefined,
        reason: ContentSavedReason
    ) => {
        console.log(`Model has been changed because of ${reason}`);
        // Do whatever you want here, save the XML and SVG in the backend etc.
        setXml(newXml);
    }, []);

    const onSaveClicked = useCallback(async () => {
        if (!modelerRef.current) {
            // Should actually never happen, but required for type safety
            return;
        }

        console.log("Saving model...");
        const result = await modelerRef.current.save();
        console.log("Saved model!", result.xml, result.svg);
    }, []);

    const onEvent = useCallback(async (event: Event<any>) => {
        if (isContentSavedEvent(event)) {
            // Content has been saved, e.g. because user edited the model or because he switched
            // from BPMN to XML.
            onXmlChanged(event.data.xml, event.data.svg, event.data.reason);
            return;
        }

        if (isNotificationEvent(event)) {
            // There's a notification the user is supposed to see, e.g. the model could not be
            // imported because it was invalid.
            return;
        }

        if (isUIUpdateRequiredEvent(event)) {
            // Something in the modeler has changed and the UI (e.g. menu) should be updated.
            // This happens when the user selects an element, for example.
            return;
        }

        if (isPropertiesPanelResizedEvent(event)) {
            // The user has resized the properties panel. You can save this value e.g. in local
            // storage to restore it on next load and pass it as initializing option.
            console.log(`Properties panel has been resized to ${event.data.width}`);
            return;
        }

        if (isBpmnIoEvent(event)) {
            // Just a regular bpmn-js event - actually lots of them
            return;
        }

        // eslint-disable-next-line no-console
        console.log("Unhandled event received", event);
    }, [onXmlChanged]);

    /**
     * ====
     * CAUTION: Using useMemo() is important to prevent additional render cycles!
     * ====
     */

    const xmlTabOptions = useMemo(() => ({
        className: undefined,
        disabled: undefined,
        monacoOptions: undefined
    }), []);

    const propertiesPanelOptions = useMemo(() => ({
        className: undefined,
        containerId: undefined,
        container: undefined,
        elementTemplates: undefined,
        hidden: undefined,
        size: {
            max: undefined,
            min: undefined,
            initial: undefined
        }
    }), []);

    const modelerOptions = useMemo(() => ({
        className: undefined,
        refs: [modelerRef],
        container: undefined,
        containerId: undefined,
        size: {
            max: undefined,
            min: undefined,
            initial: undefined
        }
    }), []);

    const bpmnJsOptions = useMemo(() => undefined, []);

    const modelerTabOptions = useMemo(() => ({
        className: undefined,
        disabled: undefined,
        bpmnJsOptions: bpmnJsOptions,
        modelerOptions: modelerOptions,
        propertiesPanelOptions: propertiesPanelOptions
    }), [bpmnJsOptions, modelerOptions, propertiesPanelOptions]);

    return (
        <div style={{
            height: "100vh"
        }}>

            <button
                onClick={onSaveClicked}
                style={{
                    position: "absolute",
                    zIndex: 100,
                    top: 25,
                    left: "calc(50% - 100px)",
                    textTransform: "none",
                    fontWeight: "bold",
                    minWidth: "200px",
                    minHeight: "40px",
                    backgroundColor: "yellow",
                    borderWidth: "1px",
                    borderRadius: "4px"
                }}>
                Save Diagram
            </button>

            <BpmnModeler
                xml={xml}
                onEvent={onEvent}
                xmlTabOptions={xmlTabOptions}
                modelerTabOptions={modelerTabOptions} />
        </div>
    );
}

export default App;

const BPMN = /* .... */;
```

## Usage with DMN

Usage with DMN is essentially the same. You just have to use the `<DmnModeler>` component instead. The API is very consistent between the two components.

# Contributing 🙌

Contributions are always welcome! If you have any ideas or suggestions, please feel free to open an issue or submit a pull request.