javascriptvue.jsvuejs3

Unable to connect drag 'n dropped custom nodes


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,
  }
}


Solution

  • 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.