/

Dynamic Layouts

The optional @ng-draw-flow/layouts package keeps a strict tree organized as nodes are added or resized. It calculates positions while core continues to render nodes, connectors, edges and interactions.

Strict treeOne root, one parent per child and no disconnected nodes.
Dynamic sizingFixed dimensions or measured DOM wrappers with a fallback.
Stable interactionAnchored mutations keep the activated parent under the pointer.

How It Fits Together

  1. Application changes dataAdd or remove nodes and connections as immutable objects.
  2. Layouts calculate positionsDfAutoLayoutService validates and arranges the strict tree.
  3. Core renders the resultReal connector anchors and optional position animation update together.

Installation

Install layouts after @ng-draw-flow/core . Keep both packages on a compatible release line.

    
      npm install @ng-draw-flow/layouts
    

Register the Layout

Applications configure supported tree options and inject DfAutoLayoutService . D3 is an internal implementation detail; application code does not construct an engine.

dynamic-tree.component.ts

    
      import {DfNodeSizingStrategy, DfTreeLayoutDirection, provideNgDrawFlowLayouts} from '@ng-draw-flow/layouts';

@Component({
  providers: [
    provideNgDrawFlowLayouts({
      tree: {
        direction: DfTreeLayoutDirection.TopToBottom,
        nodeSizing: {
          strategy: DfNodeSizingStrategy.Fixed,
          size: {width: 240, height: 80},
        },
        levelGap: 120,
        siblingGap: 40,
      },
    }),
  ],
})
export class GraphEditorComponent {}
    

Call apply() after the editor is attached. The method is synchronous and does not return a Promise or require a subscription. The latest operation is available through signals.

dynamic-tree.component.ts

    
      private readonly autoLayout = inject(DfAutoLayoutService);

readonly layoutRunning = this.autoLayout.running;
readonly layoutResult = this.autoLayout.result;
readonly layoutError = this.autoLayout.error;

ngAfterViewInit(): void {
  this.autoLayout.apply();
}
    

Tree Options

OptionDefaultBehavior
directionLeftToRightAlso supports RightToLeft, TopToBottom and BottomToTop.
nodeSizingFixed 180 x 64Uses one fixed size or measured wrapper dimensions.
levelGap80Space between parent and child generations.
siblingGap32Space between nodes in the same generation.
preserveRootPositiontrueKeeps the root at its position from the input model.
origin{x: 0, y: 0}Root position when preservation is disabled.
rootIdUndefinedRequires the graph's only root to have a specific id.

Dynamic Node Sizing

Use DfNodeSizingStrategy.Fixed when every node has known dimensions. Use Measured for forms, variable text and expandable content. Measured mode opts into core's wrapper ResizeObserver and requires a fallback for the first calculation.

dynamic-tree.component.ts

    
      import {DfNodeSizingStrategy, provideNgDrawFlowLayouts} from '@ng-draw-flow/layouts';

@Component({
  providers: [
    provideNgDrawFlowLayouts({
      tree: {
        nodeSizing: {
          strategy: DfNodeSizingStrategy.Measured,
          fallback: {width: 180, height: 64},
        },
      },
    }),
  ],
})
export class DynamicTreeComponent {}
    
DOM dimensions are runtime view state and are never written to DfDataModel . Later size changes are coalesced per animation frame and trigger a new calculation automatically.

Real Connector Anchors

Layouts calculate node positions only. Every connection still starts and ends at the actual df-output and df-input rendered by custom nodes. This supports several independent branches from one form node without virtual center points.

tree-node.component.ts

    
      import {ChangeDetectionStrategy, Component, inject, Injectable} from '@angular/core';
import {
  type DfDataConnectorConfig,
  DfInputComponent,
  DfOutputComponent,
  DfOutputMode,
  DrawFlowBaseNode,
} from '@ng-draw-flow/core';
import {TuiIcon} from '@taiga-ui/core';

@Injectable()
export class TreeActionsService {
  private handler?: (connector: DfDataConnectorConfig) => void;

  configure(handler: (connector: DfDataConnectorConfig) => void): void {
    this.handler = handler;
  }

  addChild(connector: DfDataConnectorConfig): void {
    this.handler?.(connector);
  }
}

@Component({
  standalone: true,
  selector: 'app-tree-node',
  imports: [DfInputComponent, DfOutputComponent, TuiIcon],
  templateUrl: './tree-node.component.html',
  styles: `
    :host {
      position: relative;
      display: block;
      min-inline-size: 10rem;
    }

    .input,
    .output {
      position: absolute;
      transform: translateY(-50%);
    }

    .input {
      inset-block-start: 50%;
      inset-inline-start: -1rem;
    }

    .output {
      inset-inline-end: -1rem;
    }

    .output-primary {
      inset-block-start: 35%;
    }

    .output-secondary {
      inset-block-start: 65%;
    }
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TreeNodeComponent extends DrawFlowBaseNode {
  private readonly actions = inject(TreeActionsService);

  readonly outputMode = DfOutputMode;

  addChild(connector: DfDataConnectorConfig): void {
    this.actions.addChild(connector);
  }
}
    

tree-node.component.html

    
      <ng-template #addChildIcon>
  <tui-icon icon="@tui.plus" />
</ng-template>

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

<strong>{{ modelSignal().title }}</strong>

<df-output
  class="output output-primary"
  [content]="addChildIcon"
  [connectorData]="{
    nodeId: nodeIdSignal(),
    connectorId: nodeIdSignal() + '-add-primary',
    single: false,
    data: {branch: 'primary'},
  }"
  [layoutOrder]="0"
  [mode]="outputMode.Action"
  (activated)="addChild($event)"
/>

<df-output
  class="output output-secondary"
  [content]="addChildIcon"
  [connectorData]="{
    nodeId: nodeIdSignal(),
    connectorId: nodeIdSignal() + '-add-secondary',
    single: false,
    data: {branch: 'secondary'},
  }"
  [layoutOrder]="1"
  [mode]="outputMode.Action"
  (activated)="addChild($event)"
/>
    

When children use several outputs, assign each connected output a unique zero-based layoutOrder . Horizontal trees read it top to bottom; vertical trees read it left to right. Children sharing one output retain their order from DfDataModel.connections .

End-to-End Child Creation

An action output asks the application to mutate the graph. The host creates a positioned child and its edge, then calls apply({model, anchorNodeId}) . Passing the next model directly avoids waiting for form-to-store synchronization, while the anchor keeps the activated parent at the same canvas position.

dynamic-tree.component.ts

    
      import {type AfterViewInit, Component, inject} from '@angular/core';
import {FormControl, ReactiveFormsModule} from '@angular/forms';
import {
  DfArrowhead,
  DfConnectionPoint,
  DfConnectionType,
  type DfDataConnectorConfig,
  type DfDataModel,
  NgDrawFlowComponent,
  provideNgDrawFlowConfigs,
} from '@ng-draw-flow/core';
import {
  DfAutoLayoutService,
  DfNodeSizingStrategy,
  DfTreeLayoutDirection,
  provideNgDrawFlowLayouts,
} from '@ng-draw-flow/layouts';

import {TreeActionsService, TreeNodeComponent} from './tree-node.component';

@Component({
  standalone: true,
  selector: 'app-dynamic-tree',
  imports: [NgDrawFlowComponent, ReactiveFormsModule],
  template: `
    <ng-draw-flow [formControl]="form" />
  `,
  providers: [
    TreeActionsService,
    provideNgDrawFlowConfigs({
      nodes: {task: TreeNodeComponent},
      connection: {
        type: DfConnectionType.SmoothStep,
        arrowhead: {type: DfArrowhead.ArrowClosed},
        curvature: 16,
      },
      options: {
        nodesDraggable: false,
        connectionsCreatable: false,
        connectionsDeletable: false,
      },
      positionAnimation: {duration: 280, easing: 'ease-in-out'},
    }),
    provideNgDrawFlowLayouts({
      tree: {
        direction: DfTreeLayoutDirection.LeftToRight,
        nodeSizing: {
          strategy: DfNodeSizingStrategy.Measured,
          fallback: {width: 180, height: 64},
        },
        levelGap: 96,
        siblingGap: 32,
      },
    }),
  ],
})
export class DynamicTreeComponent implements AfterViewInit {
  private readonly actions = inject(TreeActionsService);
  private readonly autoLayout = inject(DfAutoLayoutService);

  readonly form = new FormControl<DfDataModel>(
    {
      nodes: [
        {
          id: 'root',
          data: {type: 'task', title: 'Root'},
          position: {x: 0, y: 0},
          startNode: true,
        },
      ],
      connections: [],
    },
    {nonNullable: true},
  );

  constructor() {
    this.actions.configure((connector) => this.addChild(connector));
  }

  ngAfterViewInit(): void {
    this.autoLayout.apply();
  }

  private addChild({nodeId, connectorId}: DfDataConnectorConfig): void {
    const model = this.form.getRawValue();
    const parent = model.nodes.find(({id}) => id === nodeId);

    if (!parent) {
      return;
    }

    const childId = crypto.randomUUID();
    const position = 'position' in parent ? {...parent.position} : {x: 0, y: 0};

    this.autoLayout.apply({
      anchorNodeId: nodeId,
      model: {
        nodes: [
          ...model.nodes,
          {
            id: childId,
            data: {type: 'task', title: 'New task'},
            position,
          },
        ],
        connections: [
          ...model.connections,
          {
            source: {
              nodeId,
              connectorId,
              connectorType: DfConnectionPoint.Output,
            },
            target: {
              nodeId: childId,
              connectorId: `${childId}-input-1`,
              connectorType: DfConnectionPoint.Input,
            },
          },
        ],
      },
    });
  }
}
    
Dynamic layouts do not force read-only interaction. Keep nodesDraggable enabled to calculate an initial arrangement and then allow manual adjustment, or disable it when every mutation must remain under layout control.

Smooth Position Changes

Position animation belongs to core and is opt-in. It applies to layout results and any other model update that changes node coordinates.

graph-editor.component.ts

    
      provideNgDrawFlowConfigs({
  positionAnimation: {
    duration: 280,
    easing: 'ease-in-out',
  },
});
    

Strict Tree Requirements

A valid model has exactly one root, unique node ids, existing endpoints, at most one parent for each child and no disconnected nodes. Failures are published to DfAutoLayoutService.error as DfTreeLayoutError instances.

layout-errors.ts

    
      readonly treeLayoutError = computed(() => {
  const error = this.autoLayout.error();

  return error instanceof DfTreeLayoutError ? error : null;
});

// error.code:
// 'duplicate-node' | 'missing-node' | 'multiple-parents'
// | 'invalid-root' | 'disconnected-graph'
// | 'missing-output-order' | 'invalid-output-order'
// | 'duplicate-output-order'
    
General DAGs, cycles, multiple parents and custom layout engines are not supported by the current layouts package.