Workflow editor in Angular and Dagre

Szilard Peteri
5 min readJul 12, 2021

--

Note: when i started writing this article (10th of July in 2021) Dagre seems to became deprecated. That does not mean that anything I wrote about it is deprecated or the project itself is dead.

The web became the major platform for application development even for complex systems. When you start a project especially a SaaS one the number one platform is the web.

We thought it the same way when started creating RequestLogic, our email marketing platform this year. We know that automated processes are a must-have feature for marketing software and visual editing of these workflows is crucial as well.

Our platform is built with Angular as we have plenty of years of experience with it since version 2 became public. I personally like the way it works and for non-performance-heavy applications its data binding is magical.

Creating a graph-based editor is easier than ever. There are several tools to choose from. I am familiar with the Dagre and D3 combo as I used it in a custom visual development for Power BI. As angular is capable of working with SVG the same way as with HTML documents the use of D3 is unnecessary unless there are performance issues. As it is more “angulary” to define the view in a template and define the actual data in the component code with data binding.

The only difference you have to take care of is the way to data bind SVG attributes. The ‘attr.’ prefix is needed for SVG attributes.

SVG with Angular

Here is a simple SVG example in angular:

<div>
<svg xmlns:xhtml="http://www.w3.org/1999/xhtml"
width="800" height="400">
<g *ngFor="let edge of edges">
<path class="edge" [attr.d]="edge.path"></path>
</g>
<g *ngFor=”let box of boxes”>
<rect [attr.x]="box.x" [attr.y]="box.y"
[attr.width]="box.width" [attr.height]="box.height"/>
<text [attr.x]="box.x + 10" [attr.y]="box.y + 24">
{{ box.label }}
</text>
</g>
</svg>
</div>

Example-svg.component.scss:

.edge {
fill: none;
stroke: gray;
stroke-width: 2px;
}
.box {
fill: lightblue;
stroke: gray;
stroke-width: 2px;
transition: fill 0.2s;
}
.box:hover {
fill: beige;
}

Example-svg.component.ts:

import { Component } from "@angular/core";export class Box {
constructor(
public x: number,
public y: number,
public width: number,
public height: number) {
}
}
export interface IPoint {
x: number;
y: number;
}
export class Edge {
constructor(private points: IPoint[]) {
}
public get path(): string {
if (this.points.length < 2) return "";
var result = "M ";
this.points.forEach(pt => {
result += `${pt.x} ${pt.y} L`;
});
return result.substr(0, result.length - 2);
}
}
@Component({
selector: 'example-svg',
templateUrl: './example-svg.component.html',
styleUrls: ['./example-svg.component.scss']
})
export class ExampleSvgComponent {

boxes: Box[] = [];
edges: Edge[] = [];

constructor() {
this.boxes.push(new Box(120, 20, 200, 40));
this.boxes.push(new Box(10, 100, 200, 40));
this.boxes.push(new Box(240, 100, 200, 40));

this.edges.push(new Edge([{x: 220, y: 40}, {x: 110, y: 120}]));
this.edges.push(new Edge([{x: 220, y: 40}, {x: 340, y: 120}]));
}
}

I think there is not much explanation needed as there is only a simple array of boxes and an array of edges that act as the ViewModel. The SVG part in the template (View) creates a group node with a rectangle for all the boxes where the rectangle positions are coming from data-binding. The edges are path elements that connect the center of the boxes.

The example-svg-component should end up looking like this.

This is really simple. The harder part that I leave to you is the way you figure out the width and height of the SVG area itself.

Till now we just hard-coded the box positions. To make it a bit data-driven we have to delegate the layout creation job to a clever algorithm that Dagre can provide us.

When Dagre steps in

Let’s start creating a logical graph first. To do that we have to specify the parent-child relationship between the nodes. In our case let assume a DAG when every node can have a parent node. In this case, specifying the parent id of each node is enough to define the graph. (Dagre can handle non-DAG graphs as well by reducing the graph to a DAG, rendering the layout for the DAG, and then inserting the removed edges. Working with DAG-s is much easier for many algorithms.)

Here is an example code for a tree that is specified by Node objects:

export class Node {
constructor(
public id: string,
public parentId: string
) {
}
}
var nodes = [];
nodes.push(new Node("root", null));
nodes.push(new Node("Node A", "root"));
nodes.push(new Node("Node B", "root"));
nodes.push(new Node("Node C", "root"));
nodes.push(new Node("Node D", "Node C"));

Add Dagre with npm to the project:

npm add dagre

Now to create a list of boxes with the correct 2d layout we need to feed the graph structure to Dagre.

import * as dagre from "dagre";...var g = new dagre.graphlib.Graph();
g.setGraph({});
nodes.forEach(node => {
g.setNode(node.id, { width: 200, height: 40});
if (node.parentId != null) {
g.setEdge(node.parentId, node.id, {});
}
});

Finally, we have the rendered layout in Dagre we just need to convert them to our list of boxes.

dagre.layout(g);this.boxes = [];
g.nodes().forEach(nodeId => {
var node = g.node(nodeId);
var box = new Box {
x: node.x,
y: node.y,
width: node.width,
height: node.height,
label: nodeId
};
});
this.edges = [];
g.edges().forEach(edge => {
this.edges.push(new Edge(g.edge(e).points));
});

As you see we do not need to change anything in the View and the ViewModel part of the code, only the way we create the ViewModel is different. Feeding Dagre with the nodes and edges then generate the layout with the dagre.layout call. Next, we can read the positions of the nodes through the g.nodes() list and the edge definitions through the g.edges() list.

The end result should look similar to this:

Dagre powered layout of Box objects

So we created some graph rendering that is MVVM (Model-View-ViewModel) based and flexible enough to improve it with additional features.

We are at the end of the article but where is the promised workflow editor? It is just a rendered graph without any interactivity.

If this article produces enough claps then part two will come sooner!

--

--