Struggling with connecting custom nodes while utilizing the vueflow package and its example functionalities like drag-and-drop & custom nodes. Some components are from Vuetify.
The StartNode renders as expected, and dragging the InputNode into VueFlow functions correctly. However, issues arise when trying to connect these nodes. One specific error occurs when dragging from StartNode to InputNode:
Error:
<path>
attribute d: Expected number
No error is displayed when dragging from InputNode to StartNode, but it results in a new "default node" being rendered instead.
I am seeking guidance on how to properly connect these custom nodes.
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 (retrieved 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,
}
}