/

State & Events

ng-draw-flow is a ControlValueAccessor. Use an Angular form control as the source of truth, component outputs for local template reactions, and NgDrawFlowStoreService when controls or observers live outside the editor component.

Form controlPersistent graph state and application-side mutations.
Component outputsTemplate-local reactions to user interactions.
Store facadeSignal snapshots, RxJS events and commands for surrounding UI.
Treat DfDataModel as immutable application state. Replace node, connection and position objects when updating the control so Angular and the editor can reconcile them predictably.

Interactive State Inspector

Select and drag nodes, select an edge, use the camera controls, add a node, or remove the selected node. The inspector reads signals from the store while the event list is populated from component outputs.

    
      
    
    
      import {
    ChangeDetectionStrategy,
    Component,
    computed,
    inject,
    signal,
} from '@angular/core';
import {FormControl, ReactiveFormsModule} from '@angular/forms';
import {
    DfArrowhead,
    DfConnectionPoint,
    DfConnectionType,
    type DfDataConnection,
    type DfDataModel,
    type DfDataNode,
    type DfEvent,
    dfPanZoomOptionsProvider,
    NgDrawFlowComponent,
    NgDrawFlowStoreService,
    provideNgDrawFlowConfigs,
} from '@ng-draw-flow/core';
import {TuiButton, TuiIcon} from '@taiga-ui/core';

import {SimpleNodeComponent} from '../../../../app/modules/nodes';

interface EventEntry {
    readonly id: number;
    readonly text: string;
}

@Component({
    standalone: true,
    selector: 'state-events-example',
    imports: [NgDrawFlowComponent, ReactiveFormsModule, TuiButton, TuiIcon],
    templateUrl: './state-events-example.component.html',
    styleUrl: './state-events-example.component.less',
    changeDetection: ChangeDetectionStrategy.OnPush,
    providers: [
        NgDrawFlowStoreService,
        dfPanZoomOptionsProvider({leftPosition: 40}),
        provideNgDrawFlowConfigs({
            connection: {
                type: DfConnectionType.SmoothStep,
                arrowhead: {type: DfArrowhead.ArrowClosed},
                curvature: 12,
            },
            nodes: {simpleNode: SimpleNodeComponent},
            positionAnimation: {duration: 220, easing: 'ease-in-out'},
        }),
    ],
})
export default class StateEventsExampleComponent {
    private readonly eventEntries = signal<EventEntry[]>([{id: 0, text: 'Editor ready'}]);
    private eventId = 0;
    private nodeId = 3;

    public readonly store = inject(NgDrawFlowStoreService);
    public readonly events = this.eventEntries.asReadonly();
    public readonly selectedConnection = computed(() => {
        const connection = this.store.selectedConnection();

        return connection
            ? `${connection.source.nodeId} -> ${connection.target.nodeId}`
            : 'None';
    });

    public readonly form = new FormControl<DfDataModel>(this.createModel(), {
        nonNullable: true,
    });

    public addNode(): void {
        const model = this.form.getRawValue();
        const id = `node-${this.nodeId++}`;
        const node = {
            id,
            data: {type: 'simpleNode', text: `Node ${id.slice(5)}`},
        };

        this.store.setDataModel({...model, nodes: [...model.nodes, node]});
        this.record(`Added ${id}`);
    }

    public removeSelectedNode(): void {
        const node = this.store.selectedNode();

        if (node) {
            this.store.removeNode(node.id);
        }
    }

    public recordScale(scale: number): void {
        this.record(`Scale ${scale}%`);
    }

    public recordNode(action: string, node: DfDataNode): void {
        this.record(`${action}: ${node.id}`);
    }

    public recordNodeEvent(action: string, event: DfEvent<DfDataNode>): void {
        this.record(`${action}: ${event.target.id}`);
    }

    public recordConnection(action: string, connection: DfDataConnection): void {
        this.record(
            `${action}: ${connection.source.nodeId} -> ${connection.target.nodeId}`,
        );
    }

    public recordConnectionEvent(action: string, event: DfEvent<DfDataConnection>): void {
        this.recordConnection(action, event.target);
    }

    private record(text: string): void {
        this.eventEntries.update((events) => [
            {id: ++this.eventId, text},
            ...events.slice(0, 5),
        ]);
    }

    private createModel(): DfDataModel {
        return {
            nodes: [
                {
                    id: 'node-1',
                    data: {type: 'simpleNode', text: 'Source'},
                    position: {x: 0, y: 0},
                    startNode: true,
                },
                {
                    id: 'node-2',
                    data: {type: 'simpleNode', text: 'Target'},
                    position: {x: 260, y: 80},
                    endNode: true,
                },
            ],
            connections: [
                {
                    source: {
                        nodeId: 'node-1',
                        connectorType: DfConnectionPoint.Output,
                        connectorId: 'node-1-output-1',
                    },
                    target: {
                        nodeId: 'node-2',
                        connectorType: DfConnectionPoint.Input,
                        connectorId: 'node-2-input-1',
                    },
                },
            ],
        };
    }
}

    
    
      <div class="toolbar">
    <button
        appearance="secondary"
        size="s"
        tuiButton
        type="button"
        (click)="store.zoomOut()"
    >
        <tui-icon icon="@tui.minus" />
        Zoom out
    </button>
    <button
        appearance="secondary"
        size="s"
        tuiButton
        type="button"
        (click)="store.zoomIn()"
    >
        <tui-icon icon="@tui.plus" />
        Zoom in
    </button>
    <button
        appearance="secondary"
        size="s"
        tuiButton
        type="button"
        (click)="store.resetPosition()"
    >
        <tui-icon icon="@tui.rotate-ccw" />
        Reset
    </button>
    <button
        appearance="primary"
        size="s"
        tuiButton
        type="button"
        (click)="addNode()"
    >
        <tui-icon icon="@tui.circle-plus" />
        Add node
    </button>
    <button
        appearance="secondary"
        size="s"
        tuiButton
        type="button"
        [disabled]="!store.selectedNode()"
        (click)="removeSelectedNode()"
    >
        <tui-icon icon="@tui.trash-2" />
        Remove selected
    </button>
</div>

<div class="workspace">
    <div class="editor">
        <ng-draw-flow
            [formControl]="form"
            (connectionCreated)="recordConnectionEvent('Created edge', $event)"
            (connectionDeleted)="recordConnectionEvent('Deleted edge', $event)"
            (connectionSelected)="recordConnection('Selected edge', $event)"
            (nodeDeleted)="recordNodeEvent('Deleted node', $event)"
            (nodeMoved)="recordNodeEvent('Moved node', $event)"
            (nodeSelected)="recordNode('Selected node', $event)"
            (scale)="recordScale($event)"
        />
    </div>

    <aside class="inspector">
        <h3>Live state</h3>
        <dl>
            <div>
                <dt>Scale</dt>
                <dd>{{ store.scale() }}%</dd>
            </div>
            <div>
                <dt>Selected node</dt>
                <dd>{{ store.selectedNode()?.id ?? 'None' }}</dd>
            </div>
            <div>
                <dt>Selected edge</dt>
                <dd>{{ selectedConnection() }}</dd>
            </div>
            <div>
                <dt>Nodes</dt>
                <dd>{{ store.dataModel()?.nodes?.length ?? 0 }}</dd>
            </div>
        </dl>

        <h3>Recent events</h3>
        <ol>
            @for (event of events(); track event.id) {
                <li>{{ event.text }}</li>
            }
        </ol>
    </aside>
</div>

    
    
      :host {
    display: block;
    background: var(--tui-background-base);
}

.toolbar {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    padding: 0.75rem;
    border-block-end: 0.0625rem solid var(--tui-border-normal);
}

.workspace {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 16rem;
    min-block-size: 28rem;
}

.editor {
    min-inline-size: 0;
    background: #fff;
}

.inspector {
    padding: 1rem;
    background: var(--tui-background-neutral-1);
    border-inline-start: 0.0625rem solid var(--tui-border-normal);

    h3 {
        margin-block: 0 0.75rem;
        font-size: 1rem;
    }

    h3 + ol,
    h3 + dl {
        margin-block-start: 0;
    }

    dl {
        display: grid;
        gap: 0.5rem;
        margin-block-end: 1.5rem;
    }

    dl > div {
        display: flex;
        justify-content: space-between;
        gap: 1rem;
    }

    dt {
        color: var(--tui-text-secondary);
    }

    dd {
        margin: 0;
        text-align: end;
    }

    ol {
        display: grid;
        gap: 0.375rem;
        padding-inline-start: 1.25rem;
        font-size: 0.8125rem;
    }
}

@media (max-width: 50rem) {
    .workspace {
        grid-template-columns: 1fr;
    }

    .editor {
        block-size: 24rem;
    }

    .inspector {
        border-block-start: 0.0625rem solid var(--tui-border-normal);
        border-inline-start: 0;
    }
}

    

Component Outputs

Use outputs when the editor and its consumer share a template. Mutation events contain both the affected item and the complete resulting model through DfEvent<T> . Selection events return the selected item directly.

editor.component.html

    
      <ng-draw-flow
  [formControl]="form"
  (scale)="onScale($event)"
  (nodeSelected)="onNodeSelected($event)"
  (connectionSelected)="onConnectionSelected($event)"
  (nodeMoved)="onNodeMoved($event)"
  (nodeDeleted)="onNodeDeleted($event)"
  (connectionCreated)="onConnectionCreated($event)"
  (connectionDeleted)="onConnectionDeleted($event)"
/>
    
  • scale : current zoom percentage, where 100 is actual size.
  • nodeSelected : selected DfDataNode .
  • connectionSelected : selected DfDataConnection .
  • nodeMoved and nodeDeleted : DfEvent<DfDataNode> .
  • connectionCreated and connectionDeleted : DfEvent<DfDataConnection> .

Signal-first Store

The root-scoped store mirrors live editor state. Every long-lived snapshot is available as an Angular signal and, where useful, as an RxJS stream. Event streams have a matching last-event signal for computed state and OnPush templates.

toolbar.component.ts

    
      readonly store = inject(NgDrawFlowStoreService);

readonly selectedTitle = computed(
  () => this.store.selectedNode()?.data['title'] ?? 'Nothing selected',
);
readonly canDelete = this.store.hasSelection;

zoomIn(): void {
  this.store.zoomIn();
}

addNode(): void {
  const model = this.store.dataModel();

  if (!model) {
    return;
  }

  this.store.setDataModel({
    ...model,
    nodes: [
      ...model.nodes,
      {
        id: crypto.randomUUID(),
        data: {type: 'task', title: 'New task'},
        position: {x: 0, y: 0},
      },
    ],
  });
}
    
  • dataModel / dataModel$
  • selectedNode / selectedNode$
  • selectedConnection / selectedConnection$
  • scale / scale$ and hasSelection
  • lastNodeMoved , lastNodeDeleted and lastNodeSelected
  • lastConnectionCreated , lastConnectionDeleted and lastConnectionSelected

Store Commands

zoomIn() , zoomOut() , resetPosition() , setPosition() , setScale() , setDataModel() , removeNode() , removeConnection() and setDataModel() forward to the attached editor. Model replacement preserves node selection by node id and connection selection by source and target connector endpoints while those items still exist.

Read scale signals and events as percentages, but pass a factor to setScale() and the optional setPosition().zoom field. For example, read 75 for 75% and write 0.75 to restore it.

Disable the bound form control to disable editor interaction while keeping the current graph rendered: form.disable() .

Use the form control as the persistent source of truth. Store snapshots are cloned runtime views intended for computed UI state and commands, not objects to mutate in place.

Multiple Editors

A root-scoped store controls the currently attached editor. When a page contains independent editors, scope one store instance to each editor host through component providers.

editor-host.component.ts

    
      @Component({
  // The editor and every child toolbar resolve this local instance.
  providers: [NgDrawFlowStoreService],
  template: `
    <editor-toolbar />
    <ng-draw-flow [formControl]="form" />
  `,
})
export class EditorHostComponent {}