Svelte Integration
From version 5.6.0, the Toolkit offers integration with Svelte 3. We invite all feedback and suggestions from Svelte users for ways to take this initial integration and make it more useful.
Imports
The Svelte integration ships in the package @jsplumbtoolkit/browser-ui-svelte
.
Setup
The Toolkit's Svelte integration offers a wrapper around the Surface
component. Let's take a quick look at how you'd use one. We'll declare an App
Svelte component that creates an instance of the Toolkit and renders it to a Surface:
<script>
import { newInstance, SurfaceComponent } from '@jsplumbtoolkit/browser-ui-svelte'
import { AbsoluteLayout } from '@jsplumbtoolkit/core'
import MyNodeComponent from './MyNodeComponent.svelte'
const toolkit = newInstance()
const viewParams = {
nodes:{
default:{
component:MyNodeComponent
}
}
}
const renderParams = {
layout:{
type:AbsoluteLayout.type
}
}
</script>
<div>
<SurfaceComponent viewParams={viewParams}
renderParams={renderParams}
toolkit={toolkit}/>
</div>
We create an instance of the Toolkit in the App
component, and setup some parameters for our view and for configuring the Surface. In our viewParams
we map an imported component MyNodeComponent
to the type "default", meaning that component will be used to render all the nodes in this app. One of the strengths of the Toolkit is of course the ability to declaratively map different components to different node types, but for the purposes of getting going here we just have the one.
MyNodeComponent
could be any valid Svelte component. We'll use a simple component that illustrates how to render information from a node's backing data:
<script>
export let data;
export let toolkit;
export let surface;
export let vertex;
</script>
<div class="my-component">
<h3>{data.id}</h3>
</div>
Here, MyNodeComponent
exposes four props that the Toolkit will attempt to set:
data
- The node's backing datatoolkit
- The underlying toolkit instancesurface
- The Surface that is rendering this componentvertex
- The model object representing the node being rendered
You don't have to expose these props on your components, but the Toolkit will attempt to set them, and you'll almost certainly want to access, at the very least, data
. Having access to the Toolkit instance and vertex
is also very useful - imagine, for instance, you wanted to add a "delete" button to this component. We could rewrite it to be:
<script>
export let data;
export let toolkit;
export let surface;
export let vertex;
function remove() {
toolkit.removeNode(vertex)
}
</script>
<div class="my-component">
<h3>{data.id}</h3>
<button on:click|preventDefault={remove}>X</button>
</div>
Remember, you need to export
a prop in order for Svelte to set a value on it.
Components
We've seen the SurfaceComponent
in action above, and in other Toolkit integrations such as React/Angular/Vue, there are components offered to let you add a miniview, or to support drag and drop of new nodes. The Svelte integration currently does not offer component wrappers for these, and this is because, while writing the Svelte integration it just didn't feel necessary: because Svelte is a little more "bare-bones" than other frameworks/libraries, many of the features you'll want to add are easily achieved using the same approach that you'd use with the "vanilla" Toolkit.
We'll show a few examples here. Note, though, that as newcomers to the world of Svelte we'd be happy to stand corrected about any incorrect assumptions we've made, and to add features to the Svelte integration to make it more useful to users of Svelte.
Adding a miniview
You can add a miniview to your app by configuring one in an onMount
callback. Let's take the app component from above and add a #miniview
element, then tell the Surface to add a miniview plugin using that element:
<script>
import { newInstance, SurfaceComponent } from '@jsplumbtoolkit/browser-ui-svelte'
import { AbsoluteLayout } from '@jsplumbtoolkit/core'
import { MiniviewPlugin } from '@jsplumbtoolkit/browser-ui-plugin-miniview'
import { onMount } from "svelte"
import MyNodeComponent from './MyNodeComponent.svelte'
let surfaceComponent
const toolkit = newInstance()
const viewParams = {
nodes:{
default:{
component:MyNodeComponent
}
}
}
const renderParams = {
layout:{
type:AbsoluteLayout.type
}
}
//
// instantiate the miniview inside an onMount callback, to get access to the underlying `Surface`.
//
onMount(async() => {
let surface = this.surfaceComponent.getSurface()
surface.addPlugin({
type:MiniviewPlugin.type,
container:document.getElementById("miniview")
})
}
</script>
<div>
<SurfaceComponent viewParams={viewParams}
renderParams={renderParams}
toolkit={toolkit}
bind:this={surfaceComponent} />
<div id="miniview"></div>
</div>
For more information about Miniviews, see this page.
Dragging and dropping new nodes
A common requirement for applications built on the Toolkit is the ability to drag and drop new nodes onto the canvas. Let's add that to our app alongside the miniview we just added. To do this involves three steps:
- first you need to write out the HTML representing nodes that are draggable, the structure for which is entirely up to you. We'll keep it simple with a
ul
here. - secondly, you need to ensure you've used
bind
to get a reference to yourSurfaceComponent
(as in the miniview example above) - lastly, you need to instantiate a
SurfaceDropManager
<script>
import { newInstance, SurfaceComponent } from '@jsplumbtoolkit/browser-ui-svelte'
import { AbsoluteLayout } from '@jsplumbtoolkit/core'
import { MiniviewPlugin } from '@jsplumbtoolkit/browser-ui-plugin-miniview'
import { onMount } from "svelte"
import MyNodeComponent from './MyNodeComponent.svelte'
let surfaceComponent // a reference to the SurfaceComponent this component creates
const toolkit = newInstance()
const viewParams = {
nodes:{
default:{
component:MyNodeComponent
}
}
}
const renderParams = {
layout:{
type:AbsoluteLayout.type
}
}
//
// instantiate the drop manager and miniview inside an onMount callback, to get access to the underlying `Surface`.
//
onMount(async() => {
let surface = this.surfaceComponent.getSurface()
surface.addPlugin({
type:MiniviewPlugin.type,
container:document.getElementById("miniview")
})
new SurfaceDropManager({
surface,
source:document.getElementById("node-palette"),
selector:"div",
dataGenerator:(el) => {
return {
type:el.getAttribute("data-type")
}
}
})
})
</script>
<div>
<SurfaceComponent viewParams={viewParams}
renderParams={renderParams}
toolkit={toolkit}
bind:this={surfaceComponent}/>
<div id="miniview"></div>
<div id="node-palette">
<div data-type="foo">FOO</div>
<div data-type="bar">BAR</div>
</div>
</div>
Note in the above code snippet:
- we used
bind:this={SurfaceComponent}
on our app component to store a reference to the surface component, from which we will get the underlying Surface - we imported
onMount
from "svelte" and instantiated the drop manager in anonMount
callback (because we want to access theSurfaceComponent
that has been rendered) - we identify the container for draggable nodes via the
source
argument toSurfaceDropManager
. - we use
"div"
to identify a CSS selector that is used to find all candidate draggables inside the draggable nodes container. You can use any valid CSS3 selector here. - the
dataGenerator
function is given the element that is being dragged, at the start of a drag, and is expected to return an object to the Toolkit containing, at a minimum, the object'stype
.
To read more about the drop manager, see this page
Base components and common functionality
Many apps using the Toolkit use different components to render different node types. For instance, in the the Svelte flowchart builder demonstration, three nodes types - question
, action
and output
are all rendered with different components, but each needs the ability to support a "delete" button.
The developers of Svelte have not shown a great deal of enthusiasm for the concept of "base" components:
https://github.com/sveltejs/svelte/issues/4168
so to support common functionality for a set of components, the SurfaceComponent
supports an injector
- a function that, given some vertex, can return a map of props to append to the component used to render that vertex. In the demonstration linked above, for example, we add a remove
and edit
method to each node like this:
1. Declare some function to act as the injector
in your app's script
<script>
...
/**
* Optional props injector for vertices. In this app we supply a manager to each vertex that offers remove and edit methods.
* @param vertex
* @return {{}}
*/
function injectManager(vertex) {
return {
manager:{
remove: (vertex) => {
toolkit.removeNode(vertex)
},
edit:(vertex) => {
dialogs.editName(vertex.data, (d) => {
if (d.text && d.text.length > 2) {
// if name is at least 2 chars long, update the underlying data and
// update the UI.
toolkit.updateNode(vertex, d);
}
})
}
}
}
}
...
2. Set this on the SurfaceComponent
<SurfaceComponent viewParams={viewParams}
renderParams={renderParams}
toolkit={toolkit}
bind:this={surfaceComponent}
injector={injectManager}
/>
Now any component used to render any vertex in this demonstration will have a manager
exposed on it, with remove
and edit
methods. Note that we could have inspected vertex
in the injectManager
method and returned different values for different vertices if we wanted to.
Here's the component used to render action
nodes, for example:
<script>
export let data;
export let toolkit;
export let surface;
export let vertex;
export let manager
</script>
<div style="width:{data.w}px;height:{data.h}px"
class="flowchart-object flowchart-action"
data-jtk-target="true"
data-jtk-port-type="target">
<svg width={data.w} height={data.h}>
<rect x="10" y="10" width={data.w-20} height={data.h-20} class="inner" rx="5" ry="5"/>
</svg>
<span>{data.text}</span>
<div class="node-edit node-action" on:click={() => manager.edit(vertex)}></div>
<div class="node-delete node-action" on:click={() => manager.remove(vertex)}></div>
<div class="drag-start connect" data-jtk-source="true" data-jtk-port-type="source"></div>
</div>
This component declares export let manager
: this is a requirement if you want the return value from the injector to be set on the component.