/

Feature Overview

This is an overview example of the capabilities of NgDrawFlow. Below, you can observe that nodes can contain absolutely anything inside.
    
      
    
    
      {
  imports: [
    //... (other necessary imports)
    ReactiveFormsModule,
    NgDrawFlowComponent,
  ];
  providers: [
    //... (other necessary providers)
    provideNgDrawFlowConfigs({
      nodes: {
        infoNode: InfoNodeComponent,
        simpleNode: NodeWithoutConnectorsComponent,
        formNode: FormNodeComponent,
      },
    }),
  ];
}
    
    
      data: DfDataModel = {
  nodes: [
    {
      id: 'info-node',
      data: {type: 'infoNode', text: 'This base node'},
      position: {x: 160, y: 20},
    },
    {
      id: 'node-1',
      data: {
        type: 'simpleNode',
        text: 'The immutable starting point of the graph with no predecessors, marked by the startNode',
      },
      position: {x: 0, y: 280},
      startNode: true,
    },
    {
      id: 'node-2',
      data: {
        type: 'simpleNode',
        text: 'A flexible node that illustrates the flow from the start to various pathways within the graph',
      },
      position: {x: 250, y: 200},
    },
    {
      id: 'node-3',
      data: {
        type: 'simpleNode',
        text: 'Another versatile node demonstrating multiple connections, highlighting the complex interactivity of the graph.',
      },
      position: {x: 250, y: 350},
    },
    {
      id: 'node-4',
      data: {
        type: 'simpleNode',
        text: "The final destination in the graph's journey, characterized by the endNode parameter and lacking further paths.",
      },
      position: {x: 500, y: 280},
      endNode: true,
    },
    {
      id: 'node-5',
      data: {
        type: 'formNode',
        value: {},
        text: 'Nodes can contain any kind of content, for example, a form for user interaction.',
      },
      position: {x: 200, y: 550},
    },
  ],
  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',
      },
    },
    {
      source: {
        nodeId: 'node-1',
        connectorType: DfConnectionPoint.Output,
        connectorId: 'node-1-output-1',
      },
      target: {
        nodeId: 'node-5',
        connectorType: DfConnectionPoint.Input,
        connectorId: 'node-5-input-1',
      },
    },
  ],
};
    
    
      import {AsyncPipe} from '@angular/common';
import {ChangeDetectionStrategy, Component} from '@angular/core';
import {DfInputComponent, DfOutputComponent, DrawFlowBaseNode} from '@ng-draw-flow/core';

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

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

<p class="tui-text_body-xs">{{ model.text }}</p>

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

    
    
      :host {
    display: flex;
    inline-size: 10rem;
}

.input,
.output {
    position: absolute;
    z-index: 1;
}

.input {
    inset-inline-start: -0.5rem;
    inset-block-start: 0.25rem;
}

.output-wrapper {
    position: relative;
}

.output {
    inset-inline-end: -0.5rem;
    inset-block-start: 0;
}

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

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

    
    
      <p class="tui-text_body-xs">
    This node is your first step towards mastering the powerful features of our editor. Feel free to move nodes and
    connections around to better organize your graphs.
</p>

<p class="tui-text_body-xs">Deleting Elements:</p>

<ul class="tui-list tui-list_extra-small">
    <li class="tui-list__item">Select the node or connection you wish to remove.</li>
    <li class="tui-list__item">
        Press the
        <b>Backspace</b>
        or
        <b>Delete</b>
        key.
    </li>
</ul>

<p class="tui-text_body-xs">Explore, experiment, and create complex data structures with ease!</p>

    
    
      :host {
    display: block;
    inline-size: 30rem;
}

.tui-list__item {
    color: #000;
}

    
    
      import {
    type AfterViewInit,
    ChangeDetectionStrategy,
    Component,
    DestroyRef,
    inject,
} from '@angular/core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {
    FormControl,
    FormGroup,
    FormRecord,
    ReactiveFormsModule,
    Validators,
} from '@angular/forms';
import {DfInputComponent, DfOutputComponent, DrawFlowBaseNode} from '@ng-draw-flow/core';
import {TuiButton, TuiInput, TuiTextfield} from '@taiga-ui/core';

interface NodeFieldForm {
    connectorId: FormControl<string>;
    fieldValue: FormControl<string>;
}

@Component({
    standalone: true,
    selector: 'app-form-node',
    imports: [
        DfInputComponent,
        DfOutputComponent,
        ReactiveFormsModule,
        TuiButton,
        TuiInput,
        TuiTextfield,
    ],
    templateUrl: './form-node.component.html',
    styleUrl: './form-node.component.less',
    changeDetection: ChangeDetectionStrategy.OnPush,
    host: {'(keydown.backspace.stop)': '0', '(keydown.delete.stop)': '0'},
})
export class FormNodeComponent extends DrawFlowBaseNode implements AfterViewInit {
    private readonly destroyRef = inject(DestroyRef);

    public readonly form = new FormRecord<FormGroup<NodeFieldForm>>({
        field1: this.createFieldGroup(1, true),
        field2: this.createFieldGroup(2, true),
    });

    public get fieldNames(): string[] {
        return Object.keys(this.form.controls);
    }

    public getConnectorId(fieldName: string | null): string | null {
        if (!fieldName) {
            return null;
        }

        const group = this.form.controls[fieldName];

        if (!group) {
            return null;
        }

        return group.controls.connectorId.value;
    }

    public ngAfterViewInit(): void {
        this.form.valueChanges
            .pipe(takeUntilDestroyed(this.destroyRef))
            .subscribe((value) => {
                this.model.value = value;
            });
    }

    public add(): void {
        const index = Object.keys(this.form.controls).length + 1;
        const newFieldKey = `field${index}`;

        this.form.addControl(newFieldKey, this.createFieldGroup(index, false));
    }

    protected override get invalidState(): boolean {
        const formInvalid = Object.values(this.form.controls).some(
            (fieldGroup: FormGroup<NodeFieldForm>): boolean =>
                fieldGroup.controls.fieldValue.touched &&
                fieldGroup.controls.fieldValue.invalid,
        );

        return this.invalidSignal() || formInvalid;
    }

    private createFieldGroup(index: number, required: boolean): FormGroup<NodeFieldForm> {
        return new FormGroup<NodeFieldForm>({
            connectorId: new FormControl(`node-5-output-${index}`, {
                nonNullable: true,
            }),
            fieldValue: new FormControl('', {
                nonNullable: true,
                validators: required ? [Validators.required] : [],
            }),
        });
    }
}

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

<div class="tui-space_bottom-4 tui-text_body-s">
    {{ model.text }}
</div>

<form
    tuiTheme="light"
    [formGroup]="form"
>
    @for (fieldName of fieldNames; track fieldName; let i = $index) {
        <div [formGroupName]="fieldName">
            <div class="field tui-space_bottom-4">
                <tui-textfield
                    tuiTextfieldSize="s"
                    tuiTheme="light"
                    class="text-field"
                >
                    <input
                        formControlName="fieldValue"
                        tuiInput
                    />
                </tui-textfield>
                @if (!!getConnectorId(fieldName)) {
                    <df-output
                        class="output"
                        [connectorData]="{nodeId, connectorId: getConnectorId(fieldName)!, single: false}"
                    />
                }
            </div>
        </div>
    }
</form>

<button
    size="s"
    tuiButton
    type="button"
    (click)="add()"
>
    Add field
</button>

    
    
      :host {
    display: block;
    inline-size: 12.5rem;
    min-block-size: 2.5rem;
}

.field {
    position: relative;
    display: flex;
    align-items: center;

    .text-field {
        inline-size: 100%;
    }
}

.input,
.output {
    position: absolute;
    z-index: 1;
}

.input {
    inset-inline-start: -0.5rem;
    inset-block-start: 0.25rem;
}

.output-wrapper {
    position: relative;
}

.output {
    inset-inline-end: -0.5rem;
    inset-block-start: 0.75rem;
}