Introducing the @ngneat/dag Library
By: Preston Lamb
Tags: Directed Acyclic Graph, Angular

tldr;

If you need to build a workflow of some sort, a good way to organize the model is by using a directed acyclic graph. DAG models can be used to store a lot of information, and they can be somewhat complicated. Essentially, the data is represented by vertices (which I call nodes throughout this article) and edges. The edges connect one node to another, and go from earlier to later in the sequence. This model works perfectly for building a workflow, and in this article you’ll see how to use the @ngneat/dag library to manage a DAG model in your Angular application.

Library Installation and Setup

Before we get started, install the library in your Angular application:

OR

After the library is installed in the application, you’re ready to implement it in your application. The library consists of a service (which does all the heavy lifting) and an interface. The service is a little different than many services in Angular apps, and that’s because it’s not meant to be provided in the root of the application. When services are provided that way, they are instantiated as singletons, and the data in the service is available to any component or service in the application. In this case though, the DAG model doesn’t need to be persisted outside the life of the component used to build the workflow. So, to use the service, you need to include it in a component’s providers array:



import { DagManagerService } from ‘

({

selector: ‘app-workflow-builder’,

templateUrl: ‘’,

styleUrls: [],

providers: [DagManagerService]

}) // workflow-builder.component.tsimport { DagManagerService } from ‘ @ngneat/dag ’; @Component ({selector: ‘app-workflow-builder’,templateUrl: ‘’,styleUrls: [],providers: [DagManagerService]})

If you don’t remember this step, you’ll get an error in your application about there not being a provider for the service. After including the library in the providers array, the next step is to inject the service into your component with dependency injection, just like any other Angular service. Before doing that though, you need to define an interface or class that extends the DagModelItem interface from the library. The DagModelItem interface defines a few attributes that are required to be on each item in the DAG model array. The attributes on the interface are stepId , parentIds , and branchPath . The interface or class you define can have any other number of attributes you want:

// workflow-step.interface.ts

export interface WorkflowStep extends DagModelItem {

databaseId: number;

name: string;

data: any;

}

Then, back in the workflow builder component:



import { DagManagerService } from ‘

import { WorkflowStep } from ‘./workflow-step.interface.ts’; // workflow-builder.component.tsimport { DagManagerService } from ‘ @ngneat/dag ’;import { WorkflowStep } from ‘./workflow-step.interface.ts’; export class WorkflowBuilderComponent {

constructor(private _dag: DagMangerService<WorkflowStep>) {}

}

When you declare the service that you’re injecting in the constructor, you need to provide the interface or class that you defined for your DAG model inside the angle brackets. This helps the library have more information about the model by using generics. Again, if you don’t do this step, you’ll see errors in your application and IDE.

At this point, all the setup is complete and you’re ready to start using the library.

Database Storage

The DagManagerService converts an array of items into a two dimensional array that represents the DAG model for displaying it. The best way to store it in your database though is in a single dimensional array. Your backend data service should provide the workflows as a single dimensional array as well when loading the workflow. The DagManagerService can convert the single dimensional array to the two dimensional DAG display model, as well as converting that back to a single dimensional array for you.

ngOnInit() {

// Providing the service with items to convert to the DAG model

this._dagManager.setNewItemsArrayAsDagModel(this.startingItems); // Converting the DAG model to a single dimensional array

const itemsArray =

this._dagManager.getSingleDimensionalArrayFromModel();

}

These two arrays allow you to get the data from the server and provide the starting array to the service, as well as getting the latest model and flattening out the two dimensional array for sending to the database.

Creating and Managing a DAG Model

Now that the library service is installed and configured, you’re ready to start using it. The first thing you should do in the ngOnInit method of the component is to set the next stepId that the service will use when creating new steps. Each step in the DAG model should be unique. If you’re starting with a brand new workflow, the next number would be 1. If you’re loading a workflow, the best way to determine the next number is to loop over all the items that come back from the database and find the max value for stepId . Adding one to that max value will ensure that you don’t replicate stepId values. You can set the next number in the service like this:

// workflow-builder.component.ts

ngOnInit() {

const nextNumber = this.determineNextNumber();

this._dag.setNextNumber(nextNumber);

}

The next thing you should do after setting the next stepId number to use is to set the DAG model in the service. You can do that with any single dimensional array of WorkflowStep s like this:

ngOnInit() {

// this.startingItems can be an array retrieved from a backend

// or a new array if you’re creating a new workflow

this._dag.setNewItemsArrayAsDagModel(this.startingItems)

}

The service will then have an array which will be converted to the two dimensional DAG model and to which items can be added or from which they can be removed. You can gain access to the DAG model as an Observable from the service like this:

public dagModel$: Observable<WorkflowStep[][]>;

ngOnInit() {

this.dagModel$ = this._dag.dagModel$;

}

By using that Observable, your application, or UI, can update automatically any time an item is added or removed.

Speaking of adding items, there are two ways of adding items to the model. One of the ways automatically updates the observable, and the other way returns the items in a single dimension array. The easiest way is to have the service automatically update the observable. To add an item to the model, you can use the addNewStep method. This method takes 4 parameters:

the parentIds that the new item(s) are related to;

the number of new children to add;

the branch path to start with (this will likely almost always be 1);

and an object with all the attributes for a WorkflowStep except the attributes from DagModelItem .

Here’s an example:

doAddStep() {

this._dag.addNewStep([1], 1, 1, { name: ‘’, id: null });

}

The above function adds one new item with a branchPath of 1, with the parentId array containing a single number (meaning it’s a child of stepId 1). The name attribute on the new item will be an empty string, and the id will be null .

If you want to remove a node from the model, you only need to pass the ID of the item which needs to be removed. If you want to remove stepId 1, you can do so like this:

doAddStep() {

this._dag.removeStep(1);

}

Both of these methods will be really useful as you manage the model for your application. To read more about other ways to add or remove items from the model, check out the documentation. You can also play with the demo application to get a feel for how it works.

Tips for Displaying the Model

One of the hardest parts when I was creating this service was trying to figure out how to display the workflow on the page. I figured though that the more I could use default functionality in Angular (i.e. *ngFor ) and CSS (i.e. flexbox), the better. That’s the reasoning behind the dagModel$ observable being a two dimensional array. This made it so that all that I needed to do to output the model was to nest an *ngFor loop inside another *ngFor loop. The first loop outputs each row in the model, working from top down. The second loop outputs the columns, from left to right. You can see a detailed example of this in the demo app in the repository.

In addition, you can use the leader-line package to draw lines between nodes to show the flow of the model. You can read this article to learn how to do that, and also check the above mentioned demo app. The key is that each time the model is updated, or more items are output to the screen, all the leader lines are removed and then redrawn. Again, the demo app will help you see how to do this.

Conclusion

I’m really excited about the possibilities for this library. We’re using it on a project at work, but wanted it open sourced so that we could get some help from the community and get some ideas on what can be added. Check it out, try it, and submit feedback. Hopefully it helps out in your project as well!