A container is a layout-only field that chains wrapper components around its children. It gives you a place to stack wrappers without the wrappers having to know about each other — no form nesting, no new form context, values flatten to the parent just like a row.

When to reach for a container

  • You want one wrapper (or several stacked) around a group of sibling fields, not a single field.
  • You don't want a new form key or nested value shape — the fields should flatten.
  • You want a purely visual grouping: a section header, a card, a collapsible panel.

If you need a nested value shape (e.g. user.address.street), use a form group instead. If you only need horizontal layout, use a form row.

Interactive Demo

Basic Shape

{
  key: 'contactSection',
  type: 'container',
  wrappers: [{ type: 'css', cssClasses: 'card' }],
  fields: [
    { key: 'firstName', type: 'input', label: 'First Name', value: '' },
    { key: 'email', type: 'input', label: 'Email', value: '' },
  ],
}

The wrappers array chains outermost-first: the first wrapper wraps everything else.

{
  key: 'section',
  type: 'container',
  wrappers: [
    { type: 'section', title: 'Contact' },   // outermost
    { type: 'css', cssClasses: 'p-4' },       // innermost
  ],
  fields: [/* ... */],
}

See Writing a Wrapper for how to build your own, and Registering and Applying for how to make a custom wrapper type available.

Container vs row vs group

Container type Adds form nesting? Horizontal layout? Wrapper chain?
row No (flat) Yes (12-col grid) Implicit row
group Yes (under key) No No
container No (flat) No Yes (explicit)

A row is itself a container-field under the hood — the row type resolves to the container renderer with an auto-injected row wrapper. Keep writing type: 'row' for horizontal layouts; use type: 'container' when the wrappers are the point.

Value Structure

Containers flatten their children into the parent form value:

// Config
{
  fields: [
    {
      key: 'contactSection',
      type: 'container',
      wrappers: [{ type: 'section', title: 'Contact' }],
      fields: [
        { key: 'firstName', type: 'input', value: '' },
        { key: 'email', type: 'input', value: '' },
      ],
    },
  ];
}

// Form value (flat — no `contactSection` key)
{
  firstName: 'Ada',
  email: 'ada@example.com',
}

The key on the container itself is used for DOM id / test selectors only. It never appears in the form value.

Nesting Rules

Containers follow the same rules as rows — they are layout-only.

Containers can appear inside:

  • The top-level form
  • Pages
  • Groups
  • Array items

Containers cannot appear inside:

  • Rows
  • Other containers (treat each container as a single wrapping layer — stack wrappers on one container instead)

Containers can contain:

  • Leaf fields (input, select, checkbox, …)
  • Group fields (for nested data)
  • Array fields (for repeating sections)
  • Rows (for horizontal layout within the container)

Conditional Visibility

Like other containers, container supports the logic property for conditional show/hide:

{
  key: 'shippingSection',
  type: 'container',
  wrappers: [{ type: 'section', title: 'Shipping' }],
  logic: [{
    type: 'hidden',
    condition: {
      type: 'fieldValue',
      fieldPath: 'sameAsBilling',
      operator: 'equals',
      value: true,
    },
  }],
  fields: [
    { key: 'street', type: 'input', value: '' },
    { key: 'city', type: 'input', value: '' },
  ],
}

Only 'hidden' is supported as a logic type on containers. See Conditional Logic for the full condition surface.

Complete Example

import { Component } from '@angular/core';
import { DynamicForm } from '@ng-forge/dynamic-forms';

@Component({
  selector: 'app-profile-form',
  imports: [DynamicForm],
  template: `<form [dynamic-form]="formConfig"></form>`,
})
export class ProfileFormComponent {
  formConfig = {
    fields: [
      {
        key: 'identitySection',
        type: 'container',
        wrappers: [{ type: 'section', title: 'Identity' }],
        fields: [
          { key: 'firstName', type: 'input', label: 'First Name', value: '', required: true },
          { key: 'lastName', type: 'input', label: 'Last Name', value: '', required: true },
        ],
      },
      {
        key: 'contactSection',
        type: 'container',
        wrappers: [{ type: 'section', title: 'Contact' }],
        fields: [
          { key: 'email', type: 'input', label: 'Email', value: '', email: true },
          { key: 'phone', type: 'input', label: 'Phone', value: '' },
        ],
      },
      { key: 'submit', type: 'submit', label: 'Save' },
    ],
  };
}

The resulting form value is flat — containers contribute structure to the UI, not the data:

{
  firstName: 'Ada',
  lastName: 'Lovelace',
  email: 'ada@example.com',
  phone: '',
}

Next Steps