While using the vueflow package and the various examples (drag-n-drop & custom nodes) I have a hard time connecting custom nodes. Some used components are from Vuetify.
The StartNode is rendered as expected. Dragging the InputNode from the side into VueFlow also works as expected. But then the issues start. There are multiple issues but maybe one answers the other, if not I'll ask another question. When dragging from the StartNode to the InputNode the following error is given:
Error:
<path>
attribute d: Expected number
When dragging from the InputNode to the StartNode no error is given but a new "default node" is rendered instead.
I hope someone can point me in the right direction of how to connect custom nodes.
The main file
<template>
<v-row>
<v-col cols="9" style="height: 500px" @drop="onDrop">
<VueFlow
v-model:nodes="nodes"
v-model:edges="edges"
:node-types="types"
:connection-mode="ConnectionMode.Strict"
@dragover="onDragOver"
@dragleave="onDragLeave"
/>
</v-col>
<v-col cols="3">
<InputNode
:draggable="true"
@dragstart="onDragStart( $event, 'input' )"
/>
</v-col>
</v-row>
</template>
<script setup>
import { ref, markRaw } from 'vue';
import { VueFlow, useVueFlow, ConnectionMode } from '@vue-flow/core';
import useDragAndDrop from './drag-n-drop.js';
import StartNode from './nodes/StartNode.vue';
import InputNode from './nodes/InputNode.vue';
const { onConnect, addEdges } = useVueFlow();
const { onDragOver, onDrop, onDragLeave, onDragStart } = useDragAndDrop()
const nodes = ref( [
{
id: 'start-node',
type: 'start',
position: { x: 0, y: 50 },
dimensions: { width: '150px', height: '50px' },
},
] );
const edges = ref( [] );
const types = {
start: markRaw( StartNode ),
input: markRaw( InputNode ),
};
onConnect( addEdges );
</script>
<style>
@import '@vue-flow/core/dist/style.css';
@import '@vue-flow/core/dist/theme-default.css';
</style>
The StartNode:
<template>
<div>
<v-card color="green" :width="props.dimensions?.width ?? '150px'" :height="props.dimensions?.height ?? '50px'">
<v-card-text>Start</v-card-text>
</v-card>
<Handle id="start" type="source" :position="Position.Right" :connectable="handleConnectable" />
</div>
</template>
<script setup>
import { Position, Handle } from '@vue-flow/core';
const props = defineProps( {
id: {
type: String,
required: true,
},
data: {
type: Object,
required: true,
},
dimensions: {
type: Object,
required: false,
},
} );
function handleConnectable( node, connectedEdges )
{
return connectedEdges.length <= 1;
}
</script>
The InputNode:
<template>
<div
>
<Handle v-if="data" id="input-target" type="target" :position="Position.Right" :connectable="handleConnectable" />
<v-card
:draggable="props.draggable"
:width="'300px'"
:height="'104px'"
>
<v-card-title>Input</v-card-title>
<v-card-text>
<v-text-field
label="Input"
outlined
hide-details
density="compact"
/>
</v-card-text>
</v-card>
<Handle v-if="data" id="input-source" type="source" :position="Position.Left" :connectable="handleConnectable" />
</div>
</template>
<script setup>
import { Handle, Position } from '@vue-flow/core';
const props = defineProps( {
id: {
type: String,
required: false,
},
data: {
type: Object,
required: false,
},
dimensions: {
type: Object,
required: false,
},
draggable: {
type: Boolean,
required: false,
default: true,
},
} );
function handleConnectable( node, connectedEdges )
{
return connectedEdges.length <= 1;
}
</script>
The drag-n-drop.js (taken from the example):
import { useVueFlow } from '@vue-flow/core'
import { ref, watch } from 'vue'
let id = 0
/**
* @returns {string} - A unique id.
*/
function getId() {
return `dndnode_${id++}`
}
/**
* In a real world scenario you'd want to avoid creating refs in a global scope like this as they might not be cleaned up properly.
* @type {{draggedType: Ref<string|null>, isDragOver: Ref<boolean>, isDragging: Ref<boolean>}}
*/
const state = {
/**
* The type of the node being dragged.
*/
draggedType: ref(null),
isDragOver: ref(false),
isDragging: ref(false),
}
export default function useDragAndDrop() {
const { draggedType, isDragOver, isDragging } = state
const { addNodes, screenToFlowCoordinate, onNodesInitialized, updateNode } = useVueFlow()
watch(isDragging, (dragging) => {
document.body.style.userSelect = dragging ? 'none' : ''
})
function onDragStart(event, type) {
if (event.dataTransfer) {
event.dataTransfer.setData('application/vueflow', type)
event.dataTransfer.effectAllowed = 'move'
}
draggedType.value = type
isDragging.value = true
document.addEventListener('drop', onDragEnd)
}
/**
* Handles the drag over event.
*
* @param {DragEvent} event
*/
function onDragOver(event) {
event.preventDefault()
if (draggedType.value) {
isDragOver.value = true
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'move'
}
}
}
function onDragLeave() {
isDragOver.value = false
}
function onDragEnd() {
isDragging.value = false
isDragOver.value = false
draggedType.value = null
document.removeEventListener('drop', onDragEnd)
}
/**
* Handles the drop event.
*
* @param {DragEvent} event
*/
function onDrop(event) {
const position = screenToFlowCoordinate({
x: event.clientX,
y: event.clientY,
})
const nodeId = getId()
const newNode = {
id: nodeId,
type: draggedType.value,
position,
data: { label: nodeId },
}
/**
* Align node position after drop, so it's centered to the mouse
*
* We can hook into events even in a callback, and we can remove the event listener after it's been called.
*/
const { off } = onNodesInitialized(() => {
updateNode(nodeId, (node) => ({
position: { x: node.position.x - node.dimensions.width / 2, y: node.position.y - node.dimensions.height / 2 },
}))
off()
})
addNodes(newNode)
}
return {
draggedType,
isDragOver,
isDragging,
onDragStart,
onDragLeave,
onDragOver,
onDrop,
}
}
There were several issues. The first issue being that the handle source and target locations were switched.
The second issue in the StartNode
is the dynamic width
and height
.
The third issue was on the InputNode
, The declared :draggable="props.draggable"
on the v-card
caused the issue of adding default nodes.