/

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
    
      
    
    
      @Component({
  standalone: true,
  selector: 'editor',
  imports: [
    AsyncPipe,
    CommonModule,
    ReactiveFormsModule,
    NgDrawFlowComponent,
    TuiButton,
    TuiInputNumber,
    TuiLabel,
    TuiTextfield,
    TuiTextfieldComponent,
  ],
  templateUrl: './editor.component.html',
  styleUrls: ['./editor.component.less'],
  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush,
  providers: [
    dfPanZoomOptionsProvider({
      leftPosition: 50,
    }),
  ],
})
export default class EditorComponent implements OnInit {
  private readonly destroyRef = inject(DestroyRef);
  public readonly drawFlowStore = inject(NgDrawFlowStoreService);
  public readonly panZoomOptions = inject<DfPanZoomOptions>(DF_PAN_ZOOM_OPTIONS);

  public readonly scaleControl = new FormControl<number>(1, {nonNullable: true});
  public fullscreen$ = new BehaviorSubject<boolean>(false);
  public counter = 0;
  public form = new FormControl<DfDataModel>(this.data);

  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 ngOnInit(): void {
    this.scaleControl.valueChanges
      .pipe(distinctUntilChanged(), takeUntilDestroyed(this.destroyRef))
      .subscribe((value: number) => {
        if (!Number.isFinite(value)) {
          return;
        }

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

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

  public toggleFullscreen(): void {
    this.fullscreen$.next(!this.fullscreen$.value);
    this.drawFlowStore.resetPosition();
  }

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

    this.data.nodes = [
      ...this.data.nodes,
      {
        id,
        data: {
          type: 'simpleNode',
          text: `created node ${this.counter + 1}`,
        },
        position: {x: 600, y: this.counter * 40},
      },
    ];

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

    
    
      <div
  class="editor"
  [ngClass]="{fullscreen: fullscreen$ | async}"
>
  <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
    class="add"
    size="s"
    tuiButton
    type="button"
    (click)="addNodeToDrawFlow()"
  >
    Add node
  </button>

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

  <button
    class="remove-node-btn"
    size="s"
    tuiButton
    type="button"
    (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
      class="scale-field"
      tuiTextfieldSize="m"
      tuiTheme="light"
      [tuiTextfieldCleaner]="false"
    >
      <label tuiLabel>Set scale</label>

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

    <div class="scale-value">{{ drawFlowStore.scale$ | async }}%</div>

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

  <div class="selection-info">
    <ng-container *ngIf="drawFlowStore.selectedNode() as node; else connectionInfo">
      <div class="selection-title">Selected node</div>
      <div class="selection-value">{{ node.id }}</div>
    </ng-container>

    <ng-template #connectionInfo>
      <ng-container
        *ngIf="
          drawFlowStore.selectedConnection() as connection;
          else noSelection
        "
      >
        <div class="selection-title">Selected connection</div>
        <div class="selection-value">{{ connection.source.nodeId }} → {{ connection.target.nodeId }}</div>
      </ng-container>
    </ng-template>

    <ng-template #noSelection>
      <div class="selection-title">Nothing selected</div>
    </ng-template>
  </div>
</div>