/
{
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;
}