Action buttons for an array (Add, Clear, etc.) don't have to be separate fields in the config — a wrapper can own the button and the behaviour. Here, arraySection renders a titled card whose header contains an Add button; clicking it dispatches arrayEvent(key).append(itemTemplate) against the array it wraps.

Live Demo

Material Material
Loading live example

Config

The consumer only lists the wrapper and the item template. No separate addArrayItem field; no button placement logic.

const tagItemTemplate = [
  { key: 'value', type: 'input', label: 'Tag', required: true, minLength: 2 },
  { key: 'removeTag', type: 'removeArrayItem', label: 'Remove', props: { color: 'warn' } },
] as const;

const config = {
  fields: [
    {
      key: 'tags',
      type: 'array',
      wrappers: [{ type: 'arraySection', title: 'Tags', addLabel: 'Add tag', itemTemplate: tagItemTemplate }],
      fields: [...],
    },
  ],
} as const satisfies FormConfig;

Inside the wrapper

The wrapper component reads the array's key from fieldInputs, then injects EventBus and dispatches the append event itself.

@Component({
  selector: 'demo-array-section-wrapper',
  template: `
    <div class="demo-section">
      <div class="demo-section__header demo-section__header--with-action">
        <span>{{ title() }}</span>
        <button type="button" (click)="addItem()">+ {{ addLabel() ?? 'Add' }}</button>
      </div>
      <div class="demo-section__body">
        <ng-container #fieldComponent></ng-container>
      </div>
    </div>
  `,
})
export default class ArraySectionWrapper implements FieldWrapperContract {
  private readonly eventBus = inject(EventBus);

  readonly fieldComponent = viewChild.required('fieldComponent', { read: ViewContainerRef });
  readonly title = input<string>();
  readonly addLabel = input<string>();
  readonly itemTemplate = input<FieldDef<unknown> | readonly FieldDef<unknown>[]>();
  readonly fieldInputs = input<WrapperFieldInputs>();

  addItem(): void {
    const key = this.fieldInputs()?.key;
    const template = this.itemTemplate();
    if (!key || !template) return;
    this.eventBus.dispatch(arrayEvent(key).append(template));
  }
}

Why this pattern

  • Config stays focused. The array owns the data shape; the wrapper owns the chrome plus the add action.
  • Reusable. Apply the same wrapper to any array by changing the itemTemplate — no custom buttons to wire up.
  • Consistent placement. Every array wrapped this way has the Add button in the same visual position.