DEV Community

Preston Lamb
Preston Lamb

Posted on • Originally published at Medium on

Introducing the @ngneat/dag Library

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(#library-installation-setup)

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

$ npm install @ngneat/dag
Enter fullscreen mode Exit fullscreen mode

OR

$ yarn add @ngneat/dag
Enter fullscreen mode Exit fullscreen mode

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:

// workflow-builder.component.ts
import { DagManagerService } from '@ngneat/dag';

@Component({
  selector: 'app-workflow-builder',
  templateUrl: '',
  styleUrls: [],
  providers: [DagManagerService]
})
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Then, back in the workflow builder component:

// workflow-builder.component.ts
import { DagManagerService } from '@ngneat/dag';
import { WorkflowStep } from './workflow-step.interface.ts';

export class WorkflowBuilderComponent {
  constructor(private _dag: DagMangerService<WorkflowStep>) {}
}
Enter fullscreen mode Exit fullscreen mode

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, you're ready to get into using the library, and all the setup is done.

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();
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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 WorkflowSteps 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)
}
Enter fullscreen mode Exit fullscreen mode

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 Observable from the service like this:

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

ngOnInit() {
  this.dagModel$ = this._dag.dagModel$;
}
Enter fullscreen mode Exit fullscreen mode

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 WorkflowStep except the attributes from DagModelItem. Here's an example:
doAddStep() {
  this._dag.addNewStep([1], 1, 1, { name: '', id: null });
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)