documentationfor yFiles for HTML 2.6

Creating Edges

Edge creation is handled by CreateEdgeInputMode, mostly used as a child input mode of GraphEditorInputMode.

The actual edge creation is done in method createEdge which itself delegates to the callback set in edgeCreator. While it is possible to customize the actual edge creation by overriding createEdge setting a new edgeCreator should be preferred:

// set a custom edge creator which creates a label on the new edge
graphEditorInputMode.createEdgeInputMode.edgeCreator = (
  context,
  graph,
  sourceCandidate,
  targetCandidate,
  dummyEdge
) => {
  // the targetCandidate might be null if the edge creation ended prematurely
  if (targetCandidate === null || dummyEdge === null) {
    // no target - no edge
    return null
  }
  // get the source and target ports from the candidates
  const sourcePort = sourceCandidate.port || sourceCandidate.createPort(context)
  const targetPort = targetCandidate.port || targetCandidate.createPort(context)
  // create the edge between the source and target port
  const edge = graph.createEdge(sourcePort, targetPort, dummyEdge.style)
  // create a label
  graph.addLabel(edge, 'A new edge')
  // return the created edge
  return edge
}
// set a custom edge creator which creates a label on the new edge
graphEditorInputMode.createEdgeInputMode.edgeCreator = (
  context: IInputModeContext,
  graph: IGraph,
  sourceCandidate: IPortCandidate,
  targetCandidate: IPortCandidate | null,
  dummyEdge: IEdge | null
) => {
  // the targetCandidate might be null if the edge creation ended prematurely
  if (targetCandidate === null || dummyEdge === null) {
    // no target - no edge
    return null
  }
  // get the source and target ports from the candidates
  const sourcePort = sourceCandidate.port || sourceCandidate.createPort(context)
  const targetPort = targetCandidate.port || targetCandidate.createPort(context)
  // create the edge between the source and target port
  const edge = graph.createEdge(sourcePort, targetPort, dummyEdge.style)
  // create a label
  graph.addLabel(edge, 'A new edge')
  // return the created edge
  return edge
}

If prematureEndHitTestable is not changed the edgeCreator's targetCandidate parameter is never null.

During edge creation a preview of the new edge is shown. The IEdge instance which represents this preview edge can be retrieved via the dummyEdge property. You can change this preview edge (e.g. setting a different style, or adding labels) using the graph provided by dummyEdgeGraph.

By default, all properties of the preview edge are copied to the final edge that is created eventually. This includes bends, style, labels, and the edge’s tag. Thus, a viable option for customizing the created edge can also be to change the preview edge during the gesture, and have those changes copied automatically to the final edge. This has the benefit that the changes show up during the gesture, as opposed to changes done by the edgeCreator, which are only applied after the edge creation gesture is already complete:

// add an event handler for the GestureStarting event that adds a label to the dummy edge
graphEditorInputMode.createEdgeInputMode.addGestureStartingListener((sender, args) => {
  const createEdgeInputMode = sender
  createEdgeInputMode.dummyEdgeGraph.addLabel(createEdgeInputMode.dummyEdge, 'A new edge')
})

The preview edge is reset to its default state after every edge creation gesture, so no further cleanup is necessary to prevent preview edge changes from persisting over multiple gestures.

The following events occur during an edge creation gesture (in the order they might occur):

Related events
Event Occurs when …​
EdgeCreationStarted…​ the edge creation has started, i.e., when the cursor is dragged over the source node.
GestureStarting…​ CreateEdgeInputMode begins its actual creation gesture (i.e., starting to drag the edge from its source node). This event is raised before the preparations start.
GestureStarted…​ CreateEdgeInputMode begins its actual creation gesture (i.e., starting to drag the edge from its source node). This event is raised after the preparations have been finished.
Moving…​ the mouse moved during the drag operation and before any actions related to that drag are performed.
Moved…​ the mouse moved during the drag operation and after any actions related to that drag have been performed.
SourcePortCandidateChanged…​ the value of sourcePortCandidate changes.
TargetPortCandidateChanged…​ the value of targetPortCandidate changes.
GestureCanceling…​ the edge creation is about to be aborted, before the input mode has been reset to its initial state.
GestureCanceled…​ the edge creation has been aborted and the input mode has been reset to its initial state.
GestureFinishing…​ the edge creation gesture is about be finished.
PortAdded…​ a new port is created during edge creation.
EdgeCreated…​ an edge has been created. The new edge is already present in the graph at this point.
GestureFinished…​ the edge creation gesture has been completely finished, i.e. after the edge has been created and the input mode has been reset to its initial state.

Port Candidates and Port Candidate Providers

Fine-grained control where edges can and cannot connect during interactive edge creation is possible using port candidates: CreateEdgeInputMode retrieves an IPortCandidateProvider to find potential candidates for both source and target ports. The provider returns a set of port candidates which represent potential connection points for source and target ports for the edge to be created.

An IPortCandidate represents a potential port. Depending on the implementation this can be an existing port, but could also be a placeholder for a port yet to be created. Furthermore, an IPortCandidate has a validity that changes how (or if) edges can connect to it. CreateEdgeInputMode can potentially connect the edge to be created to a valid port candidate but not to an invalid one.

Port candidates and edge creation: no candidates, invalid candidate, valid candidate
Invalid port candidates add visual feedback to edge creation: the red square of the invalid port candidate in the middle shows explicitly that it is not allowed to connect to that node.

Note that invalid port candidates are visually indicated, thus providing a better user experience than simply not providing a port candidate. They can also help distinguishing the case “no edge may connect here” (no candidate) from “this edge may not connect here” (invalid candidate) — similar to disabled UI controls.

A third kind of validity is DYNAMIC: a dynamic port candidate has no fixed position but will be placed at the current (cursor) location. This usually has to be accompanied by a gesture, usually the Shift ⇧ key held down.

When edge creation is finished IPortCandidate’s port property is queried first. If the candidate represents an existing port, that port is used. If not, the createPort method is called by CreateEdgeInputMode to create and provide an appropriate port.

IPortCandidates are provided by an IPortCandidateProvider which is retrieved from the lookup of a potential port owner. During edge creation CreateEdgeInputMode calls the following methods:

Method Description
getAllSourcePortCandidates(IInputModeContext)Called at the beginning of edge creation for a list of all possible candidates for an edge’s source port.
getTargetPortCandidates(IInputModeContext, IPortCandidate)Called during edge creation for a list of all possible candidates for a target port for an edge which starts at the given source port candidate.
getAllTargetPortCandidates(IInputModeContext)Called during edge creation for a list of all possible candidates for target ports.
getSourcePortCandidates(IInputModeContext, IPortCandidate)Should return a list of possible candidates for source ports opposite to the given target port candidate. Some IEdgeReconnectionPortCandidateProvider implementations may delegate to this method to find a new source port during edge reconnection.

Implementers have to implement above methods to return an appropriate list of candidates. Note that this list may be empty (or contain only invalid candidates) if an edge should not connect to the owner which is related to the provider.

The PortCandidateProviderBase class is a simplification of the IPortCandidateProvider interface. All its IPortCandidateProvider method implementations delegate to the getPortCandidates method and thus all return the same set of candidates. They can also be overridden to return something different, of course. Furthermore, PortCandidateProviderBase provides a set of convenience methods which you can use to easily create port candidates: the addExistingPorts method, and createCandidate.

The following example shows a simple port candidate provider which allows edges to connect to existing ports, but only if the source port has the same tag as the target port:

class SameTagPortCandidateProvider extends PortCandidateProviderBase {
  // each instance is built for a specific port owner
  /**
   * @param {!INode} owner
   */
  constructor(owner) {
    super()
    this.owner = owner
  }

  // the port candidate list which is returned by default
  /**
   * @param {!IInputModeContext} context
   * @returns {!IEnumerable.<IPortCandidate>}
   */
  getPortCandidates(context) {
    // create a new list
    const candidates = new List()
    // add candidates for all existing ports
    this.addExistingPorts(this.owner, candidates)
    return candidates
  }

  // override this method to return candidates for each existing port
  // but mark some as invalid
  /**
   * @param {!IInputModeContext} context
   * @param {!IPortCandidate} source
   * @returns {!IEnumerable.<IPortCandidate>}
   */
  getTargetPortCandidates(context, source) {
    return this.owner.ports
      .map((port) => {
        // create a candidate
        const pc = new DefaultPortCandidate(port)
        // mark it as invalid if the source port's user tag
        // is not equal to the current port's user tag
        pc.validity = port.tag === source.port.tag ? PortCandidateValidity.VALID : PortCandidateValidity.INVALID
        return pc
      })
      .toList()
  }
}class SameTagPortCandidateProvider extends PortCandidateProviderBase {
  // each instance is built for a specific port owner
  constructor(private readonly owner: INode) {
    super()
  }

  // the port candidate list which is returned by default
  getPortCandidates(context: IInputModeContext): IEnumerable<IPortCandidate> {
    // create a new list
    const candidates = new List<IPortCandidate>()
    // add candidates for all existing ports
    this.addExistingPorts(this.owner, candidates)
    return candidates
  }

  // override this method to return candidates for each existing port
  // but mark some as invalid
  getTargetPortCandidates(context: IInputModeContext, source: IPortCandidate): IEnumerable<IPortCandidate> {
    return this.owner.ports
      .map((port) => {
        // create a candidate
        const pc = new DefaultPortCandidate(port)
        // mark it as invalid if the source port's user tag
        // is not equal to the current port's user tag
        pc.validity = port.tag === source.port!.tag ? PortCandidateValidity.VALID : PortCandidateValidity.INVALID
        return pc
      })
      .toList()
  }
}

The port owner’s lookup has to be modified to return an instance of this provider. You can do this using a NodeDecorator (or EdgeDecorator in case of edge to edge connections):

graph.decorator.nodeDecorator.portCandidateProviderDecorator.setFactory(
  (node) => new SameTagPortCandidateProvider(node)
)

By default, a node returns a port candidate provider which returns all unoccupied ports if there is at least one, else it creates a new port candidate for the center of the node. yFiles for HTML already provides a number of IPortCandidateProvider implementations for the most common use cases which are mostly available through factory methods on the IPortCandidateProvider interface:

Predefined port candidate providers
Factory Method Description
NO_CANDIDATES (constant)An IPortCandidateProvider which provides no candidates. This effectively disables edge creation.
fromExistingPortsCreates an IPortCandidateProvider which returns candidates for all ports held by the given port owner.
fromUnoccupiedPortsCreates an IPortCandidateProvider which returns candidates for all unoccupied ports held by the given port owner.
fromNodeCenterCreates an IPortCandidateProvider which returns a single candidate at the given node’s center.
fromPortDefaultsCreates an IPortCandidateProvider which returns a single candidate at the default location.
fromCandidatesCreates an IPortCandidateProvider which returns the given list of candidates.
fromShapeGeometryCreates an IPortCandidateProvider which places a list of port candidates along the node’s outline using the given ratios (nodes only).
combineCreates an IPortCandidateProvider that returns all candidates returned by any of the given providers.

The Port Candidate Provider demo shows how to implement IPortCandidateProvider for different purposes.

Showing Port Candidates while Creating Edges

CreateEdgeInputMode can display port candidates during edge creation. Its showPortCandidates property determines which port candidates should be displayed.

Displaying port candidates
ShowPortCandidates value Description
NONEDon’t show any port candidates.
SOURCEShow port candidates for starting edge creation.
TARGETShow port candidates for possible ends of the newly created edge.
ALLShow port candidates for both the start and the end of the newly created edge.

Setting this property to SOURCE or ALL will result in displaying source port candidates for each node the mouse is hovering over.

graphEditorInputMode.createEdgeInputMode.showPortCandidates = ShowPortCandidates.ALL
Showing port candidates for both the start and the end
Starting edge creation
Creating a bend
Finishing edge creation

For a CreateEdgeInputMode as child of a default GraphEditorInputMode selected nodes will rather be moved than serve as source of an edge creation. There are two ways to solve this problem:

We can change the priority of the MoveInputMode lower (higher value) than the priority of the CreateEdgeInputMode and enable the startOverCandidateOnly property. Now you can create edges directly on the candidate. In places where there is no candidate, the selected node can be moved.

graphEditorInputMode.createEdgeInputMode.startOverCandidateOnly = true
graphEditorInputMode.createEdgeInputMode.priority = 40
graphEditorInputMode.moveInputMode.priority = 45

Or we simply do not show candidates on selected nodes with an appropriate hit testable …​

class UnselectedNodesHitTestable extends BaseClass(IHitTestable) {
  /**
   * @param {!IInputModeContext} context
   * @param {!Point} location
   * @returns {boolean}
   */
  isHit(context, location) {
    const graph = context.graph
    if (graph !== null) {
      const graphComponent = context.canvasComponent
      return graph.nodes.some(
        (node) =>
          !graphComponent.selection.isSelected(node) &&
          node.style.renderer.getHitTestable(node, node.style).isHit(context, location)
      )
    }
    return false
  }
}class UnselectedNodesHitTestable extends BaseClass(IHitTestable) implements IHitTestable {
  isHit(context: IInputModeContext, location: Point): boolean {
    const graph = context.graph
    if (graph !== null) {
      const graphComponent = context.canvasComponent as GraphComponent
      return graph.nodes.some(
        (node) =>
          !graphComponent.selection.isSelected(node) &&
          node.style.renderer.getHitTestable(node, node.style).isHit(context, location)
      )
    }
    return false
  }
}

… and set an instance to the properties showSourcePortCandidatesHitTestable and beginHitTestable.

const hitTestable = new UnselectedNodesHitTestable()
graphEditorInputMode.createEdgeInputMode.showSourcePortCandidatesHitTestable = hitTestable
graphEditorInputMode.createEdgeInputMode.beginHitTestable = hitTestable

Example: Triggering Edge Creation Gesture Programmatically

There are scenarios in which the edge creation gesture of the CreateEdgeInputMode should be triggered programmatically. For instance, it should start automatically after another action has finished, e.g., immediately after create a new node. Or it should start on a button click in the context menu or on the toolbar.

To start the edge creation programmatically, CreateEdgeInputMode provides the doStartEdgeCreation method.

In case the gesture based edge creation should be disabled, setting prepareRecognizer to NEVER is sufficient to disable it.

function configureInteraction() {
  // register a GraphEditorInputMode with a CreateEdgeInputMode that does not allow gesture based edge creation
  const inputMode = new GraphEditorInputMode()
  inputMode.createEdgeInputMode.prepareRecognizer = EventRecognizers.NEVER
  graphComponent.inputMode = inputMode
}

async function startEdgeCreation(inputMode, node) {
  // starts edge creation on the given node
  await inputMode.createEdgeInputMode.doStartEdgeCreation(
    new DefaultPortCandidate(node, FreeNodePortLocationModel.NODE_CENTER_ANCHORED)
  )
}

function configureInteraction(): void {
  // register a GraphEditorInputMode with a CreateEdgeInputMode that does not allow gesture based edge creation
  const inputMode = new GraphEditorInputMode()
  inputMode.createEdgeInputMode.prepareRecognizer = EventRecognizers.NEVER
  graphComponent.inputMode = inputMode
}

async function startEdgeCreation(inputMode: GraphEditorInputMode, node: INode): Promise<void> {
  // starts edge creation on the given node
  await inputMode.createEdgeInputMode.doStartEdgeCreation(
    new DefaultPortCandidate(node, FreeNodePortLocationModel.NODE_CENTER_ANCHORED)
  )
}

Example: Starting Edge Creation Inside the Source Node

By default, CreateEdgeInputMode starts edge creation gesture only after the mouse pointer leaves the source node. In some cases, it makes sense to start immediately when dragging inside the source node. This can be the case if edges should be created between two overlapping nodes, or for the creation of self-loops without the mouse having to leave and enter the node.

To configure when to start edge creation, CreateEdgeInputMode provides the property sourceNodeDraggingFinishedRecognizer.

To immediately start edge creation upon dragging from the source node, it is sufficient to assign ALWAYS.

function configureInteraction(graphComponent) {
  // register a GraphEditorInputMode with a CreateEdgeInputMode that starts edge creation
  // immediately inside the source node
  const mode = new GraphEditorInputMode()
  graphComponent.inputMode = mode
  mode.createEdgeInputMode.sourceNodeDraggingFinishedRecognizer = EventRecognizers.ALWAYS
}

function configureInteraction(graphComponent: GraphComponent): void {
  // register a GraphEditorInputMode with a CreateEdgeInputMode that starts edge creation
  // immediately inside the source node
  const mode = new GraphEditorInputMode()
  graphComponent.inputMode = mode
  mode.createEdgeInputMode.sourceNodeDraggingFinishedRecognizer = EventRecognizers.ALWAYS
}

Example: Configuring CreateEdgeInputMode to Create Sub Trees

In this example we demonstrate the use of various settings to configure CreateEdgeInputMode to a completely different behavior.

By default, CreateEdgeInputMode can only connect two existing nodes. It is, however, possible to let the edge creation end anywhere (“prematurely”) and create a new target node at the end point.

First, we configure CreateEdgeInputMode not to end at other nodes by setting the endHitTestable to IHitTestable.NEVER. Instead, we support ending at any location (“prematurely”) by setting the prematureEndHitTestable to IHitTestable.ALWAYS.

// never search for target ports
createEdgeInputMode.endHitTestable = IHitTestable.NEVER
// any location is a valid target location
createEdgeInputMode.prematureEndHitTestable = IHitTestable.ALWAYS

Since we don’t want to connect to other nodes we have to turn off snapping to targets. We also turn off showing possible targets.

// don't react to target ports
createEdgeInputMode.forceSnapToCandidate = false
createEdgeInputMode.snapToTargetCandidate = false
// don't even show them
createEdgeInputMode.showPortCandidates = ShowPortCandidates.NONE
createEdgeInputMode.allowSelfloops = false
// don't highlight target nodes
graphComponent.graph.decorator.nodeDecorator.highlightDecorator.hideImplementation()

Since we always create a new node at the target of the edge we want to show the new node already in the preview. Actually, we always have a target node during edge creation. It is, however, invisible because it has size (0,0) and the invisible VoidNodeStyle. To show the the preview target node we have to set the defaults of the dummyEdgeGraph:

// provide a default size
createEdgeInputMode.dummyEdgeGraph.nodeDefaults.size = graphComponent.graph.nodeDefaults.size
// set a style to become visible
createEdgeInputMode.dummyEdgeGraph.nodeDefaults.style = graphComponent.graph.nodeDefaults.style

Finally, we have to use an edgeCreator which does not only create a new edge but also a target node to connect the edge to:

// let the EdgeCreator create a new target node and connect the new edge to it
createEdgeInputMode.edgeCreator = (context, graph, sourcePortCandidate, targetPortCandidate, dummyEdge) => {
  // copy the style from the dummy node
  const dummyTargetNode = createEdgeInputMode.dummyTargetNode
  const node = graph.createNode(dummyTargetNode.layout.toRect(), dummyTargetNode.style, dummyTargetNode.tag)
  // create a port at the center
  const targetPort = graph.addPort(node, createEdgeInputMode.dummyTargetNodePort.locationParameter)
  // create the edge from the source port candidate to the new node
  return graph.createEdge(sourcePortCandidate.createPort(context), targetPort, dummyEdge.style)
}
// let the EdgeCreator create a new target node and connect the new edge to it
createEdgeInputMode.edgeCreator = (
  context: any,
  graph: IGraph,
  sourcePortCandidate: any,
  targetPortCandidate: any,
  dummyEdge: any
) => {
  // copy the style from the dummy node
  const dummyTargetNode = createEdgeInputMode.dummyTargetNode
  const node = graph.createNode(dummyTargetNode.layout.toRect(), dummyTargetNode.style, dummyTargetNode.tag)
  // create a port at the center
  const targetPort = graph.addPort(node, createEdgeInputMode.dummyTargetNodePort.locationParameter)
  // create the edge from the source port candidate to the new node
  return graph.createEdge(sourcePortCandidate.createPort(context), targetPort, dummyEdge.style)
}

That’s it: now the re-configured CreateEdgeInputMode doesn’t connect two existing nodes anymore. Instead, it creates a new connected node.

To run a layout after each edge creation you can listen to the EdgeCreated event.