xyflow / xyflow

React Flow | Svelte Flow - Powerful open source libraries for building node-based UIs with React (https://reactflow.dev) or Svelte (https://svelteflow.dev). Ready out-of-the-box and infinitely customizable.
https://xyflow.com
MIT License
21.54k stars 1.43k forks source link

on:connect isn't working as intend to #4215

Closed letalboy closed 1 week ago

letalboy commented 3 weeks ago

Describe the Bug

I'm trying to make a detector to new edge connections, I tried several methods, but the on:connection method isn't working for svelte case, in react version I saw people using it with success however for svelte it isn't working directly, the goal is simple, update the edge's storage with the new edge created with the connection, and possible handle edge deletions too

Your Example Website or App

No response

Steps to Reproduce the Bug or Issue

This is the setup that is causing the problem, below is the code of the entire page of the network:

<script lang="ts">
    import { onMount, onDestroy } from 'svelte';
    import type { User } from '../../common/types/userTypes';
    import { getUser, logout } from "../../api/user";
    import { writable, type Writable } from "svelte/store";
    import ELK from "elkjs/lib/elk.bundled";
    import {
        SvelteFlow,
        Controls,
        Background,
        Position,
        BackgroundVariant,
        MiniMap,
        ConnectionLineType,
        Panel,
        useSvelteFlow,
        MarkerType,
        type Connection,
        type ColorMode,
        type Node,
        type Edge,
    } from "@xyflow/svelte";
    import "@xyflow/svelte/dist/style.css";

    let user: User | null;
    onMount(async () => {
        user = await getUser();
    });

    // Predefined nodes and edges
    let initialNodes: Node[] = [
        {
            id: "1",
            data: { label: "test1", key: "test1", type: 'node', isActive: true },
            type: "default",
            position: { x: 100, y: 150 },
        },
        {
            id: "2",
            data: { label: "test2", key: "test2", type: 'node', isActive: true },
            type: "default",
            position: { x: 400, y: 150 },
        },
        {
            id: "3",
            data: { label: "test3", key: "test3", type: 'node', isActive: true  },
            type: 'textInputNode',  // Reference to the custom node type
            position: { x: 250, y: 300 },
        }
    ];
    let initialEdges: Edge[] = [
        {
          id: "e1",
          type: "default",
          source: "1",
          target: "2",
          label: "Connection 1-2",
          style: 'stroke-width: 3px; stroke: #454CFF'
        }
    ];

    // Initialize writable stores with initial values
    let nodes: Writable<Node[]> = writable(initialNodes);
    let edges: Writable<Edge[]> = writable(initialEdges);
    let colorMode: ColorMode = "dark";

    $:console.log(edges)

    const nodeTypes = {
        textInputNode: TextInputNode
    };
    const { fitView } = useSvelteFlow();
    const elk = new ELK();

    import type { ElkExtendedEdge } from "elkjs/lib/elk.bundled";
    import TextInputNode from './nodes/TextInputNode.svelte';

    function getLayoutedElements(nodes: any[], edges: any[], options:Record<string, any> = {}):Promise<void | { nodes: any[]; edges: ElkExtendedEdge[] | undefined;}> {
        const isHorizontal = options?.["elk.direction"] === "RIGHT";
        const graph = {
        id: "root",
        layoutOptions: options,
        children: nodes.map((node) => ({
            ...node,
            // Adjust the target and source handle positions based on the layout
            // direction.
            targetPosition: isHorizontal ? Position.Left : Position.Top,
            sourcePosition: isHorizontal ? Position.Right : Position.Bottom,

            // Hardcode a width and height for elk to use when layouting.
            width: 150,
            height: 50,
        })),
        edges: edges,
        };

        return elk
        .layout(graph)
        .then((layoutedGraph) => {
            // Check if `children` is defined to satisfy TypeScript's strict null checks
            if (!layoutedGraph.children) {
            // Handle the undefined case, e.g., by returning an empty array, throwing an error, or other appropriate action
            console.error('layoutedGraph.children is undefined');
            return { nodes: [], edges: layoutedGraph.edges };
            }

            const nodes = layoutedGraph.children.map((node: any) => ({ 
            ...node,
            // Ensure `x` and `y` are defined or provide default values
            position: {
                x: node.x !== undefined ? node.x : 0,
                y: node.y !== undefined ? node.y : 0,
            },
            }));

            return {
            nodes: nodes,
            edges: layoutedGraph.edges,
            };
        })
        .catch(console.error);
    }

    const elkOptions = {
        "elk.algorithm": "layered",
        "elk.layered.spacing.nodeNodeBetweenLayers": "100",
        "elk.spacing.nodeNode": "80",
    };

    function onLayout(direction: string, useInitialNodes = false) {
        const opts = { "elk.direction": direction, ...elkOptions };
        const ns = useInitialNodes ? initialNodes : $nodes;
        const es = useInitialNodes ? initialEdges : $edges;

        getLayoutedElements(ns, es, opts).then((result) => {
            // Check if result is not undefined
            if (result) {
                const { nodes: layoutedNodes, edges: layoutedEdges } = result;

                if (layoutedEdges === undefined) {
                console.error('An error occurred:');
                return;
                }
                $nodes = layoutedNodes;
                $edges = layoutedEdges;
                fitView();

                window.requestAnimationFrame(() => fitView());
            } else {
                // Handle the case where result might be undefined
                console.error('No result from getLayoutedElements');
            }
        }).catch((error) => {
            console.error('An error occurred:', error);
        });
    }

    // Function to update the position of a node in the nodes store
    function updateNodePosition(nodes: Writable<Node[]>, nodeId: string, newX: number, newY: number): void {
        nodes.update(currentNodes => {
            return currentNodes.map(node => {
                console.log(`new x: ${newX} new y: ${newY}`);
                if (node.id === nodeId) {
                    return { ...node, position: { x: newX, y: newY } };
                }
                return node;
            });
        });
    }

    // Corrected function to handle node drag stop events
    function handleNodeDragStop(nodes: Writable<Node[]>, event: CustomEvent): void {
        const node: Node = event.detail.nodes[0];
        console.log(`Node drag stopped. Node id: ${node.id}, Current position: x=${node.position.x}, y=${node.position.y}`);
        updateNodePosition(nodes, node.id, node.position.x, node.position.y);
    }

    function handleConnect(connection: Connection) {
        console.log("new connection:", connection);
        const newEdge: Edge = {
            id: `${connection.source}-${connection.target}`,
            source: connection.source,
            target: connection.target,
            type: 'smoothstep', // Default type, maybe change if necessary...
        };
        edges.update(currentEdges => [...currentEdges, newEdge]);
    }

</script>

<div id="network-page-container">
    <div class="network-map-container">
        <SvelteFlow
            {nodes}
            {edges}
            {nodeTypes}
            {colorMode}
            fitView
            connectionLineType={ConnectionLineType.SmoothStep}
            defaultEdgeOptions={{ type: "smoothstep", animated: true }}
            on:connect={(event) => handleConnect(event)}
            on:nodedragstop={(event) => {
                console.log("Node drag stop event triggered", event);
                handleNodeDragStop(nodes, event);
            }}
            on:nodeclick={(event) => {
                console.log("Node click event triggered", event);
                if (event.detail.node.data.type === "handler") {
                    // handleNodeClick(event.detail.node.data)
                } else {
                    console.log("Normal node click:", event.detail.node);
                }
            }}
        >
            <Controls />
            <Background variant={BackgroundVariant.Dots} />
        </SvelteFlow>
    </div>
</div>

<style>

    #network-page-container {
        display: flex;
        flex-direction: column;
        flex-flow: column;

        justify-content: center;
        align-items: center;

        width: 100%;
        min-width: 100vh;
        height: 100%;
        min-height: 100vh;
    }

    .network-map-container {
        display: flex;
        flex-direction: column;
        flex-flow: column;

        align-items: center;
        justify-content: center;

        width: 100%;
        height: 100vh;
    }

</style>

When running it we can see that the edge's storage isn't updated because when a new edge is created in the graph the on:connect doesn't trigger the function

Expected behavior

The expected behavior is to the edge store be updated with the new edges

Screenshots or Videos

No response

Platform

Additional context

This method really exist, or I'm trying to do something that isn't possible with the svelte version, is this really a bug or a bad implementation? I will appreciate any feed back, tks in advance! Since I don't know if this is a bug or not I'm putting here as an issue instead of putting in the discussion session as a question, sorry if here for some reason isn't the correct place for this.

peterkogo commented 2 weeks ago

You don't need to apply any updates in Svelte Flow by hand. That is only required in React Flow. on:connect will notify you when a user has connected two handles successfully.

If you create a stackblitz with your code I can help you out.

letalboy commented 2 weeks ago

Hello @peterkogo thanks for the speed in reply! I've created the stackblitz with all the required code to the graph page, exactly how I'm trying to do here. Here is the link for the code, let me know if you are able to access t, I never had used stackblitz before so I don't know if I configured it correctly...

letalboy commented 2 weeks ago

I also forget to mention that I don't have the stackblitz plan, so I don't know if I can add you to the project, in the case that you can't modify it directly you don't need to wait, simple to a fork, since you already know how to use the platform it will be more simple, then we continue here to solve this particular thing, if possible. Let me know if you need something else in the svelte setup ;)

letalboy commented 1 week ago

Hey @peterkogo, got any updates? I just need to know how to capture new links when a user connects nodes. I know we can use an approximation similar to the 'node approximation connection' example in the Svelte docs, but I need something to trigger a function to save a new connection or to auto-sync with the variable when changes occur in the graph. I've got 2 projects depending on this that I'm kinda late to finish, and I can't find a clear explanation in Svelte in any place. If you know how, could you use the source code I uploaded on StackBlitz? Feel free to fork it and implement a basic example, or just explain how it works in Svelte, you know just a simple example nothing too elaborated, just a simple example of a node edge update using callbacks or a reference link intenally. That would be super helpful and would allow me to continue with the Svelte implementation of the graph that i'm kinda. Without this functionality, I might have to switch everything to React, which has better documentation for this. Any feedback you can provide would be greatly appreciated. I'm waiting for any updates.

moklick commented 1 week ago

Hey @letalboy

there is no "on:connect" handler, but an "onconnect" (and also "onconnectstart" and "onconnectend"). Svelte Flow creates edges automatically for you. As Peter pointed out there is no need to do it on your own. However, you can use the onconnect handler or the onedgecreate handler if you want to adjust the id.

Svelte Flow adds new edges to the edges writable as you can see in the onconnectend handler: https://stackblitz.com/edit/svelte-flow-ts-pbp7xu?file=src/Flow/index.svelte

Does this solve your issue?

letalboy commented 1 week ago

Thanks @moklick this does exactly what I want. I saw this method before but think it will not work because of the missing : standard of the on:connect and other events notation in svelt but I copied what you uploaded in the Stackblitz to my code here, and it does exactly what I was looking for. This completely solves my issue. Thank you!

moklick commented 1 week ago

I am glad you could solve it :)