/

Connectors

Connectors are real elements rendered by custom nodes. They identify edge endpoints, provide path direction and can carry custom content, constraints and application data.

df-inputAccepts regular draggable connections.
df-outputStarts a regular connection by default.
Action outputEmits a click request without creating a draft edge.

Connector Data

Both connector types require [connectorData] with a node id, connector id and single constraint. When single is true, a connected input rejects another edge and a connected output cannot start another edge or action.

Connector ids must be unique across the complete graph, not only inside one node. Use a convention such as {nodeId}-input-1 and {nodeId}-output-1 .

Placement and Path Direction

Place connectors with the custom node's CSS. The position input accepts Top , Right , Bottom or Left and tells Bezier or SmoothStep calculation which direction the path should leave or approach. It does not move the connector element.

Custom Content

The [content] input accepts any Polymorpheus content: text, a component or an ng-template . A template receives the complete DfDataConnectorConfig as its implicit context, including optional application data .

task-node.component.html

    
      <ng-template
  #connectorContent
  let-connector
>
  <tui-icon icon="@tui.plus" />
  <span>{{ connector.data?.['label'] }}</span>
</ng-template>

<df-output
  [connectorData]="{
    nodeId: nodeIdSignal(),
    connectorId: nodeIdSignal() + '-output-1',
    single: false,
    data: {label: 'Add child'},
  }"
  [content]="connectorContent"
/>
    

Regular Graph Connectors

DfOutputMode.Connection is the default output mode. A pointer drag starts a draft edge and dropping it on an input appends a connection to the bound model. Regular outputs do not emit application actions.

    
      
    
    
      import {ChangeDetectionStrategy, Component} from '@angular/core';
import {FormControl, ReactiveFormsModule} from '@angular/forms';
import {
    DfArrowhead,
    DfConnectionType,
    type DfDataModel,
    dfPanZoomOptionsProvider,
    NgDrawFlowComponent,
    provideNgDrawFlowConfigs,
} from '@ng-draw-flow/core';

import {ConnectorExampleNodeComponent} from './connector-example-node.component';

@Component({
    standalone: true,
    selector: 'connector-example',
    imports: [NgDrawFlowComponent, ReactiveFormsModule],
    template: `
        <div class="editor">
            <ng-draw-flow [formControl]="form" />
        </div>
    `,
    styles: `
        :host {
            display: block;
        }

        .editor {
            block-size: 22rem;
            background: #fff;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush,
    providers: [
        dfPanZoomOptionsProvider({leftPosition: 60}),
        provideNgDrawFlowConfigs({
            connection: {
                type: DfConnectionType.SmoothStep,
                arrowhead: {type: DfArrowhead.ArrowClosed},
                curvature: 12,
            },
            nodes: {connectorExample: ConnectorExampleNodeComponent},
        }),
    ],
})
export default class ConnectorExampleComponent {
    public readonly form = new FormControl<DfDataModel>(
        {
            nodes: [
                {
                    id: 'source',
                    data: {type: 'connectorExample', text: 'Source node'},
                    position: {x: 0, y: 0},
                    startNode: true,
                },
                {
                    id: 'target',
                    data: {type: 'connectorExample', text: 'Target node'},
                    position: {x: 300, y: 80},
                    endNode: true,
                },
            ],
            connections: [],
        },
        {nonNullable: true},
    );
}

    
    
      import {ChangeDetectionStrategy, Component} from '@angular/core';
import {DfInputComponent, DfOutputComponent, DrawFlowBaseNode} from '@ng-draw-flow/core';

@Component({
    standalone: true,
    selector: 'connector-example-node',
    imports: [DfInputComponent, DfOutputComponent],
    templateUrl: './connector-example-node.component.html',
    styleUrl: './connector-example-node.component.less',
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ConnectorExampleNodeComponent extends DrawFlowBaseNode {}

    
    
      @if (!startNodeSignal()) {
    <df-input
        class="input"
        [connectorData]="{
            nodeId: nodeIdSignal(),
            connectorId: nodeIdSignal() + '-input-1',
            single: false,
        }"
    />
}

<strong>{{ modelSignal().text }}</strong>
<span>{{ startNodeSignal() ? 'Drag the orange output' : 'Drop the edge here' }}</span>

@if (!endNodeSignal()) {
    <df-output
        class="output"
        [connectorData]="{
            nodeId: nodeIdSignal(),
            connectorId: nodeIdSignal() + '-output-1',
            single: false,
        }"
    />
}

    
    
      :host {
    position: relative;
    display: grid;
    gap: 0.25rem;
    inline-size: 11rem;
    min-block-size: 3rem;

    span {
        color: var(--tui-text-secondary);
        font-size: 0.75rem;
    }
}

.input,
.output {
    position: absolute;
    inset-block-start: 50%;
    transform: translateY(-50%);
}

.input {
    inset-inline-start: -1rem;
}

.output {
    inset-inline-end: -1rem;
}

    

Dynamic Layout Connectors

Set DfOutputMode.Action when a click should request an application-owned graph mutation. The output remains visible when regular connection creation is disabled, never starts a draft edge and emits its connector config through (activated) .

tree-node.component.html

    
      <ng-template #addChildIcon>
  <tui-icon icon="@tui.plus" />
</ng-template>

<df-output
  title="Add child"
  [connectorData]="{
    nodeId: nodeIdSignal(),
    connectorId: nodeIdSignal() + '-add-child',
    single: false,
    data: {childType: 'task'},
  }"
  [content]="addChildIcon"
  [layoutOrder]="0"
  [mode]="outputMode.Action"
  (activated)="addChild($event)"
/>
    

The connector does not add a node itself. The handler creates both the child and its edge, then passes the complete next model to the layouts service.

dynamic-tree.component.ts

    
      private readonly autoLayout = inject(DfAutoLayoutService);

readonly outputMode = DfOutputMode;

addChild({nodeId, connectorId}: DfDataConnectorConfig): void {
  const model = this.form.getRawValue();
  const parent = model.nodes.find(({id}) => id === nodeId);

  if (!parent) {
    return;
  }

  const childId = crypto.randomUUID();
  const position = 'position' in parent ? {...parent.position} : {x: 0, y: 0};

  this.autoLayout.apply({
    anchorNodeId: nodeId,
    model: {
      nodes: [
        ...model.nodes,
        {
          id: childId,
          data: {type: 'task', title: 'New task'},
          position,
        },
      ],
      connections: [
        ...model.connections,
        {
          source: {
            nodeId,
            connectorId,
            connectorType: DfConnectionPoint.Output,
          },
          target: {
            nodeId: childId,
            connectorId: `${childId}-input-1`,
            connectorType: DfConnectionPoint.Input,
          },
        },
      ],
    },
  });
}
    

Multiple action outputs can carry different data or constraints. In strict-tree layouts, assign connected outputs a unique zero-based layoutOrder in visual order: top to bottom for horizontal trees and left to right for vertical trees.

Working Dynamic Tree

    
      
    
    
      import {
    type AfterViewInit,
    ChangeDetectionStrategy,
    Component,
    inject,
} from '@angular/core';
import {FormControl, ReactiveFormsModule} from '@angular/forms';
import {
    DfArrowhead,
    DfConnectionPoint,
    DfConnectionType,
    type DfDataConnection,
    type DfDataConnectorConfig,
    type DfDataModel,
    type DfDataNode,
    dfPanZoomOptionsProvider,
    NgDrawFlowComponent,
    provideNgDrawFlowConfigs,
} from '@ng-draw-flow/core';
import {
    DfAutoLayoutService,
    DfNodeSizingStrategy,
    DfTreeLayoutDirection,
    provideNgDrawFlowLayouts,
} from '@ng-draw-flow/layouts';

import {DynamicConnectorExampleActionsService} from './dynamic-connector-example-actions.service';
import {DynamicConnectorExampleNodeComponent} from './dynamic-connector-example-node.component';

@Component({
    standalone: true,
    selector: 'dynamic-connector-example',
    imports: [NgDrawFlowComponent, ReactiveFormsModule],
    template: `
        <div class="editor">
            <ng-draw-flow [formControl]="form" />
        </div>
    `,
    styles: `
        :host {
            display: block;
        }

        .editor {
            block-size: 22rem;
            background: #fff;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush,
    providers: [
        DynamicConnectorExampleActionsService,
        dfPanZoomOptionsProvider({leftPosition: 60, topPosition: null}),
        provideNgDrawFlowConfigs({
            connection: {
                type: DfConnectionType.SmoothStep,
                arrowhead: {type: DfArrowhead.ArrowClosed},
                curvature: 12,
            },
            nodes: {dynamicConnectorExample: DynamicConnectorExampleNodeComponent},
            options: {
                nodesDraggable: false,
                connectionsCreatable: false,
                connectionsDeletable: false,
            },
            positionAnimation: {duration: 240, easing: 'ease-in-out'},
        }),
        provideNgDrawFlowLayouts({
            tree: {
                direction: DfTreeLayoutDirection.LeftToRight,
                nodeSizing: {
                    strategy: DfNodeSizingStrategy.Measured,
                    fallback: {width: 176, height: 64},
                },
                levelGap: 80,
                siblingGap: 24,
            },
        }),
    ],
})
export default class DynamicConnectorExampleComponent implements AfterViewInit {
    private readonly actions = inject(DynamicConnectorExampleActionsService);
    private readonly autoLayout = inject(DfAutoLayoutService);
    private counter = 2;

    public readonly form = new FormControl<DfDataModel>(
        {
            nodes: [
                this.createNode('root', 'Root', 0, true),
                this.createNode('node-1', 'Node 1', 260),
            ],
            connections: [this.createConnection('root', 'node-1')],
        },
        {nonNullable: true},
    );

    constructor() {
        this.actions.configure((connector) => this.addChild(connector));
    }

    public ngAfterViewInit(): void {
        this.autoLayout.apply();
    }

    private addChild({nodeId, connectorId}: DfDataConnectorConfig): void {
        const model = this.form.getRawValue();
        const parent = model.nodes.find(({id}) => id === nodeId);

        if (!parent) {
            return;
        }

        const id = `node-${this.counter++}`;
        const position = 'position' in parent ? parent.position : {x: 0, y: 0};
        const child = this.createNode(id, `Node ${this.counter - 1}`, position.x);

        this.autoLayout.apply({
            anchorNodeId: nodeId,
            model: {
                nodes: [...model.nodes, child],
                connections: [
                    ...model.connections,
                    this.createConnection(nodeId, id, connectorId),
                ],
            },
        });
    }

    private createNode(
        id: string,
        title: string,
        x: number,
        startNode = false,
    ): DfDataNode {
        return {
            id,
            data: {type: 'dynamicConnectorExample', title},
            position: {x, y: 0},
            startNode,
        };
    }

    private createConnection(
        sourceId: string,
        targetId: string,
        sourceConnectorId = `${sourceId}-output-1`,
    ): DfDataConnection {
        return {
            source: {
                nodeId: sourceId,
                connectorType: DfConnectionPoint.Output,
                connectorId: sourceConnectorId,
            },
            target: {
                nodeId: targetId,
                connectorType: DfConnectionPoint.Input,
                connectorId: `${targetId}-input-1`,
            },
        };
    }
}

    
    
      import {Injectable} from '@angular/core';
import {type DfDataConnectorConfig} from '@ng-draw-flow/core';

@Injectable()
export class DynamicConnectorExampleActionsService {
    private addChildHandler: ((connector: DfDataConnectorConfig) => void) | null = null;

    public configure(handler: (connector: DfDataConnectorConfig) => void): void {
        this.addChildHandler = handler;
    }

    public addChild(connector: DfDataConnectorConfig): void {
        this.addChildHandler?.(connector);
    }
}

    
    
      import {ChangeDetectionStrategy, Component, inject} from '@angular/core';
import {
    type DfDataConnectorConfig,
    DfInputComponent,
    DfOutputComponent,
    DfOutputMode,
    DrawFlowBaseNode,
} from '@ng-draw-flow/core';
import {TuiIcon} from '@taiga-ui/core';

import {DynamicConnectorExampleActionsService} from './dynamic-connector-example-actions.service';

@Component({
    standalone: true,
    selector: 'dynamic-connector-example-node',
    imports: [DfInputComponent, DfOutputComponent, TuiIcon],
    templateUrl: './dynamic-connector-example-node.component.html',
    styleUrl: './dynamic-connector-example-node.component.less',
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DynamicConnectorExampleNodeComponent extends DrawFlowBaseNode {
    private readonly actions = inject(DynamicConnectorExampleActionsService);

    protected readonly outputMode = DfOutputMode;

    protected addChild(connector: DfDataConnectorConfig): void {
        this.actions.addChild(connector);
    }
}

    
    
      <ng-template #addChildIcon>
    <tui-icon icon="@tui.plus" />
</ng-template>

@if (!startNodeSignal()) {
    <df-input
        class="input"
        [connectorData]="{
            nodeId: nodeIdSignal(),
            connectorId: nodeIdSignal() + '-input-1',
            single: true,
        }"
    />
}

<strong>{{ modelSignal().title }}</strong>
<span>Dynamic tree node</span>

<df-output
    title="Add child"
    class="action-output"
    [connectorData]="{
        nodeId: nodeIdSignal(),
        connectorId: nodeIdSignal() + '-output-1',
        single: false,
        data: {childType: 'dynamicConnectorExample'},
    }"
    [content]="addChildIcon"
    [layoutOrder]="0"
    [mode]="outputMode.Action"
    (activated)="addChild($event)"
/>

    
    
      :host {
    position: relative;
    display: grid;
    gap: 0.25rem;
    inline-size: 9rem;
    min-block-size: 2.5rem;

    span {
        color: var(--tui-text-secondary);
        font-size: 0.75rem;
    }
}

.input,
.action-output {
    position: absolute;
    inset-block-start: 50%;
    transform: translateY(-50%);
}

.input {
    inset-inline-start: -1rem;
    opacity: 0;
    pointer-events: none;
}

.action-output {
    display: grid;
    inset-inline-end: -1rem;
    place-items: center;
    color: var(--tui-text-action);
    background: var(--tui-background-base);
    border: 0.0625rem solid var(--tui-border-normal);

    &::after {
        opacity: 0;
    }

    tui-icon {
        inline-size: 0.875rem;
        block-size: 0.875rem;
        font-size: 0.875rem;
    }
}

    

Connector Theming

Override --df-connector-input-color , --df-connector-output-color and their -hover variants on ng-draw-flow . The generic --df-connector-color remains a fallback for both types.

Continue with connection rendering or the dynamic layouts guide .