projectstorm / react-diagrams

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

Incorrect target port position when link added after initial render #791

Closed phwt closed 3 years ago

phwt commented 3 years ago

Current Behavior

When adding a link after its initial render the target port position is incorrectly placed at the top-left corner of the canvas, as shown in the video below.

https://user-images.githubusercontent.com/28344318/103510386-bba34600-4e97-11eb-8f07-80f6565a67af.mov

Expected Behavior

The target position of the link is correctly placed the the top port of the newly added node.

Steps to Reproduce

This is how the canvas was setup

import {
  DiagramEngine,
  DiagramModel,
  DiagramWidget,
} from "storm-react-diagrams";
import React from "react";
import { CustomNodeModel } from "../../custom/CustomNodeModel";
import { CustomNodeFactory } from "../../custom/CustomNodeFactory";
import { Button } from "react-bootstrap";

const CustomDiagram = () => {
  const engine = new DiagramEngine();
  engine.installDefaultFactories();
  engine.registerNodeFactory(new CustomNodeFactory());

  const model = new DiagramModel();

  // Initial Nodes
  const customNode = new CustomNodeModel();
  customNode.setPosition(100, 100);

  const customNode2 = new CustomNodeModel();
  customNode2.setPosition(100, 350);

  // This link works as intended
  const link = customNode.getPort("bottom").link(customNode2.getPort("top"));

  model.addAll(customNode, customNode2, link);
  engine.setDiagramModel(model);

  const addNode = () => {
    const newNode = new CustomNodeModel();
    newNode.setPosition(100, 600);
    model.addNode(newNode);

    // This link position is incorrect
    const newLink = customNode2.getPort("bottom")?.link(newNode.getPort("top"));

    model.addLink(newLink);
    engine.repaintCanvas();
  };

  return (
    <>
      <DiagramWidget className="srd-demo-canvas" diagramEngine={engine} />
      <Button onClick={addNode}>Add Node</Button>
    </>
  );
};

export default CustomDiagram;

And this is how the CustomNode was set up - basically the same as DiamondNode in the demo

CustomNodeWidget.tsx ```tsx import React from "react"; import { Card } from "react-bootstrap"; import { NodeModel, PortWidget } from "storm-react-diagrams"; const CardPort = (props: { name: string; node: NodeModel }) => (
); export class CustomNodeWidget extends React.Component<{ node: any }, {}> { render() { return ( {/* Place the port at top, left, bottom, right side of the card */}
{" "}
{" "}

Custom Node

); } } ```
CustomNodeModel.ts ```ts import { NodeModel } from 'storm-react-diagrams' import { CustomPortModel } from './CustomPortModel' import * as _ from 'lodash' export class CustomPortModel extends NodeModel { constructor() { super('customNode') this.addPort(new CustomPortModel('top')) this.addPort(new CustomPortModel('bottom')) this.addPort(new CustomPortModel('left')) this.addPort(new CustomPortModel('right')) } // Override to return as CustomPortModel instead of PortModel getPort(name: string): CustomPortModel { return super.getPort(name) as CustomPortModel } } ```
CustomPortModel.ts ```ts import * as _ from 'lodash' import { LinkModel, DiagramEngine, PortModel, DefaultLinkModel, } from 'storm-react-diagrams' export class CustomPortModel extends PortModel { position: string | 'top' | 'bottom' | 'left' | 'right' constructor(pos: string = 'top') { super(pos, 'customNode') this.position = pos } serialize() { return _.merge(super.serialize(), { position: this.position, }) } deSerialize(data: any, engine: DiagramEngine) { super.deSerialize(data, engine) this.position = data.position } createLinkModel(): LinkModel { return new DefaultLinkModel() } link(port: CustomPortModel): LinkModel { let link = this.createLinkModel() link.setSourcePort(this) link.setTargetPort(port) return link } } ```
CustomNodeFactory.tsx ```tsx import * as SRD from 'storm-react-diagrams' import { CustomNodeWidget } from './CustomNodeWidget' import { CustomNodeModel } from './CustomNodeModel' import * as React from 'react' export class CustomNodeFactory extends SRD.AbstractNodeFactory { constructor() { super('customNode') } generateReactWidget( diagramEngine: SRD.DiagramEngine, node: SRD.NodeModel ): JSX.Element { return } getNewInstance() { return new CustomNodeModel() } } ```

The external library that's used is bootstrap and react-bootstrap for widget structure and styling. I don't know if this issue is related to the library or with my CustomNode - Please let me know, thanks.

phwt commented 3 years ago

I figured out that the port cannot be linked until it has been rendered on the canvas. I managed to fix that by wrapping port linking/adding in setImmidiate() to make sure this executes after the newNode has been rendered on the canvas.

const addNode = () => {
  const newNode = new CustomNodeModel();
  newNode.setPosition(100, 600);
  model.addNode(newNode);

  setImmediate(() => {
    const newLink = customNode2.getPort("bottom")?.link(newNode.getPort("top"));
    model.addLink(newLink);
    engine.repaintCanvas();
  });
};

I'm not sure if this solution the best practice for this particular problem. Please let me know.