v1.0 · production-ready

Forms are boring.Your code shouldn't be.

A type-safe forms engine for Angular. Write the config, ship the form.

Fromngx-formly?

See it in action

The exact config you write, and the live form it renders. Flip between them.

The hard parts, declarative

The stuff that usually means imperative spaghetti, declared in the config.

Conditional fields

A field can hide itself or turn required based on another field's value, re-checked as the form changes.

{
  key: 'company',
  type: 'input',
  logic: [{
    type: 'required',
    condition: {
      type: 'fieldValue',
      fieldPath: 'accountType',
      operator: 'equals',
      value: 'business',
    },
  }],
}

Server-driven values

Pull a field's value from an HTTP endpoint; the requests debounce and cancel as inputs change.

{
  key: 'shippingCost',
  type: 'input',
  logic: [{
    type: 'derivation',
    source: 'http',
    http: { url: '/api/shipping' },
    responseExpression: 'response.cost',
    dependsOn: ['zip'],
  }],
}

Multi-step wizards

Split a long form into pages that validate one step at a time, with next and previous navigation.

{
  fields: [
    {
      type: 'page',
      key: 'info',
      fields: [...],
    },
    {
      type: 'page',
      key: 'review',
      fields: [...],
    },
  ],
}

Repeatable arrays

Repeatable rows, nested groups included, that users can add and remove from the config.

{
  key: 'contacts',
  type: 'array',
  fields: [
    {
      key: 'name',
      type: 'input',
      required: true,
    },
  ],
}

It's all just data

A form is plain data, all the way down to its validation, derivations, and conditions. None of it depends on a function, so a backend can build one as JSON and hand it to the client.

field.ts
{
  key: 'username',
  type: 'input',
  label: 'Username',
  validators: [{
    type: 'http',
    http: { url: '/api/check-username' },
    responseMapping: {
      validWhen: 'response.available',
      errorKind: 'taken',
    },
  }],
}
  • Validation that hits your API. An HTTP check is a declared request; in-flight requests are cancelled when inputs change.
  • Computed values as expressions.derivation: 'formValue.qty * formValue.price' replaces the subscription you'd otherwise wire up by hand.
  • Conditions are data. A rule that hides or requires a field is JSON you can store, not a callback.
  • Store it anywhere. It lives happily in a database column or a CMS entry, and a form changes without a redeploy.

Typed end to end

Mistype a field and it won't compile. as const satisfies FormConfig infers your submit value from the config, with zero hand-written interfaces.

form.config.ts
const config = {
  fields: [
    {
      key: 'email',
      type: 'input',
      required: true,
      email: true,
    },
    {
      key: 'age',
      type: 'input',
      required: true,
      props: { type: 'number' },
    },
  ],
} as const satisfies FormConfig;

// Inferred, no hand-written types:
type Value = InferFormValue<typeof config.fields>;

Already using Zod or Valibot? Pass them through any Standard Schema with schema: standardSchema(yourSchema) and they validate as-is.

You don't have to hand-write it

Generate the config from your API contract or your editor, or fetch it at runtime.

From an OpenAPI spec

Your OpenAPI spec becomes a typed FormConfig.

# Turn an OpenAPI document into
# typed, ready-to-render FormConfigs
npx @ng-forge/openapi-generator \
  --spec ./openapi.yaml \
  --output ./src/forms

From your AI assistant

An MCP server lets Claude, Cursor, or Copilot generate and check a config from inside your editor.

{
  "ng-forge": {
    "command": "npx",
    "args": [
      "-y",
      "@ng-forge/dynamic-form-mcp"
    ]
  }
}

From your backend

Store configs in your database. Change a form without shipping a new build.

export class SurveyComponent {
  private http = inject(HttpClient);

  // Fetch the config at runtime
  config = toSignal(
    this.http.get<FormConfig>(
      '/api/forms/survey',
    ),
  );
}

Works with your stack

Swap one import. The same config renders in Material, PrimeNG, Ionic, or Bootstrap.

Need something else? Build your own adapter by implementing the field contract.

Production-grade by default

Accessibility, i18n, and performance come built in, and everything is extensible once you outgrow the defaults.

Accessible

Built on adapter components that are accessible already, with ARIA and focus handled on top.

Internationalized

Any text in a form can be an Observable or a Signal, so Transloco, ngx-translate, or your own translation source works unchanged.

Extensible

Reach for a prefix or suffix addon, wrap a field in your own markup, or drop to a fully custom component when the built-ins run out.

Fast

Signal-native and zoneless-ready. Field components and the derivation engine load on demand, so a form only ships what it renders.

Stop writing forms. Start forging them.

  • MIT licensed
  • Angular 22+
  • Signal-native
  • Zoneless-ready
  • 4 UI adapters
npm install @ng-forge/dynamic-forms @ng-forge/dynamic-forms-material