ng-forge — dynamic forms for Angular
Find your way around — six task-shaped paths into the docs.
Pick what you're trying to do, jump straight to the page that answers it. A general FAQ and the most common pitfalls are at the bottom.
Render a form
Get the library wired up and put a form on screen.
Pick the right field
What kind of input does the user need to give you?
Group, lay out, repeat
Compose multiple fields into structured forms.
user.address.street shape.Rows + containersFlat layout primitives — no nesting in the form value.Multi-step pagesWizard flows with per-page validation and navigation.Repeating arraysSimplified API for primitives, full API for object arrays.Wrap a fieldAdd a card, panel, or label around any field via wrappers.Validate input
Stop bad data before submission, in three execution models.
Make fields react
Show, hide, compute, fetch — fields that respond to other fields.
Production concerns
Type safety, value control, internationalisation, migration.
FormConfig + form-value type from your spec.Migrating from ngx-formlyConcept-by-concept mapping with side-by-side code.FAQ
General-purpose questions worth knowing the answer to before you start.
How do I add a field type ng-forge does not ship?
provideDynamicForm(...) is variadic — it takes a list of field-type registrations. Spread an adapter's bundle (...withMaterialFields() registers all the built-in Material fields), then append your own: { name: 'rich-text', loadComponent: () => import('./rich-text'), mapper: valueFieldMapper }. Your component is a plain standalone Angular component that receives Signal Forms' FieldTree<T> via input.required(). See Adding custom fields.How do I lazy-load select options from an API?
targetProperty: 'options' derivation with source: 'http'. Pass the URL (or query params with field-value interpolation), a responseExpression that maps the response to { value, label }[], and dependsOn if it should re-fetch when another field changes. See Async data.Can ng-forge run side by side with ngx-formly during a migration?
@ngx-formly/*.How do I share a config across multiple forms?
FormConfig is a plain TypeScript object — extract reusable pieces (a field, a validator entry, a default props object) as named consts and import them. For application-wide defaults, use defaultProps on the form or adapter-level providers like withMaterialFields({ appearance: 'fill' }).How do I localise labels and validation messages?
string | Signal<string> | Observable<string> — wire them to your i18n service. Validation kinds map to messages via per-field validationMessages or form-level defaultValidationMessages. See the i18n guide.How do I export the submitted form value as plain JSON?
(submitted) event emits the form value directly — JSON.stringify(value) is enough. To strip hidden, disabled, or readonly fields, set excludeValueIfHidden (and the sibling options) on the form config or via withValueExclusionDefaults(). See Value exclusion.Are hidden field values still in the submitted form value?
excludeValueIfHidden: true to strip them at submission output time, or wire a derivation that clears the value when the hide condition is true (formly's resetOnHide behaviour).How do I debounce a field, an HTTP derivation, or a custom condition?
trigger: 'debounced' and debounceMs on the derivation, condition, or HTTP validator that reads the value. The Signal Forms substrate commits on every change — there is no updateOn: 'blur' equivalent today, so debouncing the consumers is the closest workaround for blur-style commit timing.Does ng-forge work without one of the four official UI adapters?
Does ng-forge use Reactive Forms (FormGroup / FormControl)?
@angular/forms/signals). The primitive is FieldTree<T> — a signal-native tree of value, validity, dirty/touched state, and errors. Reactive Forms still works in Angular, but the two systems don't share types or APIs.Is there a built-in file-upload field?
valueFieldMapper: a standalone component that renders an <input type='file'>, captures File | FileList into the field's value, and (optionally) uploads to your backend via a custom validator or value-derivation. See Adding custom fields.Is ng-forge SSR / hydration safe?
@defer blocks that wrap heavy components. If you author a custom field type, follow the same rule the core library follows: keep all per-form state in DI-scoped services, never in module-level singletons.Is there a codemod or automated migration tool from formly?
How big is the bundle?
import() per field type, so a form that only uses input and select only fetches those two component bundles. UI adapters (Material, PrimeNG, Bootstrap, Ionic) themselves are independent packages — only the one you install ships.Which Angular and browser versions are supported?
Common pitfalls
The patterns that trip people up — symptom, fix, and a link to the canonical doc.
type: 'group' is nesting my form value
SymptomYou wanted a section, you got { section: { firstName, … } } instead of flat keys.
FixUse type: 'container' for visual grouping (flat value + wrapper slot). Reserve group for genuine data nesting like user.address.
Hidden field values still appear in the submitted form
SymptomA hidden field's previous value is in the (submitted) payload.
Fixng-forge filters values at submission output, not state. Set excludeValueIfHidden: true on the form options, or apply globally via withValueExclusionDefaults().
Custom validator runs but no message renders
SymptomValidator returns { kind: 'noSpaces' }, field is invalid, no message appears, console warns about a missing kind.
FixValidators only declare kinds. Messages live separately in validationMessages (per-field) or defaultValidationMessages (form-level), so the same kind can be reused and localised.
Custom-function condition does not react to dependencies
Symptomcondition: { type: 'custom', functionName: 'isAdult' } reads ctx.formValue.age but doesn't re-evaluate when age changes.
FixCustom and HTTP variants can't be statically introspected. List dependencies explicitly with dependsOn: ['age']. Same applies to HTTP derivations.
Select options blank — backend returns id / name keys
SymptomDropdown shows empty rows or [object Object].
FixFieldOption is fixed at { value, label, disabled? }. Remap once at the source, or inside a targetProperty: 'options' derivation with responseExpression.
Deeper-than-one-level derivation paths throw at runtime
SymptomSetting targetProperty: 'props.config.minDate' throws a DynamicFormError when the form initialises.
FixUp to one level of nesting is supported (options, label, props.minDate). Deeper paths require a custom field type that reads the value off a sibling field directly.
Still stuck?
The MCP server lets an LLM in your IDE scaffold configs for you. Discord is the fastest way to ask a real human. GitHub takes long-form bug reports.