projectstorm / react-diagrams

a super simple, no-nonsense diagramming library written in react that just works
https://projectstorm.cloud/react-diagrams
MIT License
8.69k stars 1.17k forks source link

Event for Link completed? #750

Open jduncanRadBlue opened 4 years ago

jduncanRadBlue commented 4 years ago

I am trying to create links between nodes where I would then create a label based on node names/properties. When I link two nodes I am not seeing an event fired for linksUpdated when the nodes are successfully connected. What is the appropriate event to use for this?

sinanyaman95 commented 4 years ago

Can you share the code where you register the linksUpdated listener?

jduncanRadBlue commented 4 years ago

Yes, @sinanyaman95 , thank you for your quick reply.

Neither event2 or event3 are being logged in the console after a link is created. Event2 writes out 'link' when the link is first clicked to drag, but no event fires when nodes are connected.

import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import * as _ from 'lodash';
import '../styles/App.css';
import createEngine, {
  DiagramModel,
  DefaultNodeModel,
  DefaultLinkModel,
} from '@projectstorm/react-diagrams';
import { CanvasWidget } from '@projectstorm/react-canvas-core';
import styled from '@emotion/styled';
import { useSelector } from 'react-redux';
import TrayWidget from './TrayWidget';
import Fab from '@material-ui/core/Fab';
import SaveIcon from '@material-ui/icons/Save'

import store from '../store/store';
import { getApplications } from '../actions/applicationActions';

export default function BodyWidget() {

  const applicationResults = useSelector((state) => state.applications.data);
  console.log(applicationResults)
  const [applications, setApplications] = useState([applicationResults]);

  const diagramEngine = createEngine();
  const activeModel = new DiagramModel();
  diagramEngine.setModel(activeModel);

  const [engine, setEngine] = React.useState(diagramEngine);

  const handleUpdateEngine = (engine) => {
    setEngine(engine);
  };

  const style = {
    Body: {
      flexGrow: 1,
      display: 'flex',
      flexDirection: 'column',
      height: '90%',
    },
    Header: {
      display: 'flex',
      background: 'rgb(30, 30, 30)',
      flexGrow: 0,
      flexShrink: 0,
      color: 'white',
      fontFamily: 'Helvetica, Arial, sans-serif',
      padding: '10px',
      alignItems: 'center',
      width:'100%'
    },

    Content: {
      display: 'flex',
      flexGrow: 1,
    },
    Layer: {
      position: 'relative',
      flexGrow: 1,
      height: '100vh',
      width: '100%',
      background: 'rgb(30, 30, 30)',
    },
    Fab: {
      right: '20px',
      bottom: '80px',
      position: 'absolute',
      fontSize: '14px'
    },
    Icon: {
      width: '24px',
      height: '48px',
    }
  };

  return (
    <div style={style.Body}>
      <div style={style.Content}>
        <TrayWidget applications={applicationResults} />
        <div
          style={style.Layer}
          onDrop={(event) => {
            const data = JSON.parse(
              event.dataTransfer.getData('storm-diagram-node')
            );

            const nodesCount = _.keys(diagramEngine.getModel().getNodes())
              .length;
            let node = null;
            console.log(data);
            if (data.nodeType === 'in') {
              node = new DefaultNodeModel(data.name, 'rgb(192,255,0)');
              node.addInPort('In');
            } else {
              node = new DefaultNodeModel(data.name, 'rgb(0,192,255)');
              node.addOutPort('Out');
            }
            const point = diagramEngine.getRelativeMousePoint(event);
            node.setPosition(point);

            diagramEngine.getModel().addNode(node);
            diagramEngine.getModel().registerListener({
              linksUpdated: (event2) => {

                console.log('link');
              },
              nodesUpdated: (event3) => {

                console.log(event3);
              },
            });
            handleUpdateEngine(diagramEngine);
            diagramEngine.repaintCanvas();
          }}
          onDragOver={(event) => {
            console.log(event);
            event.preventDefault();
          }}
        >
          <CanvasWidget className="canvas" engine={engine} />
        </div>
      </div>
      <div style={style.Fab}>
        <Fab color="secondary.light" aria-label="add">
          <SaveIcon style={style.Icon} />
        </Fab>
      </div>
    </div>
  );
}
sinanyaman95 commented 4 years ago

This should work. The syntax and the listener registration is correct.

My only thought is, if you are doing this on drag-and-drop example, and registering these listeners in BodyWidget.tsx, these listeners will only be attached to the nodes you create with dragging and dropping. Can you try creating a new node by drag and drop and test the listeners on that node?

jduncanRadBlue commented 4 years ago

The only way for a user to use the node is to use the drag and drop feature. See screen shot: Two buttons are dragged and dropped onto canvas which creates the nodes. I then try to connect the previously dropped nodes with links and no event is fired after link completion. Only two events are fired that write out 'link' when the link 'out' is clicked...not when nodes are successfully linked.

Screen Shot 2020-10-21 at 9 16 35 AM

sinanyaman95 commented 4 years ago

Well, the only reason this won't work is if you use a different model than the one you registered the listeners. Just tried the same syntax for registering listeners on my example, and it worked. I can't see where the issue is for now, sorry.

jduncanRadBlue commented 4 years ago

Would you be willing to share your code from your example? This is making me a bit crazy!

sinanyaman95 commented 4 years ago

helpGithub

BodyWidget.js

import * as React from 'react';
import * as _ from 'lodash';
import { TrayWidget } from './TrayWidget';
import { Application } from '../Application';
import { TrayItemWidget } from './TrayItemWidget';
import { DefaultNodeModel, DefaultPortModel } from '@projectstorm/react-diagrams';
import { CanvasWidget } from '@projectstorm/react-canvas-core';
import { DemoCanvasWidget } from '../../helpers/DemoCanvasWidget';
import styled from '@emotion/styled';
import { Button } from 'semantic-ui-react';

export interface BodyWidgetProps {
    app: Application;
}

namespace S {
    export const Body = styled.div`
        flex-grow: 1;
        display: flex;
        flex-direction: column;
        min-height: 100%;
    `;

    export const Header = styled.div`
        display: flex;
        background: rgb(30, 30, 30);
        flex-grow: 0;
        flex-shrink: 0;
        color: white;
        font-family: Helvetica, Arial, sans-serif;
        padding: 10px;
        align-items: center;
    `;

    export const Content = styled.div`
        display: flex;
        flex-grow: 1;
    `;

    export const Layer = styled.div`
        position: relative;
        flex-grow: 1;
    `;
}

export class BodyWidget extends React.Component<BodyWidgetProps> {
    render() {
        return (
            <div>
                {this.props.app.getSumModuleOpened() ?
                    <Button onClick = { () => {this.props.app.setSumModuleOpened(false); this.forceUpdate();}}> Test </Button>
                    :
                    <S.Body>
                        <S.Header>
                            <div className="title">Storm React Diagrams - DnD demo</div>
                        </S.Header>
                        <S.Content>
                            <TrayWidget>
                                <TrayItemWidget model={{ type: 'in' }} name="In Node" color="rgb(192,255,0)" />
                                <TrayItemWidget model={{ type: 'out' }} name="Out Node" color="rgb(0,192,255)" />
                                <TrayItemWidget model={{ type: 'inout' }} name="InOut Node" color="rgb(0,54,156)" />
                                <TrayItemWidget model={{ type: 'sum' }} name="Sum Node" color="rgb(0, 204, 197)" />
                            </TrayWidget>
                            <S.Layer
                                onDrop={(event) => {
                                    var data = JSON.parse(event.dataTransfer.getData('storm-diagram-node'));
                                    var nodesCount = _.keys(this.props.app.getDiagramEngine().getModel().getNodes()).length;
                                    this.props.app.getDiagramEngine().getModel().registerListener({
                                        linksUpdated: l => {
                                            console.log("link\n");
                                        },
                                        nodesUpdated: n => {
                                            console.log("node")
                                        }
                                    })
                                    var node: DefaultNodeModel = null;
                                    if (data.type === 'in') {
                                        node = new DefaultNodeModel('Node ' + (nodesCount + 1), 'rgb(192,255,0)');
                                        node.addInPort('In');
                                        node.getOptions().extras = { "type": "in" };
                                    } else if (data.type === 'out') {
                                        node = new DefaultNodeModel('Node ' + (nodesCount + 1), 'rgb(0,192,255)');
                                        node.addOutPort('Out');
                                        node.getOptions().extras = { "type": "out" };
                                    } else if (data.type === 'inout') {
                                        node = new DefaultNodeModel('Node ' + (nodesCount + 1), 'rgb(0,54,156)');
                                        node.addOutPort('Out');
                                        node.addInPort('In');
                                        node.getOptions().extras = { "type": "inout" };
                                    }

                                    var point = this.props.app.getDiagramEngine().getRelativeMousePoint(event);
                                    node.setPosition(point);
                                    this.props.app.getDiagramEngine().getModel().addNode(node);
                                    this.forceUpdate();
                                }}
                                onDragOver={(event) => {
                                    event.preventDefault();
                                }}>
                                <DemoCanvasWidget>
                                    <CanvasWidget engine={this.props.app.getDiagramEngine()} />
                                </DemoCanvasWidget>
                            </S.Layer>
                        </S.Content>
                    </S.Body>}
            </div>
        );
    }
}

Application.ts

import * as SRD from '@projectstorm/react-diagrams';
import { LabelModel } from '@projectstorm/react-diagrams';
import { action } from '@storybook/addon-actions';

/**
 * @author Dylan Vorster
 */
export class Application {
    protected activeModel: SRD.DiagramModel;
    protected diagramEngine: SRD.DiagramEngine;
    private sumModuleOpened: boolean;

    constructor() {
        this.diagramEngine = SRD.default();
        this.sumModuleOpened = false;
        this.newModel();
    }

    public newModel() {
        this.activeModel = new SRD.DiagramModel();
        this.diagramEngine.setModel(this.activeModel);

        //3-A) create a default node
        var node1 = new SRD.DefaultNodeModel('Node 1', 'rgb(0,192,255)');
        let port = node1.addOutPort('Out');
        node1.setPosition(100, 100);
        node1.getOptions().extras={"type":"out"};

        //3-B) create another default node
        var node2 = new SRD.DefaultNodeModel('Node 2', 'rgb(192,255,0)');
        let port2 = node2.addInPort('In');
        node2.setPosition(400, 100);
        node2.getOptions().extras={"type":"in"};

        var node3 = new SRD.DefaultNodeModel('Node 3','rgb(0,54,156)');
        let port3 = node3.addInPort('In');
        let port4 = node3.addOutPort('Out');
        node3.setPosition(250,250);
        node3.getOptions().extras={"type":"inout"};

        // link the ports
        let link1 = port.link(port2);
        let link2 = port.link(port3);
        let link3 = port4.link(port2);

        var nodes = [node1, node2, node3];
        var links = [link1, link2, link3];

        this.activeModel.addAll(...nodes, ...links);;
    }

    public getActiveDiagram(): SRD.DiagramModel {
        return this.activeModel;
    }

    public getDiagramEngine(): SRD.DiagramEngine {
        return this.diagramEngine;
    }

    public setSumModuleOpened(value){
        this.sumModuleOpened = value;
    }

    public getSumModuleOpened(): boolean{
        return this.sumModuleOpened;
    }
}

In this example, the listener only works if I drag and drop them, since the application file is only for initial diagram which you can't see in the gif since I deleted it.

jduncanRadBlue commented 4 years ago

yours behaves the same way mine does. Once link is dropped on the IN node, no event fires.

sinanyaman95 commented 4 years ago

Ahh, I totally missed your point my bad. There are events on Link Model:

sourcePortChanged
targetPortChanged

Maybe you can give them a go, my last hope to be helpful in this issue.

oscarkwan commented 3 years ago

I achieved this by adding a mouseUp event handler and saving the linksUpdated event to a ref and using that event inside the mouseUp event handler.

AshishAvesta commented 3 years ago

I achieved this by adding a mouseUp event handler and saving the linksUpdated event to a ref and using that event inside the mouseUp event handler.

Can you please provide code.? How you achieved that?

RaiMX commented 3 years ago

I achieved this by adding a mouseUp event handler and saving the linksUpdated event to a ref and using that event inside the mouseUp event handler.

Can you please provide code.? How you achieved that?

...
const linkRef = React.useRef();
...

model.registerListener({
    linksUpdated: (e) => this.emit('linksUpdated', e),
});
...
(on the wrapper div)

onMouseUp={(event) => {
    if(linkRef.current){
        const link = linkRef.current;
        if(link.sourcePort !== null && link.targetPort !== null){
            console.log('FROM', link.sourcePort.parent.options.name);
            console.log('TO', link.targetPort.parent.options.name);
        }
        linkRef.current = null;
    }
}}
jtwigg commented 2 years ago

yours behaves the same way mine does. Once link is dropped on the IN node, no event fires.

I'm doing the same thing. I cannot get events fired from anything when links are made.

Listening to onMouseUp doesn't seem to make sense or the description is incomplete; where do I attach the linkRef?

Is there a coherent way of listening to clean events when links are made?