Addons decorate a field with inline content — a search icon, a clear button, a currency symbol, a password-visibility toggle — placed in the field s prefix or suffix slot. They re typed, JSON-safe, and ship as a first-class feature of every UI adapter.
Prerequisites. Addons attach to a field, not the form. You need a working ng-forge setup first:
provideDynamicForm(...with{Adapter}Fields())at the application config and a[dynamic-form]-bearing form using a field type that supports addons (today: every adapter'sinput). See Getting Started if you re new to ng-forge.
Quickstart
{
key: 'search',
type: 'input',
label: 'Search',
addons: [
{ slot: 'prefix', kind: 'mat-icon', icon: 'search', ariaLabel: 'Search' },
{ slot: 'suffix', kind: 'mat-button', icon: 'close', ariaLabel: 'Clear', preset: 'clear' },
],
}
Live example
A universal addon (works the same in every adapter, no per-kind branching):
Available kinds
| Kind | Renders | Notes |
|---|---|---|
mat-icon | <mat-icon>search</mat-icon> | Material Icons name. Add ariaLabel for non-decorative icons. |
mat-button | <button mat-button> / <button mat-icon-button> | Exactly one of preset / actionRef / action. Severity/color, label, icon all supported. |
text | <span> with DynamicText | Universal; supports plain strings, signals, observables, i18n keys. |
template | Named <ng-template> | Reference by templateKey. JSON-safe — backend ships the key, FE supplies the template. |
component | Arbitrary Angular component | Code-only — dropped from JSON-derived configs. |
How addons render
Addons render as direct <mat-form-field> children with matPrefix / matSuffix attribute directives applied to <df-addon-slot> — Material's native projection API.
The wrapper is dropped entirely when every addon is reactively hidden — no empty group element.
Provider setup
import { provideDynamicForm } from '@ng-forge/dynamic-forms';
import { withMaterialFields } from '@ng-forge/dynamic-forms-material';
// mat-icon + mat-button work out of the box.
provideDynamicForm(...withMaterialFields());
// Standalone — addon kinds without the field types:
// provideDynamicForm(...myCustomFields(), withMaterialAddons());
Reactive hidden and disabled
Both axes accept DynamicValue<boolean> — any of boolean, Signal<boolean>, or Observable<boolean>. Four equivalent shapes for the same toggle:
{ ..., hidden: true } // Static — JSON-safe.
{ ..., hidden: signal(false) } // Signal — code-only.
{ ..., hidden: computed(() => !hasValue()) } // Derived — code-only.
{ ..., hidden: visibility$ } // Observable — code-only.The classic "show clear button only when input has value" pattern, in your active adapter:
const hasValue = computed(() => (formValue()?.search?.length ?? 0) > 0);
{
slot: 'suffix',
kind: 'mat-button',
icon: 'close',
ariaLabel: 'Clear',
preset: 'clear',
hidden: computed(() => !hasValue()),
}
When hidden resolves to true the addon stays in DOM but is display: none — cheaper than tearing down the component, and the reactive transition is instant.
Authoring forms in JSON? Reactive values can't survive serialization — see Reactive addons from JSON.
Accessibility
Icon kinds emit aria-hidden="true" by default. When the icon conveys meaning (status, action), set ariaLabel. Icon-only button kinds (no label) require ariaLabel — TypeScript and the runtime validator both enforce this.
Troubleshooting
- Addon doesn t render at all. Check the console for
[Dynamic Forms]warnings. Common causes: the active adapter swith*Fields()helper isn t inprovideDynamicForm; thekindstring belongs to a different adapter (e.g.prime-buttonin a Material form); the host field type doesn t support addons. - Icon-only button has no
ariaLabel. TypeScript and the runtime validator both refuse this — setariaLabel. For genuinely decorative icons, preferkind: 'text'or the adapter s*-iconkind. actionRefwarning at click time. The handler name isn t registered. Did you callprovideAddonActions({ runSearch: ... })for that name?- Inline function silently dropped. Configs with
source: 'json'strip functions onaction,hidden,disabled,loadingat validation time — they cant round-trip through JSON. See Reactive addons from JSON. - Material
MatFormFieldControlContentChild missing. Caused by wrapping the input in a template that breaks Material s content-projection query. Renderdirectly inside.
Where to next
- Presets and Actions — built-in click presets (
clear,reset,paste,copy,toggle-password-visibility),actionReffor registered handlers, and inlineactionfor code-only behaviour. - Custom Kinds — register your own addon kind (rating widget, status pill, anything) with
withCustomAddon(...)and augment the type-level extensions seam.