/ 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.
DfAutoLayoutService validates and arranges the strict tree. Install layouts after @ng-draw-flow/core . Keep both packages on a compatible release line.
npm install @ng-draw-flow/layouts
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();
}
| Option | Default | Behavior |
|---|---|---|
direction | LeftToRight | Also supports RightToLeft, TopToBottom and BottomToTop. |
nodeSizing | Fixed 180 x 64 | Uses one fixed size or measured wrapper dimensions. |
levelGap | 80 | Space between parent and child generations. |
siblingGap | 32 | Space between nodes in the same generation. |
preserveRootPosition | true | Keeps the root at its position from the input model. |
origin | {x: 0, y: 0} | Root position when preservation is disabled. |
rootId | Undefined | Requires the graph's only root to have a specific id. |
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 {}
DfDataModel . Later size changes are coalesced per animation frame and trigger a new calculation automatically. 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 .
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,
},
},
],
},
});
}
}
nodesDraggable enabled to calculate an initial arrangement and then allow manual adjustment, or disable it when every mutation must remain under layout control. 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',
},
});
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'