Skip to main content

One post tagged with "jsplumb"

View All Tags

· 8 min read
Simon Porritt

Angular's new(ish) Signals system is a great addition to their library, and one which we're keeping an eye on with a view to updating JsPlumb's Angular integration to help you make the best use of it.

Signals were introduced in Angular 16 and have been improved upon in Angular 17. We know that upgrading Angular versions is not something people take lightly, so for the time being we are focusing on what Angular 16 offers, and we'll be looking at ways of releasing support in JsPlumb.

Right now, though - if you're already using Angular 16 or 17 - we thought you might like to see how you can take advantage of signals with the current version of JsPlumb.

In the demonstration below we have 3 nodes that each have a name and a list of values. We render these with an Angular component called NodeComponent, whose template uses a computed property based on a signal to write out each node's current list of values - try clicking the Add a value button on one of these nodes:

In this article we're going to take a look at the steps involved in supporting this. If you just want to cut to the chase and download some code, it's available as a standalone Angular app on Github here

Node component

Let's start with the node component's template:

<div class="jtk-signals-node" style="display: flex;flex-direction: column;">
<div class="jtk-signals-delete" (click)="this.removeNode()"></div>
<div>{{obj['name']}}</div>
<div>{{valueList()}}</div>
<button (click)="addValue()" style="margin-top:0.5rem">Add a value</button>
</div>

The key thing to note here is {{valueList()}} - valueList is a computed property that is based on a signal.

Configuring a signal

The first thing we do is to declare a signal inside our node component:

import { signal, computed, OnInit, Component } from "@angular/core"
import { BaseNodeComponent } from "@jsplumbtoolkit/browser-ui-angular"

export class NodeComponent extends BaseNodeComponent implements OnInit {

//
// Declare a signal at the component level, and give it type (which is optional - you could use ObjectData, JsPlumb's default,
// or you could of course also use `any`...but we didn't suggest that!)
//
readonly objSignal = signal<MyDataObject>(undefined!);

}
info

MyDataObject is an interface created for this demonstration that defines the data object backing each node. In this case we have:

interface MyDataObject {
name:string
values:Array<number>
}

Configuring a computed property

Now that we have a signal we can create a property whose value will be computed from it. As shown above, our computed property is called valueList, and its a comma delimited dump of the node's values:

//
// This is a computed value which will be updated whenever `objSignal` gets updated. Here we concat the list of values
// in this node to a comma delimited string.
//
readonly valueList = computed( () =>
this.objSignal().values.join(",")
);

Initialize the signal

We've created our signal with no start value, so we're going to give it out start value inside ngOnInit:

ngOnInit() {
this.objSignal.set(this.obj as MyDataObject);
}

This setup would be sufficient to do an initial node render with the values array from our computed property. But now we need to ensure the signal stays current.

Updating the signal

The key here is our node component is going to listen to an update event on the underlying Toolkit, and when it receives an event for the node it is managing, it will update the signal. The setup for this comes in 3 parts:

Define an update listener

//
// Listens for an update event on the Toolkit. When the updated vertex's ID matches this component's vertex's id, we
// set a new value on `objSignal`.
//
private _signalBinding(n:{vertex:Node}) {
if (n.vertex.id === this.obj['id']) {
console.log(`Updating signal binder for vertex ${this.obj['id']}`)
this.objSignal.set(this.obj as MyDataObject)
}
}

We define this private _signalBinding method on our class, which takes an update payload as argument. If the node being updated is the same as the node this component is mapped to, then the objSignal is set with a new value. Angular will propagate this change to any computed properties, such as our valueList property.

Bind the update listener to the class

This step may seem counter-intuitive but bear with us. We're going to declare another private class level member and assign our update listener to it:

//
// Declare a class-level handler for JsPlumb's node update event. This is necessary in order to maintain the `this`
// reference correctly, and to be able to unbind the handler when this component is destroyed.
//
private _signalBinder!:(params:{vertex:Node}) => any

Register the update listener with the Toolkit

Now we can update our ngOnInit to assign a value to __signalBinder and bind that function to our Toolkit:

//
// In the init method we do three things:
//
// 1. Set the initial value on our signal
// 2. Store a reference to our update handler to a function bound to this class.
// 3. Register our bound handler on the Toolkit to listen for updates to this vertex
//
ngOnInit() {
this.objSignal.set(this.obj as MyDataObject);
this._signalBinder = this._signalBinding.bind(this)
this.toolkit.bind(EVENT_NODE_UPDATED, this._signalBinder)
}

There are two main reasons for this two-step approach to binding the update listener. Firstly, without it, the code will "lose" the this reference and be unable to access other class members, so that's kind of a show-stopper. But secondly, binding in this way lets us setup a nice way to cleanup if this component gets destroyed.

Cleaning up

If this component gets destroyed for some reason - which can happen via Angular maybe if a route changes or its parent gets unloaded, but can also happen when the related node is removed from the Toolkit - we're going to want to unbind our event listener from the Toolkit:

//
// When the component is destroyed, unbind the update listener.
//
override ngOnDestroy() {
console.log(`Destroy - unbinding signal binder for vertex ${this.obj['id']}`)
this.toolkit.unbind(EVENT_NODE_UPDATED, this._signalBinder)
super.ngOnDestroy()
}

Updating the value list

As mentioned above, when the user presses Add a value we invoke a method on the node component:

//
// When the user clicks Add a value, add a new value to the vertex's `values` array and update the object in the Toolkit.
// Our update listener will pick this up and update the signal, and Angular will handle the rest.
//
addValue() {
const newValues = this.obj['values'].slice()
newValues.push(Math.floor(Math.random() * 150))
this.updateNode({
values:newValues
})
}

The key thing to note here is that this method is unaware of the existence of the signal - it operates in the same way that methods that want to update the Toolkit always have done. It's the code above that provides the link between the Toolkit and the signal and its computed properties.

The code in full

This is the full code for the node component used in this demonstration:

import { signal, computed, OnInit, Component } from "@angular/core"
import { Node, EVENT_NODE_UPDATED } from "@jsplumbtoolkit/browser-ui"
import { BaseNodeComponent } from "@jsplumbtoolkit/browser-ui-angular"

interface MyDataObject {
name:string
values:Array<number>
}

@Component({
template:`<div class="jtk-signals-node" style="display: flex;flex-direction: column;">
<div class="jtk-signals-delete" (click)="this.removeNode()"></div>
<div>{{obj['name']}}</div>
<div>{{valueList()}}</div>
<button (click)="addValue()" style="margin-top:0.5rem">Add a value</button>
</div>`
})
export class NodeComponent extends BaseNodeComponent implements OnInit {

//
// Declare a signal at the component level, and give it type (which is optional - you could use ObjectData, JsPlumb's default,
// or you could of course also use `any`...but we didn't suggest that!)
//
readonly objSignal = signal<MyDataObject>(undefined!);

//
// This is a computed value which will be updated whenever `objSignal` gets updated. Here we concat the list of values
// in this node to a comma delimited string.
//
readonly valueList = computed( () =>
this.objSignal().values.join(",")
);

//
// Declare a class-level handler for JsPlumb's node update event. This is necessary in order to maintain the `this`
// reference correctly, and to be able to unbind the handler when this component is destroyed.
//
private _signalBinder!:(params:{vertex:Node}) => any

//
// Listens for an update event on the Toolkit. When the updated vertex's ID matches this component's vertex's id, we
// set a new value on `objSignal`.
//
private _signalBinding(n:{vertex:Node}) {
if (n.vertex.id === this.obj['id']) {
console.log(`Updating signal binder for vertex ${this.obj['id']}`)
this.objSignal.set(this.obj as MyDataObject)
}
}

//
// In the init method we do three things:
//
// 1. Set the initial value on our signal
// 2. Store a reference to our update handler to a function bound to this class.
// 3. Register our bound handler on the Toolkit to listen for updates to this vertex
//
ngOnInit() {
this.objSignal.set(this.obj as MyDataObject);
this._signalBinder = this._signalBinding.bind(this)
this.toolkit.bind(EVENT_NODE_UPDATED, this._signalBinder)
}

//
// When the component is destroyed, unbind the update listener.
//
override ngOnDestroy() {
console.log(`Destroy - unbinding signal binder for vertex ${this.obj['id']}`)
this.toolkit.unbind(EVENT_NODE_UPDATED, this._signalBinder)
super.ngOnDestroy()
}

//
// When the user clicks Add a value, add a new value to the vertex's `values` array and update the object in the Toolkit.
// Our update listener will pick this up and update the signal, and Angular will handle the rest.
//
addValue() {
const newValues = this.obj['values'].slice()
newValues.push(Math.floor(Math.random() * 150))
this.updateNode({
values:newValues
})
}
}

Further reading


Start a free trial

Not a user of jsPlumb but thinking of checking it out? There's a whole lot more to discover and it's a great time to get started!


Get in touch!

If you'd like to discuss any of the ideas/concepts in this article we'd love to hear from you - drop us a line at hello@jsplumbtoolkit.com.