Integrating GoJS 1.8.17 with a Angular App

Hi,

our company currently evaluates GoJS. We are essentially looking for a functionality similar to your dynamic ports example.

In order to get startet we used your example for Angular.

We were trying to convert your dynamic port example to TS without changes and work our way from there. However we always end up with the ports not beeing linked correctly.

All links are beeing drawn at the exact same starting and end point.

It appears that the fromPort and toPort are not set/interpreted correctly for some reason.

Angular versions:

Angular CLI: 1.7.4
Node: 8.10.0
OS: win32 x64
Angular: 5.2.10
... animations, common, compiler, compiler-cli, core, forms
... http, language-service, platform-browser
... platform-browser-dynamic, router

@angular/cli: 1.7.4
@angular/flex-layout: 5.0.0-beta.14
@angular/material: 5.2.5
@angular-devkit/build-optimizer: 0.3.2
@angular-devkit/core: 0.3.2
@angular-devkit/schematics: 0.3.2
@ngtools/json-schema: 1.2.0
@ngtools/webpack: 1.10.2
@schematics/angular: 0.3.2
@schematics/package-update: 0.3.2
typescript: 2.5.3
webpack: 3.11.0

diagram.component.ts

import { Component, OnInit, ViewChild, ElementRef, Input, Output, EventEmitter } from '@angular/core';
import { GoLinkTemplate } from './go.linktemplate';
import { GoNodeTemplate } from './go.nodetemplate';
import * as go from 'gojs';

@Component({
  selector: 'app-diagram',
  templateUrl: './diagram.component.html',
  styleUrls: ['./diagram.component.css']
})
export class DiagramComponent implements OnInit {
  private _diagram: go.Diagram = new go.Diagram();
  private _palette: go.Palette = new go.Palette();

  @ViewChild('diagramDiv')
  private _diagramRef: ElementRef;

  @Input()
  get model(): go.Model { return this._diagram.model; }
  set model(val: go.Model) { this._diagram.model = val; }

  @Output()
  nodeSelected = new EventEmitter<go.Node|null>();

  @Output()
  modelChanged = new EventEmitter<go.ChangedEvent>();

  constructor() {
    const $ = go.GraphObject.make;
    this._diagram = new go.Diagram();
    this._diagram.initialContentAlignment = go.Spot.Center;
    this._diagram.allowDrop = true;  // necessary for dragging from _palette
    this._diagram.undoManager.isEnabled = true;
    this._diagram.addDiagramListener('ChangedSelection',
        e => {
          const node = e.diagram.selection.first();
          this.nodeSelected.emit(node instanceof go.Node ? node : null);
        });
    this._diagram.addModelChangedListener(e => e.isTransactionFinished && this.modelChanged.emit(e));

    this._diagram.nodeTemplate = GoNodeTemplate.getSampleNodeTemplate();
    this._diagram.linkTemplate = GoLinkTemplate.getLinkTemplate();
  }

  ngOnInit() {
    this._diagram.div = this._diagramRef.nativeElement;
  }

  saveDiagram() {
    return this._diagram.model.toJson();
  }
}

go.linktemplate.ts

import { CustomLink } from './go.customlink';
import * as go from 'gojs';

export class GoLinkTemplate {

  public static getLinkTemplate(): go.Link {
  const $ = go.GraphObject.make;
  go.Diagram.inherit(CustomLink, go.Link);
  const link: go.Link =
    $(CustomLink,
      {
        routing: go.Link.AvoidsNodes,
        corner: 4,
        curve: go.Link.JumpGap,
        reshapable: true,
        resegmentable: true,
        relinkableFrom: true,
        relinkableTo: true
      },
      new go.Binding('points').makeTwoWay(),
      $(go.Shape, { stroke: '#2F4F4F', strokeWidth: 2, toArrow: 'OpenTriangle' })
    );

    return link;
  }
}

go.customlink.ts

import * as go from 'gojs';

export class CustomLink extends go.Link {
  private _baseType;

  constructor() {
    super();
    this._baseType = Object.getPrototypeOf(go.Link);
  }

  /** @override */
  hasCurviness() {
    if (isNaN(this.curviness)) {
      return true;
    }

    return this._baseType.hasCurviness();
  }

  /** @override */
  findSidePortIndexAndCount(node, port) {
    const nodedata = node.data;
    let len = 0;

    if (nodedata !== null) {
      const portdata = port.data;
      const side = port._side;
      const arr = nodedata[side + 'Array'];

      len = arr.length;
      for (let i = 0; i < len; i++) {
        if (arr[i] === portdata) {
          return [i, len];
        }
      }
    }
    return [-1, len];
  }

  /** @override */
  computeCurviness() {
    if (isNaN(this.curviness)) {
      const fromnode = this.fromNode;
      const fromport = this.fromPort;
      const fromspot = this.computeSpot(true);
      const frompt = fromport.getDocumentPoint(fromspot);
      const tonode = this.toNode;
      const toport = this.toPort;
      const tospot = this.computeSpot(false);
      const topt = toport.getDocumentPoint(tospot);

      if (Math.abs(frompt.x - topt.x) > 20 || Math.abs(frompt.y - topt.y) > 20) {
        if ((fromspot.equals(go.Spot.Left) || fromspot.equals(go.Spot.Right)) &&
            (tospot.equals(go.Spot.Left) || tospot.equals(go.Spot.Right))) {
          const fromseglen = this.computeEndSegmentLength(fromnode, fromport, fromspot, true);
          const toseglen = this.computeEndSegmentLength(tonode, toport, tospot, false);
          const c = (fromseglen - toseglen) / 2;

          if (frompt.x + fromseglen >= topt.x - toseglen) {
            if (frompt.y < topt.y) {
              return c;
            }
            if (frompt.y > topt.y) {
              return -c;
            }
          }
        } else if ((fromspot.equals(go.Spot.Top) || fromspot.equals(go.Spot.Bottom)) &&
                   (tospot.equals(go.Spot.Top) || tospot.equals(go.Spot.Bottom))) {
          const fromseglen = this.computeEndSegmentLength(fromnode, fromport, fromspot, true);
          const toseglen = this.computeEndSegmentLength(tonode, toport, tospot, false);
          const c = (fromseglen - toseglen) / 2;
          if (frompt.x + fromseglen >= topt.x - toseglen) {
            if (frompt.y < topt.y) {
              return c;
            }
            if (frompt.y > topt.y) {
              return -c;
            }
          }
        }
      }
    }

    return this._baseType.computeCurviness.call(this);
  }

  /** @override */
  computeEndSegmentLength(node, port, spot, from) {
    const esl = this._baseType.computeEndSegmentLength.call(this, node, port, spot, from);
    const other = this.getOtherPort(port);

    if (port !== null && other !== null) {
      const thispt = port.getDocumentPoint(this.computeSpot(from));
      const otherpt = other.getDocumentPoint(this.computeSpot(!from));
      if (Math.abs(thispt.x - otherpt.x) > 20 || Math.abs(thispt.y - otherpt.y) > 20) {
        const info = this.findSidePortIndexAndCount(node, port);
        const idx = info[0];
        const count = info[1];

        if (port._side === 'left' || port._side === 'right') {
          if (otherpt.y < thispt.y) {
            return esl + 4 + idx * 8;
          } else {
            return esl + (count - idx - 1) * 8;
          }
        }
      }
    }
    return esl;
  }
}

go.nodetemplate.ts

import * as go from 'gojs';

export class GoNodeTemplate {

  public static getSampleNodeTemplate(): go.Node {
    const portSize = new go.Size(8, 8);
    const $ = go.GraphObject.make;
    const node: go.Node =
      $(go.Node, 'Table',
      { locationObjectName: 'BODY',
        locationSpot: go.Spot.Center,
        selectionObjectName: 'BODY'
      },
      new go.Binding('location', 'loc', go.Point.parse).makeTwoWay(go.Point.stringify),

      // the body
      $(go.Panel, 'Auto',
        { row: 1, column: 1, name: 'BODY',
          stretch: go.GraphObject.Fill },
        $(go.Shape, 'Rectangle',
          { fill: '#AC193D', stroke: null, strokeWidth: 0,
            minSize: new go.Size(56, 56) }),
        $(go.TextBlock,
          { margin: 10, textAlign: 'center', font: '14px  Segoe UI,sans-serif', stroke: 'white', editable: true },
          new go.Binding('text', 'name').makeTwoWay())
      ),  // end Auto Panel body

      // the Panel holding the left port elements, which are themselves Panels,
      // created for each item in the itemArray, bound to data.leftArray
      $(go.Panel, 'Vertical',
        new go.Binding('itemArray', 'leftArray'),
        { row: 1, column: 0,
          itemTemplate:
            $(go.Panel,
              { _side: 'left',  // internal property to make it easier to tell which side it's on
                fromSpot: go.Spot.Left, toSpot: go.Spot.Left,
                fromLinkable: true, toLinkable: true, cursor: 'pointer'},
              new go.Binding('portId', 'portId'),
              $(go.Shape, 'Rectangle',
                { stroke: null, strokeWidth: 0,
                  desiredSize: portSize,
                  margin: new go.Margin(1,  0) },
                new go.Binding('fill', 'portColor'))
            )  // end itemTemplate
        }
      ),  // end Vertical Panel

      // the Panel holding the right port elements, which are themselves Panels,
      // created for each item in the itemArray, bound to data.rightArray
      $(go.Panel, 'Vertical',
        new go.Binding('itemArray', 'rightArray'),
        { row: 1, column: 2,
          itemTemplate:
            $(go.Panel,
              { _side: 'right',
                fromSpot: go.Spot.Right, toSpot: go.Spot.Right,
                fromLinkable: true, toLinkable: true, cursor: 'pointer' },
              new go.Binding('portId', 'portId'),
              $(go.Shape, 'Rectangle',
                { stroke: null, strokeWidth: 0,
                  desiredSize: portSize,
                  margin: new go.Margin(1, 0) },
                new go.Binding('fill', 'portColor'))
            )  // end itemTemplate
        }
      ) // end Vertical Panel
    );  // end Node

    return node;
  }
}

You need to set https://gojs.net/latest/api/symbols/GraphLinksModel.html#linkFromPortIdProperty and https://gojs.net/latest/api/symbols/GraphLinksModel.html#linkToPortIdProperty and make sure any initial link data have the correct values for those two named properties.

This talks about the problem:

Fantastic - works great! In the home component we are setting up the model like this:

export class HomeComponent implements OnInit {
  data: any;
  node: go.Node;
  model = new go.GraphLinksModel();
  private _id = 2;

   @ViewChild('text')
   private textField: ElementRef;

   constructor() { }

   ngOnInit() {
     this.model.copiesArrays = true;
     this.model.copiesArrayObjects = true;
     this.model.linkFromPortIdProperty = 'fromPort';
     this.model.linkToPortIdProperty = 'toPort';
     this.model.nodeDataArray = [
       { key: 1, name: 'Unit 1', loc: '100 200', rightArray: [{ portId: 'right1' }, { portId: 'right2' }] },
       { key: 2, name: 'Unit 2', loc: '200 200', leftArray: [{ portId: 'left1' }, { portId: 'left2' }] }
     ];
   }
...

Thanks for the quick help!