/

Editor

Our graph editor is built to serve as an API-first platform, enabling developers to design their own UI elements and interact with the graph through exposed methods. This approach allows for the addition of new nodes and control over the zoom level, complete with the ability to display the current zoom, all while providing a customizable experience tailored to your application's needs.
100%
Nothing selected
    
      
    
    
      import {CommonModule} from '@angular/common';
import {
    ChangeDetectionStrategy,
    Component,
    DestroyRef,
    inject,
    type OnInit,
    signal,
    ViewEncapsulation,
} from '@angular/core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {FormControl, ReactiveFormsModule} from '@angular/forms';
import {
    DF_PAN_ZOOM_OPTIONS,
    DfArrowhead,
    DfConnectionPoint,
    DfConnectionType,
    dfCycleDetectionValidator,
    type DfDataConnection,
    type DfDataModel,
    type DfDataNode,
    type DfEvent,
    dfIsolatedNodesValidator,
    type DfPanZoomOptions,
    dfPanZoomOptionsProvider,
    NgDrawFlowComponent,
    NgDrawFlowStoreService,
    provideNgDrawFlowConfigs,
} from '@ng-draw-flow/core';
import {TuiAddonDoc, type TuiRawLoaderContent} from '@taiga-ui/addon-doc';
import {TuiButton, TuiLabel, TuiTextfield} from '@taiga-ui/core';
import {TuiInputNumber} from '@taiga-ui/kit';
import {MarkdownModule} from 'ngx-markdown';
import {distinctUntilChanged, filter} from 'rxjs';

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

@Component({
    standalone: true,
    selector: 'editor',
    imports: [
        CommonModule,
        MarkdownModule,
        NgDrawFlowComponent,
        ReactiveFormsModule,
        TuiAddonDoc,
        TuiButton,
        TuiInputNumber,
        TuiLabel,
        TuiTextfield,
    ],
    templateUrl: './editor.component.html',
    styleUrl: './editor.component.less',
    encapsulation: ViewEncapsulation.None,
    changeDetection: ChangeDetectionStrategy.OnPush,
    providers: [
        dfPanZoomOptionsProvider({leftPosition: 50}),
        provideNgDrawFlowConfigs({
            connection: {
                type: DfConnectionType.SmoothStep,
                arrowhead: {type: DfArrowhead.Arrow},
                curvature: 10,
            },
            nodes: {simpleNode: SimpleNodeComponent},
            options: {
                nodesDraggable: true,
                nodesDeletable: true,
                connectionsDeletable: true,
                connectionsCreatable: true,
            },
        }),
    ],
})
export default class EditorComponent implements OnInit {
    private readonly destroyRef = inject(DestroyRef);
    public readonly panZoomOptions = inject<DfPanZoomOptions>(DF_PAN_ZOOM_OPTIONS);
    public readonly drawFlowStore = inject(NgDrawFlowStoreService);

    public readonly customNodeExample: Record<string, TuiRawLoaderContent> = {
        Typescript: import('./editor.component.ts?raw'),
        Styles: import('./editor.component.less?raw'),
        Template: import('./editor.component.html?raw'),
    };

    public data: DfDataModel = {
        nodes: [
            {
                id: 'node-1',
                data: {type: 'simpleNode', text: 'This base node'},
                position: {x: 0, y: 0},
                startNode: true,
            },
            {
                id: 'node-2',
                data: {type: 'simpleNode', text: 'This child node 1'},
                position: {x: 250, y: 50},
            },
            {
                id: 'node-3',
                data: {type: 'simpleNode', text: 'This child node 2'},
                position: {x: 250, y: -50},
            },
            {
                id: 'node-4',
                data: {type: 'simpleNode', text: 'This child node 3'},
                position: {x: 500, y: 0},
                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',
                },
            },
            {
                source: {
                    nodeId: 'node-1',
                    connectorType: DfConnectionPoint.Output,
                    connectorId: 'node-1-output-1',
                },
                target: {
                    nodeId: 'node-3',
                    connectorType: DfConnectionPoint.Input,
                    connectorId: 'node-3-input-1',
                },
            },
            {
                source: {
                    nodeId: 'node-2',
                    connectorType: DfConnectionPoint.Output,
                    connectorId: 'node-2-output-1',
                },
                target: {
                    nodeId: 'node-4',
                    connectorType: DfConnectionPoint.Input,
                    connectorId: 'node-4-input-1',
                },
            },
            {
                source: {
                    nodeId: 'node-3',
                    connectorType: DfConnectionPoint.Output,
                    connectorId: 'node-3-output-1',
                },
                target: {
                    nodeId: 'node-4',
                    connectorType: DfConnectionPoint.Input,
                    connectorId: 'node-4-input-1',
                },
            },
        ],
    };

    public readonly scaleControl = new FormControl<number>(1, {nonNullable: true});
    public readonly fullscreen = signal(false);
    public counter = 0;

    public form = new FormControl<DfDataModel>(this.data, [
        dfCycleDetectionValidator(),
        dfIsolatedNodesValidator(),
    ]);

    public ngOnInit(): void {
        this.form.statusChanges.subscribe((v) => {
            console.warn(v, 'onStatusChange');
        });

        this.form.valueChanges.pipe(filter(Boolean)).subscribe((value: DfDataModel) => {
            this.data = value;
        });

        this.scaleControl.valueChanges
            .pipe(distinctUntilChanged(), takeUntilDestroyed(this.destroyRef))
            .subscribe((value: number) => {
                if (!Number.isFinite(value)) {
                    return;
                }

                this.drawFlowStore.setScale(value);
            });

        this.drawFlowStore.connectionCreated$
            .pipe(takeUntilDestroyed(this.destroyRef))
            .subscribe((data: DfEvent<DfDataConnection>) => {
                console.warn(data, 'drawFlowStore connectionCreated');
            });

        this.drawFlowStore.connectionDeleted$
            .pipe(takeUntilDestroyed(this.destroyRef))
            .subscribe((data: DfEvent<DfDataConnection>) => {
                console.warn(data, 'drawFlowStore connectionDeleted');
            });

        this.drawFlowStore.connectionSelected$
            .pipe(takeUntilDestroyed(this.destroyRef))
            .subscribe((data: DfDataConnection) => {
                console.warn(data, 'drawFlowStore connectionSelected');
            });

        this.drawFlowStore.nodeDeleted$
            .pipe(takeUntilDestroyed(this.destroyRef))
            .subscribe((data: DfEvent<DfDataNode>) => {
                console.warn(data, 'drawFlowStore nodeDeleted');
            });

        this.drawFlowStore.nodeMoved$
            .pipe(takeUntilDestroyed(this.destroyRef))
            .subscribe((data: DfEvent<DfDataNode>) => {
                console.warn(data, 'drawFlowStore nodeMoved');
            });

        this.drawFlowStore.nodeSelected$
            .pipe(takeUntilDestroyed(this.destroyRef))
            .subscribe((data: DfDataNode) => {
                console.warn(data, 'drawFlowStore nodeSelected');
            });
    }

    public onScaleChange(zoomLevel: number): void {
        this.scaleControl.setValue(zoomLevel / 100, {emitEvent: false});
    }

    public onConnectionCreated(event: DfEvent<DfDataConnection>): void {
        console.warn(event, 'onConnectionCreated');
    }

    public onConnectionDeleted(event: DfEvent<DfDataConnection>): void {
        console.warn(event, 'onConnectionDeleted');
    }

    public onConnectionSelected(event: DfDataConnection): void {
        console.warn(event, 'onConnectionSelected');
    }

    public onNodeMoved(event: DfEvent<DfDataNode>): void {
        console.warn(event, 'onNodeMoved');
    }

    public onNodeSelected(event: DfDataNode): void {
        console.warn(event, 'onNodeSelected');
    }

    public onNodeDeleted(event: DfEvent<DfDataNode>): void {
        console.warn(event, 'onNodeDeleted');
    }

    public toggleFullscreen(): void {
        this.fullscreen.update((value) => !value);
        this.drawFlowStore.resetPosition();
    }

    public addNodeToDrawFlow(): void {
        const id = `new-node-${this.counter}`;

        this.data.nodes.push({
            id,
            data: {
                type: 'simpleNode',
                text: `created node ${this.counter + 1}`,
            },
        });

        this.form.setValue(this.data);
        this.counter++;
    }
}

    
    
      .editor {
    position: relative;
    block-size: 31.25rem;
    inline-size: 100%;
    margin: 0 auto;
    background: #fff;

    &.fullscreen {
        position: fixed;
        z-index: 1;
        inset-block-start: 0;
        inset-inline-start: 0;
        inline-size: 100%;
        block-size: 100%;
        background-color: #fff;

        .add {
            inset-block-start: 5rem;
        }

        .fullscreen-btn {
            inset-block-start: 7.8rem;
        }

        .remove-node-btn {
            inset-block-start: 10.6rem;
        }

        .selection-info {
            inset-block-start: 5rem;
        }
    }
}

.add {
    position: absolute;
    inset-inline-end: 1.25rem;
    inset-block-start: 1.25rem;
}

.fullscreen-btn {
    position: absolute;
    inset-inline-end: 1.25rem;
    inset-block-start: 3.75rem;
}

.remove-node-btn {
    position: absolute;
    inset-inline-end: 1.25rem;
    inset-block-start: 6.25rem;
}

.scale-controls {
    position: absolute;
    display: flex;
    inset-block-end: 1.25rem;
    inset-inline-end: 1.25rem;
    align-items: center;
    gap: 0.75rem;
}

.scale-value {
    inline-size: 3.125rem;
    text-align: center;
    color: #333;
}

.scale-field {
    background: #fff;
}

.selection-info {
    position: absolute;
    display: flex;
    inset-block-start: 1.25rem;
    inset-inline-start: 1.25rem;
    flex-direction: column;
    gap: 0.25rem;
    padding: 0.75rem 1rem;
    min-inline-size: 11rem;
    background-color: rgba(255, 255, 255, 0.95);
    border: 1px solid #dcdcdc;
    border-radius: 0.5rem;
    box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.08);
}

.selection-title {
    font-weight: 600;
    color: #1f1f1f;
}

.selection-value {
    font-size: 0.875rem;
    color: #555;
}

    
    
      <tui-doc-page header="Editor">
    Our graph editor is built to serve as an API-first platform, enabling developers to design their own UI elements and
    interact with the graph through exposed methods. This approach allows for the addition of new nodes and control over
    the zoom level, complete with the ability to display the current zoom, all while providing a customizable experience
    tailored to your application's needs.

    <section class="tui-space_top-4">
        <tui-doc-example
            style="padding: 0"
            [content]="customNodeExample"
        >
            <div
                class="editor"
                [ngClass]="{fullscreen: fullscreen()}"
            >
                <ng-draw-flow
                    [formControl]="form"
                    (connectionCreated)="onConnectionCreated($event)"
                    (connectionDeleted)="onConnectionDeleted($event)"
                    (connectionSelected)="onConnectionSelected($event)"
                    (nodeDeleted)="onNodeDeleted($event)"
                    (nodeMoved)="onNodeMoved($event)"
                    (nodeSelected)="onNodeSelected($event)"
                    (scale)="onScaleChange($event)"
                />

                <button
                    size="s"
                    tuiButton
                    type="button"
                    class="add"
                    (click)="addNodeToDrawFlow()"
                >
                    Add node
                </button>

                <button
                    size="s"
                    tuiButton
                    type="button"
                    class="fullscreen-btn"
                    (click)="toggleFullscreen()"
                >
                    {{ fullscreen() ? 'Exit from fullscreen' : 'Fullscreen' }}
                </button>

                <button
                    size="s"
                    tuiButton
                    type="button"
                    class="remove-node-btn"
                    (click)="drawFlowStore.removeNode('node-3')"
                >
                    Remove node-3
                </button>

                <div class="scale-controls">
                    <button
                        size="s"
                        tuiButton
                        type="button"
                        (click)="drawFlowStore.zoomOut()"
                    >
                        zoomOut
                    </button>

                    <tui-textfield
                        tuiTextfieldSize="m"
                        tuiTheme="light"
                        class="scale-field"
                        [tuiTextfieldCleaner]="false"
                    >
                        <label tuiLabel>Set scale</label>

                        <input
                            aria-label="Set scale"
                            tuiInputNumber
                            [formControl]="scaleControl"
                            [max]="panZoomOptions.maxZoom"
                            [min]="panZoomOptions.minZoom"
                            [step]="panZoomOptions.zoomStep"
                        />
                    </tui-textfield>

                    <div class="scale-value">{{ drawFlowStore.scale() }}%</div>

                    <button
                        size="s"
                        tuiButton
                        type="button"
                        (click)="drawFlowStore.zoomIn()"
                    >
                        zoomIn
                    </button>
                </div>

                <div class="selection-info">
                    @if (drawFlowStore.selectedNode(); as node) {
                        <div class="selection-title">Selected node</div>
                        <div class="selection-value">{{ node.id }}</div>
                    } @else {
                        @if (drawFlowStore.selectedConnection(); as connection) {
                            <div class="selection-title">Selected connection</div>
                            <div class="selection-value">
                                {{ connection.source.nodeId }} → {{ connection.target.nodeId }}
                            </div>
                        } @else {
                            <div class="selection-title">Nothing selected</div>
                        }
                    }
                </div>
            </div>
        </tui-doc-example>
    </section>
</tui-doc-page>