Diagram Maker – Panels and Declarative DOM API

2021.10.15

Introduction

In previous post we tried Action Interceptor to modify, cancel or fire another action with Diagram Maker. Today we'll focus more on GUI part and add Panels. Panels are the UI components that contain some tools and functionalities to manipulate workspace.

Adding panels

In previous post when working with action interceptor, we implemented our logic in configuration object. To add panels we need to implement code that will also go into configuration object but in property called renderCallbacks. From documentation about renderCallbacks: Callbacks to render different parts of the diagram maker UI: node, edge panel....

mounted () {
    this.diagram = new DiagramMaker(
      this.$refs['diagramRoot'],
      {
        renderCallbacks: {
          node: (node, container) => {
            const newDiv = document.createElement('div')
            newDiv.classList.add('rectangle', 'example-node')
            return newDiv
          },
          panels: {
              #1                                        #2
              nodes_panel: (panel, state, container) => addNodesPanel(container)
          }
        },
        actionInterceptor: (action, dispatch, getState) => {
          const state = getState()
          dispatch(action)
        }
      },
      {
          initialData: {
              nodes: [nodes...],
              edges: [edges...],
              #3
              panels: {
                  'nodes_panel': {
                      id: `nodes_panel`,
                      position: { x: 2, y: 2 },
                      size: { width: 100, height: 200 }
                      positionAnchor: PositionAnchor.TOP_LEFT
                  }
              }
          }
      }
    )
}
methods () {
    addNodesPanel (container) {
        ...
        return DOM
    }
}

Each render callback has its own property. We are adding panels callbacks that is a map of the (#1) panel's id and a (#2) function that creates DOM elements. To render a panel properly we also need to add configuration object to initialData of 3rd argument used to initialize the diagram. In that object the id (nodes_panel) must match the id in renderCallbacks. In the configuration object we specify its position, size and positionAnchor.

positionAnchor is from which corner of the panel the position and size will be rendered. We can choose from following options: TOP_LEFT TOP_RIGHT BOTTOM_LEFT BOTTOM_RIGHT

Above is a very basic example that will render a panel in top left corner with size of 100px width and 200px height.

Render function

Below is a function that will build DOM elements of our panel. The div.nodes-container is where we will put our nodes. After that we create another one that will render a node. Both on container and a node we add attributes like data-*. These are the declarative DOM APIs. In Diagram-Maker they allow you to add data attributes to the DOM elements you render and avoid having to detect user interaction and fire actions yourself.

Important attributes are data-type and data-id, first specifies it is a potential node and second, node's type. The type is a type of node that will be added onto the workspace, more on it later.

addNodesPanel (container) {
  if (container.innerHTML !== '') {
      return
  }
  const div = document.createElement('div')
  div.classList.add('nodes-container')
  div.setAttribute('data-event-target', 'true')
  div.setAttribute('data-dropzone', 'true')
  const node = document.createElement('div')
  node.classList.add('node')
  node.innerText = 'example node'
  node.setAttribute('data-id', 'test-node')
  node.setAttribute('data-type', 'DiagramMaker.PotentialNode')
  node.setAttribute('data-draggable', 'true')
  node.setAttribute('data-event-target', 'true')
  node.setAttribute('data-value', 1)
  div.appendChild(node)

  container.appendChild(div)
  return div
}

Our panel has a single example-node that can be dragged into the workspace. Doing so fires chain of actions including dragging potential node and creating a new node. The latter looks like this:

{
    "type": "NODE_CREATE",
    "payload": {
        "id": "dm-node-fe52bcba-e7f9-4735-8f96-428e22ea5025",
        "typeId": "test-node",
        "position": {
            "x": -109.453125,
            "y": 65.15625
        },
        "size": {
            "width": 150,
            "height": 50
        }
    }
}

This is a "NODE_CREATE" action that has typeId of 'test-node' (node.setAttribute('data-id', 'test-node')). id and position payload are generated for us. The size part of payload is a size that we can configure ourselves. It happens in cofiguration object parameter of diagram initialization, at the same level as actionInterceptor and renderCallbacks and is called nodeTypeConfig.

mounted () {
    this.diagram = new DiagramMaker(
      this.$refs['diagramRoot'],
      {
        renderCallbacks: {},
        actionInterceptor: (action, dispatch, getState) => {},
        nodeTypeConfig: {
            'test-node': {
                size: { width: 150, height: 50 },
                visibleConnectorTypes: VisibleConnectorTypes.TOP_BOTTOM
            }
        }
      },
    )
}

Conclusion

We have discovered a way to create and render UI elements called panels into our workspace. By utilizing declarative DOM APIs we added a panel allowing us to drag-and-drop nodes into the canvas. Declarative DOM APIs save us struggle of detecting user's interaction and passing all required data to fire all the actions we need to add single node onto the canvas.