documentationfor yFiles for HTML 3.0.0.1

Creating Edges

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

CreateEdgeInputMode allows you to customize edge creation in various ways using these properties:

startOverCandidateOnly
Specifies whether edge creation must begin directly from a start port candidate or if it can start anywhere over the port candidate’s owner (typically a node). By default, this property is set to true, meaning edges can only be created by dragging from a specific candidate.
showPortCandidates
showStartPortCandidateDelay
Determines whether to show port candidates for start and end nodes and whether to display start port candidates (if enabled) only after a delay. By default, candidates are shown for both start and target ports, and the start port candidates are shown without delay.
allowCreateBend
Controls whether bends can be added during edge creation. Note that to disallow creating bends entirely, bend creation for existing edges must be disabled, too.
edgeDefaults
Specifies the defaults for newly-created edges, most notably their style. This works the same as setting defaults for new edges on a graph.
forceSnapToCandidate
Specifies whether the edge’s end should snap to the nearest end candidate if the mouse hovers over an end port candidate owner. This is enabled by default.
allowSelfLoops
minimumSelfLoopBendCount
Specifies whether a self-loop, i.e. an edge that connects a node with itself, can be created, and how many bends it must have at least.

The actual edge creation is done in method createEdge which 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,
  previewEdge
) => {
  // the targetCandidate might be null if the edge creation ended prematurely
  if (targetCandidate === null || previewEdge === 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, previewEdge.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,
  previewEdge: IEdge | null
) => {
  // the targetCandidate might be null if the edge creation ended prematurely
  if (targetCandidate === null || previewEdge === 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, previewEdge.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.

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

Related events
Event Occurs when …​
gesture-starting…​ the edge creation gesture is initiated. This happens before the edge becomes visible and marks the beginning of the interaction sequence. This event is raised before the preparations start.
gesture-started…​ the edge creation gesture is initiated, indicating that the gesture has been fully initialized and is now in progress. This event precedes any visible feedback or edge creation operations. This event is raised after the preparations have been finished.
edge-creation-started…​ the edge creation has started, i.e., when the cursor is dragged over the source node and the creation visibly begins.
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.
start-port-candidate-changed…​ the value of startPortCandidate changes.
end-port-candidate-changed…​ the value of endPortCandidate changes.
gesture-canceling…​ the edge creation is about to be aborted before the input mode has been reset to its initial state.
gesture-canceled…​ the edge creation has been aborted and the input mode has been reset to its initial state.
gesture-finishing…​ the edge creation gesture is about be finished.
port-added…​ a new port is created during edge creation.
edge-created…​ an edge has been created. The new edge is already present in the graph at this point.
gesture-finished…​ 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.

During edge creation, the cursor changes to indicate possible interactions. The following properties allow to change the cursor for each interaction:

validBeginCursor
The mouse cursor to show when edge creation can start. This is usually when hovering over a source port candidate.
startPortOwnerDraggingCursor
The mouse cursor to show when edge creation has started but the mouse is still hovering over the source port candidate owner.
dragCursor
The mouse cursor to show while edge creation when there is no valid location to create a bend. This is usually when hovering over a source port candidate.
validBendCursor
The mouse cursor to show when a bend can be added. This is usually when the mouse pointer is over an empty part of the canvas during edge creation.
validEndCursor
The mouse cursor to show when edge creation can finish. This is usually when hovering over a possible target node during edge creation.

Changing the Appearance of the Edge to be Created

During edge creation, a preview of the new edge is displayed. The IEdge instance that represents this preview edge can be retrieved via the previewEdge property. With the default edgeCreator, the newly created edge will adopt the preview’s appearance. You can change the appearance of the preview edge (e.g., setting a different style or adding labels) before the gesture using CreateEdgeInputMode's edgeDefaults or during the gesture using the graph provided by previewGraph.

The latter option allows for more complex customizations. By default, all properties of the preview edge are copied to the final edge that is created. 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. The changes are applied in real-time while the gesture is performed. In contrast, changes made by the edgeCreator take effect only after the edge creation gesture is completed.

// add an event handler for the gesture-starting event that adds a label to the preview edge
graphEditorInputMode.createEdgeInputMode.addEventListener(
  'gesture-starting',
  (_, sender) => {
    const createEdgeInputMode = sender
    createEdgeInputMode.previewGraph.addLabel(
      createEdgeInputMode.previewEdge,
      'A new edge'
    )
  }
)

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

Port Candidates and Port Candidate Providers

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

An IPortCandidate represents a potential port. This may be an existing port or a placeholder for a port that will be created, depending on the implementation. Additionally, an IPortCandidate has a validity that determines how (or if) edges can connect to it. The CreateEdgeInputMode can potentially connect the edge being 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, providing a better user experience than simply not providing a port candidate. They also distinguish 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 requires a gesture, such as holding down the Control key.

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

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

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

Implementers must implement the methods above to return an appropriate list of candidates. This list may be empty (or contain only invalid candidates) if an edge should not connect to the owner associated with the provider.

The PortCandidateProviderBase class simplifies 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 that you can use to easily create port candidates: the addExistingPorts, and createCandidate.

The following example shows a simple port candidate provider that 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 {
  owner
  // each instance is built for a specific port owner
  constructor(owner) {
    super()
    this.owner = owner
  }

  // the port candidate list which is returned by default
  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
  getTargetPortCandidates(context, source) {
    return this.owner.ports
      .map((port) => {
        // create a candidate
        const pc = new PortCandidate(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 PortCandidate(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 must be modified to return an instance of this provider. You can do this using a NodeDecorator (or EdgeDecorator in the case of edge-to-edge connections):

graph.decorator.nodes.portCandidateProvider.addFactory(
  (node) => new SameTagPortCandidateProvider(node)
)

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

Predefined port candidate providers
Factory Method Description
NO_CANDIDATES (constant)An IPortCandidateProvider that provides no candidates. This effectively disables edge creation.
fromExistingPorts(IPortOwner)Creates an IPortCandidateProvider that returns candidates for all ports held by the given port owner.
fromUnoccupiedPorts(IPortOwner)Creates an IPortCandidateProvider that returns candidates for all unoccupied ports held by the given port owner.
fromNodeCenter(INode)Creates an IPortCandidateProvider that returns a single candidate at the given node’s center.
fromPortDefaults(IPortOwner)Creates an IPortCandidateProvider that returns a single candidate at the default location.
fromCandidates(IEnumerable<IPortCandidate>)Creates an IPortCandidateProvider that returns the given list of candidates.
fromShapeGeometry(IPortOwner, number...)Creates an IPortCandidateProvider that places a list of port candidates along the node’s outline using the given ratios (nodes only).
combine(IEnumerable<IPortCandidateProvider>)Creates 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

The CreateEdgeInputMode displays port candidates during edge creation. Its showPortCandidates property determines which port candidates should be displayed.

Displaying port candidates
ShowPortCandidates value Description
NONEDo not show any port candidates.
STARTShow port candidates when starting edge creation.
ENDShow port candidates for finishing edge creation.
ALLShow port candidates for both starting and finishing edge creation.

By default, start port candidates are displayed when the cursor hovers over a node (or port owner) that provides some. Similarly, during edge creation, end port candidates are displayed when the cursor hovers over a node (or port owner) that provides some.

graphEditorInputMode.createEdgeInputMode.showPortCandidates =
  ShowPortCandidates.ALL

Showing port candidates for both the start and the end
Starting edge creation
Creating a bend
Finishing edge creation

Creating Edge-to-Edge Connections

The CreateEdgeInputMode also supports creating edges that start or end at other edges. Note that not all features of yFiles for HTML fully support edge-to-edge connections. See Edge to Edge Connections for limitations.

Creating an edge between edges
customizing interaction edge to edge

To enable creating an edge from another edge, set allowEdgeToEdgeConnections to true. Additionally, edges must return a IPortCandidateProvider in their lookup.

graphEditorInputMode.createEdgeInputMode.allowEdgeToEdgeConnections = true
graph.decorator.edges.portCandidateProvider.addFactory((edge) =>
  IPortCandidateProvider.fromPortDefaults(edge)
)

Example: Triggering Edge Creation Gesture Programmatically

There are scenarios in which you might want to trigger the edge creation gesture of the CreateEdgeInputMode programmatically. For example, you might want it to start automatically after another action has finished, such as immediately after creating a new node. Or, you might want it to start when the user clicks a button in the context menu or on the toolbar.

The CreateEdgeInputMode provides the startEdgeCreation method to start edge creation programmatically.

If you want to disable gesture-based edge creation, setting beginRecognizer to NEVER is sufficient.

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

async function startEdgeCreation(inputMode, node) {
  // starts edge creation on the given node
  const edge = await inputMode.createEdgeInputMode.startEdgeCreation(
    new PortCandidate(node, FreeNodePortLocationModel.CENTER)
  )
  if (edge == null) {
    // if edge creation has been cancelled the created edge is null
    return
  }
}

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

async function startEdgeCreation(
  inputMode: GraphEditorInputMode,
  node: INode
): Promise<void> {
  // starts edge creation on the given node
  const edge = await inputMode.createEdgeInputMode.startEdgeCreation(
    new PortCandidate(node, FreeNodePortLocationModel.CENTER)
  )
  if (edge == null) {
    // if edge creation has been cancelled the created edge is null
    return
  }
}

startEdgeCreation asynchronously returns the created edge or null if edge creation has been cancelled.

Example: Starting Edge Creation Inside the Source Node

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

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

To immediately start edge creation upon dragging from the source node, 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
  // support self-loops without bends
  mode.createEdgeInputMode.minimumSelfLoopBendCount = 0
  // start edge creation immediately
  mode.createEdgeInputMode.startPortOwnerDraggingFinishedRecognizer =
    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
  // support self-loops without bends
  mode.createEdgeInputMode.minimumSelfLoopBendCount = 0
  // start edge creation immediately
  mode.createEdgeInputMode.startPortOwnerDraggingFinishedRecognizer =
    EventRecognizers.ALWAYS
}

Example: Configuring CreateEdgeInputMode to Create Subtrees

In this example, we demonstrate how to use various settings to configure CreateEdgeInputMode for a different behavior.

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

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.snapToEndCandidate = false
// don't even show them
createEdgeInputMode.showPortCandidates = ShowPortCandidates.START
// ending at the same node is no option, either
createEdgeInputMode.allowSelfLoops = false
// don't highlight target nodes
graphComponent.graph.decorator.nodes.highlightRenderer.hide()

Furthermore, we create a new node at the target of the edge. Therefore, we want to show the new node in the preview. Actually, we always have a target node during edge creation. However, it is invisible because it has size (0,0) and the invisible VOID_NODE_STYLE. To show the preview target node, we have to set the defaults of the previewGraph:

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

Finally, we have to use an edgeCreator that not only creates 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,
  startPortCandidate,
  endPortCandidate,
  previewEdge
) => {
  // copy the style from the preview node
  const previewEndNode = createEdgeInputMode.previewEndNode
  const node = graph.createNode(
    previewEndNode.layout.toRect(),
    previewEndNode.style,
    previewEndNode.tag
  )
  // create a port at the center
  const targetPort = graph.addPort(
    node,
    createEdgeInputMode.previewEndNodePort.locationParameter
  )
  // create the edge from the source port candidate to the new node
  return graph.createEdge(
    startPortCandidate.createPort(context),
    targetPort,
    previewEdge.style
  )
}
// let the EdgeCreator create a new target node and connect the new edge to it
createEdgeInputMode.edgeCreator = (
  context: any,
  graph: IGraph,
  startPortCandidate: any,
  endPortCandidate: any,
  previewEdge: any
) => {
  // copy the style from the preview node
  const previewEndNode = createEdgeInputMode.previewEndNode
  const node = graph.createNode(
    previewEndNode.layout.toRect(),
    previewEndNode.style,
    previewEndNode.tag
  )
  // create a port at the center
  const targetPort = graph.addPort(
    node,
    createEdgeInputMode.previewEndNodePort.locationParameter
  )
  // create the edge from the source port candidate to the new node
  return graph.createEdge(
    startPortCandidate.createPort(context),
    targetPort,
    previewEdge.style
  )
}

That’s it: now the re-configured CreateEdgeInputMode no longer connects two existing nodes. Instead, it creates a new connected node.

To run a layout after each edge creation, you can listen to the edge-created event.

The Tree Layout demo shows how to configure CreateEdgeInputMode to create sub trees.