Feature overview

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.

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?
Use a 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?
Yes. Different package names, different injection tokens, different component selectors. Install ng-forge, port one form at a time, deprecate formly when nothing imports @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?
Labels, placeholders, and hint text accept 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?
The (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?
Yes by default — ng-forge keeps hidden values live so a hide/show toggle preserves what the user typed. Opt out with 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?
Debouncing happens on the consumer side: set 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?
Yes — every adapter is built on the same public surface, so you can ship a custom adapter for Kendo, NG-ZORRO, NativeScript, or an in-house design system. See Building an adapter.
Does ng-forge use Reactive Forms (FormGroup / FormControl)?
No. ng-forge is built on Angular Signal Forms (@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?
Not today — file inputs are not a built-in field type in any adapter. Implement one as a custom field with 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?
Yes. The library is built around Angular signals (no module-scoped mutable state), and the docs site itself is server-rendered with full incremental hydration through the @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?
No automated migration tool today — port manually. The concept-mapping table in the migration guide is the closest thing to a porting cheatsheet; the MCP server lets an LLM in your IDE generate large chunks of the new config from a description of the old one. A codemod is on the wishlist but not committed work.
How big is the bundle?
ng-forge is a batteries-included framework — the engine itself is intentionally large because it ships the full validation, conditional logic, derivation, schema, and array-management surface needed for API-driven forms (where the form shape is unknown at build time). What you do not pay for upfront: every adapter loads its field components via dynamic 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?
Angular 21+ (signal-native APIs depend on it). Browser-wise, ng-forge supports the same matrix as Angular 21 — modern evergreen browsers (Chrome / Edge / Firefox / Safari current and previous major). No IE 11.

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.

Read the full guide

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().

Read the full guide

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.

Read the full guide

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.

Read the full guide

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.

Read the full guide

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.

Read the full guide

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.