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

PrimeNG PrimeNG
Loading live example

Config

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

const tagItemTemplate = [
  { key: 'value', type: 'input', label: 'Tag', required: true, minLength: 2 },
  { key: 'removeTag', type: 'remove-array-item', 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 FieldWrapper {
  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, with no custom buttons to wire up.
  • Consistent placement. Every array wrapped this way has the Add button in the same visual position.