Multiple Links created between Nodes

Hi I am facing issues with links which are getting created between nodes.

I am trying to achieve below scenarios.

  • Pre-Drawn nodes between 2 existing “start” and “end” nodes.

  • There should be only 1 link drawn between 2 nodes.

  • One should be able to drag an existing link from one node to other.

  • The pre-drawn nodes link should not appear again once i delete it.

To achieve the above scenarios i did the below things

  1. Added a default link data in linkArray.(It draws a default link between nodes). But if i delete the link and move the node around it again gets linked to the previous node.

  2. Added toMaxLinks: 1,fromMaxLinks: 1, to my port but it is still not handling the scenario.

  3. Added relinkableFrom: true, relinkableTo: true, in my linkTemplate. But this is not working as well.

  4. Also the ‘undoManager.isEnabled’: true is not undoing the diagram actions.

My code is shared below in stackblitz:

Below is my code in text :

 public diagramNodeData: Array<go.ObjectData> = [
   { key: "Start", desc: "Start", color: "gray", category: "start", loc: "0 0" },
    { key: "End", desc: "End", color: "gray", category: "end", loc: "300 0" },
    { key: "Target", desc: "End", color: "gray", loc: "300 -300" },
     { key: "Target2", desc: "End", color: "gray", loc: "300 300" }
  ];
  public diagramLinkData: Array<go.ObjectData> = [
    {from:'Start', to: 'End'}
  ];


  public diagramModelData = { nodeDataArray: this.diagramNodeData,linkDataArray:this.diagramLinkData };

    // When the diagram model changes, update app data to reflect those changes
    public diagramModelChange = function (changes: go.IncrementalData) {
      this.diagramNodeData = DataSyncService.syncNodeData(changes, this.diagramNodeData);
      this.diagramLinkData = DataSyncService.syncLinkData(changes, this.diagramLinkData);
      this.diagramModelData = DataSyncService.syncModelData(changes, this.diagramModelData);
      };

  // initialize diagram / templates
  public initDiagram(): go.Diagram {

const $ = go.GraphObject.make;
const dia = $(go.Diagram,
  {
    'grid.visible': true,
    'undoManager.isEnabled': true,
    "LinkDrawn": showLinkLabel,  // this DiagramEvent listener is defined below
    "LinkRelinked": showLinkLabel,
    allowRelink: true,
    allowCopy: true,
    allowDelete: true,
    allowMove: true,
    allowUndo: true,
    model: $(go.GraphLinksModel,
      {
        linkKeyProperty: 'key' // IMPORTANT! must be defined for merges and data sync when using GraphLinksModel
      })
  }
);
// Make link labels visible if coming out of a "conditional" node.
// This listener is called by the "LinkDrawn" and "LinkRelinked" DiagramEvents.
function showLinkLabel(e) {
  var label = e.subject.findObject("LABEL");
  if (label !== null) label.visible = (e.subject.fromNode.data.category === "Conditional");
}

//Define the diagram grid 
dia.grid =
  $(go.Panel, go.Panel.Grid,  // or "Grid"
    { gridCellSize: new go.Size(10, 10) },
    $(go.Shape, "LineH", { strokeDashArray: [0.5, 9.5], strokeWidth: 0.5 }),
    $(go.Shape, "LineV", { strokeDashArray: [0.5, 9.5], strokeWidth: 0.5 })
  );
//Add Scroll Margin to add scroll in Diagram
dia.scrollMargin = 100;


//Define the link template for the link properties
dia.linkTemplate =
$(go.Link,  // the whole link panel
  {
    routing: go.Link.AvoidsNodes,
    curve: go.Link.JumpOver,
    corner: 5, toShortLength: 4,
    relinkableFrom: true,
    relinkableTo: true,
    reshapable: false,
    resegmentable: true,
    resizable: true,
    canRelinkTo: function(){return true;},
    // mouse-overs subtly highlight links:
    mouseEnter: function (e, link: any) { link.findObject("HIGHLIGHT").stroke = "rgba(30,144,255,0.2)"; },
    mouseLeave: function (e, link: any) { link.findObject("HIGHLIGHT").stroke = "transparent"; },
    selectionAdorned: false
  },
  new go.Binding("points").makeTwoWay(),
  $(go.Shape,  // the highlight shape, normally transparent
    { isPanelMain: true, strokeWidth: 8, stroke: "transparent", name: "HIGHLIGHT" }),
  $(go.Shape,  // the link path shape
    { isPanelMain: true, stroke: "gray", strokeWidth: 2 },
    new go.Binding("stroke", "isSelected", function (sel) { return sel ? "dodgerblue" : "gray"; }).ofObject()),
  $(go.Shape,  // the arrowhead
    { toArrow: "standard", strokeWidth: 0, fill: "gray" }),
  $(go.Panel, "Auto",  // the link label, normally not visible
    { visible: false, name: "LABEL", segmentIndex: 2, segmentFraction: 0.5 },
    new go.Binding("visible", "visible").makeTwoWay(),
    $(go.Shape, "RoundedRectangle",  // the label shape
      { fill: "#F8F8F8", strokeWidth: 0 }),
    $(go.TextBlock, "Yes",  // the label
      {
        textAlign: "center",
        font: "10pt helvetica, arial, sans-serif",
        stroke: "#333333",
        editable: true
      },
      new go.Binding("text").makeTwoWay())
  )
);
  // temporary links used by LinkingTool and RelinkingTool are also orthogonal:
 dia.toolManager.linkingTool.temporaryLink.routing = go.Link.Orthogonal;
 dia.toolManager.relinkingTool.temporaryLink.routing = go.Link.Orthogonal;

var templmap = new go.Map<string, go.Part>(); // In TypeScript you could write: new go.Map<string, go.Node>();
// for each of the node categories, specify which template to use

//Start Node
var startTemplate =
  $(go.Node, "Table", nodeStyle(),
    $(go.Panel, "Spot",
      $(go.Shape, "RoundedRectangle",
        { desiredSize: new go.Size(90, 70), fill: "#dcd4cd", stroke: "#09d3ac", strokeWidth: 3.5 }),
      $(go.TextBlock, "Start", textStyle(),
        new go.Binding("text", "key")),
    ),
    new go.Binding("location", "loc"),
    // two named ports, one on right and one on bottom all outgoing:

    makePort("R", go.Spot.Right, go.Spot.Right, true, true),
    makePort("B", go.Spot.Bottom, go.Spot.Bottom, true, true)
  );
// End Node Template
var endTemplate =
  $(go.Node, "Table", nodeStyle(),
    $(go.Panel, "Spot",
      $(go.Shape, "RoundedRectangle",
        { desiredSize: new go.Size(90, 70), fill: "#dcd4cd", stroke: "#09d3ac", strokeWidth: 3.5 }),
      $(go.TextBlock, "End", textStyle(),
        new go.Binding("text", "key"),

        ),
        
    ),
    new go.Binding("location", "loc"),
    // one named ports, on left incoming only:
    makePort("L", go.Spot.Left, go.Spot.Left, false, true),
  );

//Define the Style of the Text
function textStyle() {
  return {
    font: "bold 11pt Lato, Helvetica, Arial, sans-serif",
    stroke: "#F8F8F8",
    wrap: go.TextBlock.WrapFit,
    maxSize: new go.Size(80, NaN)

    
  }
}

// Define a function for creating a "port" that is normally transparent.
// The "name" is used as the GraphObject.portId,
// the "align" is used to determine where to position the port relative to the body of the node,
// the "spot" is used to control how links connect with the port and whether the port
// stretches along the side of the node,
// and the boolean "output" and "input" arguments control whether the user can draw links from or to the port.
function makePort(name, align, spot, output, input) {
  var horizontal = align.equals(go.Spot.Top) || align.equals(go.Spot.Bottom);
  // the port is basically just a transparent rectangle that stretches along the side of the node,
  // and becomes colored when the mouse passes over it
  return $(go.Shape,
    {
      fill: "transparent",  // changed to a color in the mouseEnter event handler
      strokeWidth: 0,  // no stroke
      width: horizontal ? NaN : 8,  // if not stretching horizontally, just 8 wide
      height: !horizontal ? NaN : 8,  // if not stretching vertically, just 8 tall
      alignment: align,  // align the port on the main Shape
      stretch: (horizontal ? go.GraphObject.Horizontal : go.GraphObject.Vertical),
      portId: name,  // declare this object to be a "port"
      fromSpot: spot,  // declare where links may connect at this port
      fromLinkable: output,  // declare whether the user may draw links from here
      toSpot: spot,  // declare where links may connect at this port
      toLinkable: input,  // declare whether the user may draw links to here
      cursor: "pointer",  // show a different cursor to indicate potential link point
      toMaxLinks: 1, //Gets or sets the maximum number of links that may go into this port
      fromMaxLinks: 1, //Gets or sets the maximum number of links that may come out of this port. 

      mouseEnter: function (e, port) {  // the PORT argument will be this Shape
        if (!e.diagram.isReadOnly) port.fill = "rgba(255,0,255,0.5)";
      },
      mouseLeave: function (e, port) {
        port.fill = "transparent";
      }
    });
 } 

templmap.add("start", startTemplate);
templmap.add("end", endTemplate);
dia.nodeTemplateMap = templmap;
// define the Node template
dia.nodeTemplate =
$(go.Node, "Table", nodeStyle(),
$(go.Panel, "Spot",
  $(go.Shape, "RoundedRectangle",
    { desiredSize: new go.Size(90, 70), fill: "#dcd4cd", stroke: "#09d3ac", strokeWidth: 3.5 }),
  $(go.TextBlock, "End", textStyle(),
    new go.Binding("text", "key")),
),
new go.Binding("location", "loc"),
// Two named ports, on left incoming only
makePort("L", go.Spot.Left, go.Spot.Left, true, true),
makePort("R", go.Spot.Right, go.Spot.Right, true, true),
);

templmap.add("start", startTemplate);
templmap.add("end", endTemplate);
// for the default category, "", use the same template that Diagrams use by default;
// this just shows the key value as a simple TextBlock
templmap.add("", dia.nodeTemplate);

dia.nodeTemplateMap = templmap;

function nodeStyle() {
  return [
    // The Node.location comes from the "loc" property of the node data,
    // converted by the Point.parse static method.
    // If the Node.location is changed, it updates the "loc" property of the node data,
    // converting back using the Point.stringify static method.
    new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
    {
      // the Node.location is at the center of each node
      locationSpot: go.Spot.Center
    }
  ];
}

   return dia;
  } 
}
  1. What do you mean by “move the node around it again gets linked to the previous node”? Can you move the node at all?

  2. If you only want one link to be able to be drawn between any two nodes, please read GoJS Validation -- Northwoods Software. There are various ways to interpret that requirement, so you can customize the link validation predicate to meet your requirements.

  3. Are you saying that after the user selects a link, the user cannot drag either of the relinking handles at the ends of the link? Or that those diamond-shaped relinking handles aren’t even showing when the link is selected? Of course you have imposed some additional link validation constraints, in addition to the standard ones, so the user cannot relink arbitrarily.

  4. What is the problem? That the user cannot give keyboard focus to the diagram? That when they type Control/Command-Z after a change nothing happens? What is the value of myDiagram.commandHandler.canUndo() then?

For 3. The Diamond shape relinking handles appear when the link is selected but it does not attach it to the port of the other node. In below image there is an existing link between Start and End Node. I am trying to remove the link from End and trying to attach to Target2.

If the user cannot successfully reconnect that link between Start and End, such a link must be invalid. The reasons that link validation may fail are discussed at GoJS Validation -- Northwoods Software.