Angular-native Form Builder

This is the advanced drag-and-drop story in ngbootstrap: a recursive Angular-native Form Builder built from the same primitives that also support simple list drag-drop.

Recursive form-builder canvas

This example uses ngbDndList, ngbDndItem, and ngbDndHandle to build a recursive form editor. The root canvas accepts panels and form controls. Panels can also accept text inputs, textareas, selects, checkboxes, dates, and nested panels.

When to use it

Use this pattern for builders that manage hierarchical data: form sections, dashboard widgets, page blocks, survey groups, workflow steps, fields, or nested configuration panels. If you only need sortable or cross-list movement, start with the Simple List Drag Drop example. The important part is that every rendered list binds to the exact array it owns.

How to use it

  1. Bind each container with [ngbDndList] and the same dndGroup.
  2. Bind each draggable panel or field with [ngbDndItem], [dndIndex], and [dndSourceList].
  3. Use ngbDndHandle when only a header or icon should start the drag.
  4. Use keyboard reorder within the current list with Space, ArrowUp / ArrowDown, Enter, and Escape. Nested cross-list and palette moves still use pointer drag/drop.
  5. Refresh your app state from (dndDropped) when nested arrays are mutated in place.
Components
  • Panel
  • Text input
  • Textarea
  • Select
  • Checkbox
  • Date

CanvasDrop panels or form controls here.
Drop panels or form controls here
Canvas JSON
[]

Code snippet

import { CommonModule } from '@angular/common';
import { Component, signal } from '@angular/core';
import {
  NgbDndHandleDirective,
  NgbDndItemDirective,
  NgbDndListDirective,
} from '@angular-bootstrap/ngbootstrap';

type FormNode = {
  id: string;
  kind: 'panel' | 'text' | 'textarea' | 'select' | 'checkbox' | 'date';
  title: string;
  collapsed?: boolean;
  children?: FormNode[];
};

@Component({
  standalone: true,
  imports: [CommonModule, NgbDndListDirective, NgbDndItemDirective, NgbDndHandleDirective],
  template: `
    <div [ngbDndList]="panels()" dndGroup="panels" dndChildrenKey="children" (dndDropped)="refreshCanvas()">
      @for (panel of panels(); track panel.id; let i = $index) {
        <section [ngbDndItem]="panel" [dndSourceList]="panels()" [dndIndex]="i" dndGroup="panels">
          <button ngbDndHandle type="button">Drag</button>
          <input [value]="panel.title" (input)="updateTitle(panel, $event)" />

          <div [ngbDndList]="panel.children" dndGroup="panels" dndChildrenKey="children" (dndDropped)="refreshCanvas()">
            <!-- Render child panels recursively. -->
          </div>
        </section>
      }
    </div>

    <pre><code>{{ canvasJson() }}</code></pre>
  `,
})
export class FormBuilderExample {
  panels = signal<FormNode[]>([]);

  refreshCanvas(): void {
    queueMicrotask(() => this.panels.update((panels) => [...panels]));
  }

  updateTitle(panel: FormNode, event: Event): void {
    panel.title = (event.target as HTMLInputElement).value;
    this.panels.update((panels) => [...panels]);
  }

  canvasJson(): string {
    return JSON.stringify(this.panels(), null, 2);
  }
}

This project is not affiliated with ng-bootstrap or ngx-bootstrap. Those projects focus mainly on Bootstrap components for Angular. ngbootstrap focuses on Angular UI for data-heavy apps, especially Data Grid, Angular-native Form Builder, drag and drop workflows, documentation examples, and performance-focused Angular patterns.