Yesterday we documented the process of building a chatbot app in 3 hours and 37 minutes. We wrote that app in "vanilla" JS but we know one of the features of the Toolkit that people love is its deep integration with libraries such as Angular, Vue, React and Svelte. In today's post we're going to look at porting the vanilla chatbot to Angular.
Initialization
ng new angular-chatbot
cd angular-chatbot
npm i @jsplumbtoolkit/browser-ui-angular
This gives us a brand new Angular app, and we've imported the Toolkit's Angular integration package.
Import the Toolkit's Angular module
The first step is to update app-module.ts
and import the Toolkit:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { jsPlumbToolkitModule} from "@jsplumbtoolkit/browser-ui-angular"
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
jsPlumbToolkitModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Setup the html and css
Next, we're going to clear out app.component.html
and copy in the HTML from our vanilla chatbot:
<div class="jtk-demo-main" id="jtk-demo-chatbot">
<!-- main drawing area -->
<div class="jtk-demo-canvas">
<!-- controls -->
<div class="jtk-controls-container"></div>
<!-- miniview -->
<div class="miniview"></div>
</div>
<div class="jtk-demo-rhs">
<!-- the node palette -->
<div class="sidebar node-palette">
<div class="jtk-chatbot-palette-item" data-type="start">
start
</div>
<div class="jtk-chatbot-palette-item" data-type="end">
end
</div>
<div class="jtk-chatbot-palette-item" data-type="message">
message
</div>
<div class="jtk-chatbot-palette-item" data-type="input">
input
</div>
<div class="jtk-chatbot-palette-item" data-type="choice">
choice
</div>
</div>
<!-- node/edge inspector -->
<div class="inspector"></div>
</div>
</div>
We've got placeholders for a controls component, miniview, inspector and node palette in this HTML. We'll be replacing each of those elements in turn, as with the Toolkit's Angular integration there is an Angular component available for each of these things.
CSS
In styles.css
we added these few lines:
@import "../node_modules/@jsplumbtoolkit/browser-ui/css/jsplumbtoolkit.css";
@import "../node_modules/@jsplumbtoolkit/browser-ui/css/jsplumbtoolkit-controls.css";
jsplumb-surface, .jtk-surface {
width:100%;
height:100%;
}
The first two import the Toolkit defaults stylesheet and the stylesheet for the controls component. The jsplumb-surface, .jtk-surface
rule ensures that the Surface component fills the available width and height.
We then copied everything from the vanilla chatbot's app.css
to styles.css
.
Copy the dataset
In our final take of the vanilla chatbot we had an example JSON dataset, so we're copying that into /src/assets
.
Create node components
When you use the Toolkit's Angular integration, you create a component to represent each node type - or one component representing several, it's up to you. Since it's a straightforward process we're just going to create one component for each type from our vanilla chatbot - START, END, MESSAGE, INPUT and CHOICE.
Here's what the MESSAGE component code looks like:
import {Component} from "@angular/core"
import {BaseNodeComponent} from "@jsplumbtoolkit/browser-ui-angular"
@Component({
template:`<div class="jtk-chatbot-message" data-jtk-target="true">
<div class="jtk-delete" (click)="removeNode()"></div>
{{obj['message']}}
<div class="connect" data-jtk-source="true"></div>
</div>`
})
export class MessageComponent extends BaseNodeComponent { }
The template was ported pretty much directly from the vanilla chatbot's message component template, but note the jtk-delete
element: it has a click listener on it, which calls removeNode()
, which is a method exposed on BaseNodeComponent
in the Toolkit. When you render a node via a component in the Toolkit's Angular integration there are several convenience methods available that can access the underlying model object.
We repeated this process for the other node types.
Configure app component
We made a few updates to AppComponent to get it ready for rendering to a Surface:
@ViewChild(jsPlumbSurfaceComponent) surfaceComponent!: jsPlumbSurfaceComponent
toolkit!: BrowserUIAngular
surface!: Surface
constructor(public $jsplumb:jsPlumbService) { }
ngAfterViewInit() {
this.surface = this.surfaceComponent.surface
this.toolkit = this.surfaceComponent.toolkit
this.toolkit.load({
url:'/assets/dataset.json'
})
}
}
Specifically:
- We inject the jsplumb service into the constructor. The jsplumb service has a number of useful "global" methods.
- We declared a surface component view child, that we will retrieve in our afterViewInit
- We declared an
ngAfterViewInit
, which gets a reference to the underlying surface and the toolkit, and then loads our dataset.
Add surface component
The Toolkit's angular integration offers a Surface component that you can embed in your template, which takes care of instantiating a Toolkit instance and rendering it to a Surface. We added this to our app component's html:
<jsplumb-surface surfaceId="surface"
toolkitId="toolkit"
[toolkitParams]="toolkitParams"
[renderParams]="renderParams"
[view]="view"></jsplumb-surface>
We had to pass it a few things:
- surfaceId A unique ID for this surface. Only a consideration if you have more than one Surface in your app
- toolkitId A unique ID for this Toolkit instance. Only a consideration if you have more than one Toolkit in your app
- toolkitParams Options for the Toolkit constructor. In this app we want to tell the Toolkit the name of the property that identifies a node's ports (see below)
- renderParams These are things such as layout, defaults, whether to zoom to fit after data load, etc. They are declared on
AppComponent
- we'll show them below. - view This is the mapping from node/edge/port types to components, behaviour and appearance.
toolkitParams
portDataProperty
tells the Toolkit the name of the property that identifies a node's ports, and its inclusion here allows us to use the Toolkit's addPort
and removePort
methods.
toolkitParams = {
portDataProperty:"choices"
}
renderParams
We copied these from the vanilla chatbot's render
call:
renderParams:AngularRenderOptions = {
zoomToFit:true,
consumeRightClick:false,
defaults:{
endpoint:BlankEndpoint.type,
anchor:AnchorLocations.Continuous
},
layout:{
type:AbsoluteLayout.type
}
}
view
The full view for this app looks like this:
view = {
nodes:{
[SELECTABLE]:{
events:{
[EVENT_TAP]:(p:{obj:Base}) => {
this.toolkit.setSelection(p.obj)
}
}
},
[START]:{
parent:SELECTABLE,
component:StartComponent
},
[END]:{
parent:SELECTABLE,
component:EndComponent
},
[ACTION_MESSAGE]:{
parent:SELECTABLE,
component:MessageComponent
},
[ACTION_INPUT]:{
parent:SELECTABLE,
component:InputComponent
},
[ACTION_CHOICE]:{
parent:SELECTABLE,
component:ChoiceComponent
}
},
edges:{
default:{
overlays:[
{
type:PlainArrowOverlay.type,
options:{
location:1,
width:10,
length:10
}
}
],
label:"{{label}}",
useHTMLLabel:true,
events:{
[EVENT_TAP]:(p:{edge:Edge}) => {
this.toolkit.setSelection(p.edge)
}
}
}
},
ports:{
choice:{
anchor:[AnchorLocations.Left, AnchorLocations.Right ]
}
}
}
The edge and port definitions are identical to those from the vanilla chatbot. The nodes follow the same structure - a parent which responds to a tap event, and a mapping for each type - but instead of supplying a template for each node type in Angular we supply a component.
At this point the app is up and running and it renders a dataset:
But it doesnt have the controls component, and you cannot yet drag new nodes, inspect nodes or add/remove choices.
Controls component
It is straightforward to add a controls component - there's one available in the Angular integration. We just replace this:
<div class="jtk-controls-container"></div>
with this:
<jsplumb-controls surfaceId="surface"></jsplumb-controls>
Dragging new nodes
This is also easy to configure, via the jsplumb-surface-drop
component. We replace this:
<div class="sidebar node-palette">
...
</div>
with:
<div class="sidebar node-palette"
jsplumb-surface-drop
selector=".jtk-chatbot-palette-item"
surfaceId="surface"
[dataGenerator]="dataGenerator">
<div *ngFor="let nodeType of nodeTypes" class="jtk-chatbot-palette-item" [attr.data-type]="nodeType.type" title="Drag to add new">{{nodeType.label}}</div>
</div>
We need to provide a couple of things to this code - an array called nodeTypes, and a dataGenerator
, which is responsible for extracting the initial payload from a node that is being dragged. We'll copy that from the vanilla chatbot:
dataGenerator(el:Element) {
const type = el.getAttribute("data-type")
const base = { type }
if (type === ACTION_MESSAGE) {
Object.assign(base, { message:"Send a message"})
} else if (type === ACTION_INPUT) {
Object.assign(base, { message:"Grab some input", prompt:"please enter input"})
} else if (type === ACTION_CHOICE) {
Object.assign(base, {
message:"Make a selection!",
choices:[
{ id:"1", label:"Choice 1"},
{ id:"2", label:"Choice 2"},
]
})
}
return base
}
for nodeTypes
we have:
nodeTypes = [
{type:START, label:"Start"},
{type:END, label:"End"},
{type:ACTION_MESSAGE, label:"Message"},
{type:ACTION_INPUT, label:"Input"},
{type:ACTION_CHOICE, label:"Choice"}
]
Add/remove/inspect choices
In the vanilla app we handled the addition or removal of choices via the modelEvents
argument we passed to the render
call. In Angular we can take advantage of the fact that each node is a component to isolate this behaviour - in this case, to the Choice component. So we'll add a few methods to it:
addChoice() {
this.toolkit.setSelection(this.toolkit.addPort(this.getNode(), {
id:uuid(),
label:"Choice"
}))
}
removeChoice(id:string) {
this.toolkit.removePort(this.getNode(), id)
}
inspectChoice(id:string) {
this.toolkit.setSelection(this.getNode().getPort(id))
}
toolkit
is a class member of BaseNodeComponent
. We updated the choice node's template with click handlers to invoke these methods:
<div class="jtk-chatbot-choice" data-jtk-target="true">
<div class="jtk-delete" (click)="removeNode()"></div>
{{obj['message']}}
<div class="jtk-choice-add" (click)="addChoice()"></div>
<div class="jtk-chatbot-choice-option" *ngFor="let choice of obj['choices']"
data-jtk-source="true"
data-jtk-port-type="choice"
[attr.data-jtk-port]="choice['id']"
(click)="inspectChoice(choice.id)">
{{choice['label']}}
<div class="jtk-choice-delete" (click)="removeChoice(choice.id)"></div>
</div>
</div>
Inspecting nodes
The final piece we need to port is the node inspector. Again we can copy the templates - for the most part - from the vanilla chatbot. We create an angular component for it:
export class InspectorComponent implements AfterViewInit {
currentType:string = ''
@Input() surfaceId:string
// @ts-ignore
inspector:Inspector
CHOICE_PORT = "choice-port"
EDGE = "edge"
constructor(private $jsplumb:jsPlumbService, private el:ElementRef, private changeDetector:ChangeDetectorRef) { }
ngAfterViewInit(): void {
this.$jsplumb.getSurface(this.surfaceId, (surface) => {
this.inspector = new Inspector({
container:this.el.nativeElement,
surface,
renderEmptyContainer:() => {
this.currentType = ''
},
refresh:(obj:Base, cb:() => void) => {
this.currentType = isNode(obj) ? obj.type : isPort(obj) ? this.CHOICE_PORT : this.EDGE
setTimeout(cb, 0)
this.changeDetector.detectChanges()
}
})
})
}
}
This component maintains a currentType class member, which it sets based upon whatever is passed in to the refresh
method we pass to the Inspector. After setting the current type we tell the change detector to detect changes, ie. to repaint the inspector component.
The template for this component writes out different HTML based on currentType :
<div *ngIf="currentType === ''"></div>
<div *ngIf='currentType === "${START}"'></div>
<div *ngIf='currentType === "${END}"'></div>
<div *ngIf='currentType === "${ACTION_MESSAGE}"' class="jtk-chatbot-inspector">
<span>Message:</span>
<input type="text" jtk-att="message" placeholder="message"/>
</div>
<div *ngIf='currentType === "${ACTION_INPUT}"' class="jtk-chatbot-inspector">
<span>Message:</span>
<input type="text" jtk-att="${PROPERTY_MESSAGE}" placeholder="message"/>
<span>Prompt:</span>
<input type="text" jtk-att="${PROPERTY_PROMPT}" placeholder="prompt"/>
</div>
<div *ngIf='currentType === "${ACTION_CHOICE}"' class="jtk-chatbot-inspector">
<span>Message:</span>
<input type="text" jtk-att="${PROPERTY_MESSAGE}" placeholder="message"/>
</div>
<div *ngIf="currentType === CHOICE_PORT" class="jtk-chatbot-inspector">
<span>Label:</span>
<input type="text" jtk-att="${PROPERTY_LABEL}" jtk-focus placeholder="enter label..."/>
</div>
<div *ngIf="currentType === EDGE" class="jtk-chatbot-inspector">
<div>Label</div>
<input type="text" jtk-att="${PROPERTY_LABEL}"/>
</div>
Final thoughts
That's the whole thing ported now. We reused the vast majority of the Toolkit setup code, HTML and CSS from the vanilla chatbot, but we used Angular components to render each node, which enabled us to isolate the code specific to each node - deletion of a node, for all node types, and also adding/removing/inspecting choices. The Toolkit is a powerful library on its own, but when coupled with a library integration it becomes even more so.
You can find the code for this in apps/chatbot/angular
at https://github.com/jsplumb/jsplumbtoolkit-applications
Start a free trial
Get in touch!
The jsPlumb Toolkit is a very versatile library with a rich feature set. Making these starter applications is a chance for us to demonstrate and illustrate how it might look for your company. 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.