TL;DR
Head over to Stackblitz to see @myndpm/dyn-forms in action, explore the source code, and share your ideas in the GitHub Discussions to help shape the future of Angular Forms.

Across most organizations, including Mynd, we routinely construct forms, filters, tables, and other views. We manage a large number of entities and rely on bespoke components from our Design System to meet our needs. In such a complex environment, minimizing boilerplate is essential. To accelerate development and ease the construction and upkeep of these views, we created foundational libraries that distill requirements into configuration objects, letting us adjust a form, filter, or table with minimal‑to‑no template edits.

This raises a key question: is it possible to build a standardized, adaptable layer that can handle this task and be openly shared with the Angular ecosystem?

Looking Back

This problem has been tackled by numerous developers and teams in various ways—there's even an official Angular guide on it. Some solutions involve a template that uses ngSwitch to render different field types, while others choose the entrypoint component based on the target UI library. Often, config objects use inconsistent field names for the same purpose across different controls, and the approaches can lack true genericity, type safety, or extensibility.

The ideal would be a strictly typed, serializable configuration object. That way, it can be safely stored in state or a database, and we could share reusable code patterns with the community for common scenarios—using nothing more than plain JSON, no complex functions involved. There’s a wealth of good ideas out there, and we’re actively discussing the best ways to approach each one.

At its core, the challenge is turning a Config Object (JSON) into a working FormGroup, capable of building any required nested structure—combining Controls (inputs, selects, etc.) into Containers to group them and define layouts (cards, panels, etc.).

What’s Different Here?

@myndpm/dyn-forms isn’t just another "dynamic" forms library limited to a fixed set of controls or stifling your creativity. Instead, it aims to be a generic, lightweight layer sitting on top of Angular’s Form Framework. With it, we can build, extend, and maintain forms directly from their metadata, freeing up more time to focus on business logic, custom validations, and other concerns.

What’s more, you retain full control over your model and the underlying Angular Form. The library handles the construction of the form hierarchy and its presentation, while you can still patch values and subscribe to valueChange just as you normally would with the standard FormGroup, FormArray, and FormControl methods.

Setting Up a DynForm

To get started, you only need to import DynFormsModule into your NgModule and provide the DynControls you plan to use. For demonstration, we’ve mocked a DynFormsMaterialModule at @myndpm/dyn-forms/ui-material so you can immediately see how it works with basic components:

import {
  DynFormsMaterialModule
} from '@myndpm/dyn-forms/ui-material';

@NgModule({
  imports: [
    DynFormsMaterialModule.forFeature()
Enter fullscreen mode Exit fullscreen mode

That package also includes a typed createMatConfig Factory Method, which (hopefully) makes creating config objects easier by providing type‑checks, with overloads for each control type:

import { createMatConfig } from '@myndpm/dyn-forms/ui-material';

@Component(...) {
form = new FormGroup({});
mode = 'edit';
config = {
  controls: [
    createMatConfig('CARD', {
      name: 'billing',
      params: { title: 'Billing Address' },
      controls: [
        createMatConfig('INPUT', {
          name: 'firstName',
          validators: ['required'],
          params: { label: 'First Name' },
        }),
        createMatConfig('INPUT', {
          name: 'lastName',
          validators: ['required'],
          params: { label: 'Last Name' },
        }),
        createMatConfig('DIVIDER', {
          params: { invisible: true },
        }),
        ...
Enter fullscreen mode Exit fullscreen mode

Once that’s set up, you’re ready to use the Dynamic Form directly in your template:

<form [formGroup]="form">
  <dyn-form
    [config]="config"
    [form]="form"
    [mode]="mode"
  ></dyn-form>

  <button type="button" (click)="mode = 'display'">
    Switch to Display Mode
  </button>
</div>
Enter fullscreen mode Exit fullscreen mode

And just like that, you’re done!
simple-form demo at Stackblitz

What Happens Under the Hood

The standout capability is the ease of plugging in new Dynamic Form Controls, whether you need a custom component for a specific requirement or want to integrate third‑party controls into your forms.

To achieve this, we use Angular’s InjectionTokens to apply the Dependency Inversion Principle. You’re no longer tied to a single library’s controls—any NgModule (such as DynFormsMaterialModule) can register new controls via the DYN_CONTROL_TOKEN by associating a dynamic component (DynControl) with an identifier (INPUT, RADIO, SELECT, etc.).

From there, the Dynamic Form Registry tells the Factory which component to load for a given identifier:

@Injectable()
export class DynFormRegistry {
  constructor(
    @Inject(DYN_CONTROLS_TOKEN) controls: ControlProvider[]
  )
Enter fullscreen mode Exit fullscreen mode

Naming “id” and “type” fields is always tricky, so to keep things clear, the ControlProvider interface breaks down as follows:

export interface InjectedControl {
  control: DynControlType;
  instance: DynInstanceType;
  component: Type<AbstractDynControl>;
}
Enter fullscreen mode Exit fullscreen mode
  1. The control identificator is the string used to reference the dynamic control from the Config Object.
  2. The instance defines which type of AbstractControl will be created in the form hierarchy—either FormGroup, FormArray, or FormControl.
  3. The component should extend any of the Dynamic Control base classes (DynFormGroup, DynFormArray, DynFormControl, or DynFormContainer) and implement the straightforward contract described here.

Typing the Configuration Object

The form definition relies on an array of controls, each of which may contain its own nested controls. That recursive arrangement lets you model virtually any form hierarchy you need, as demonstrated in the earlier example. The shape of a single configuration unit is governed by the DynBaseConfig interface, which mirrors a straightforward tree structure:

export interface DynBaseConfig<TMode, TParams> {
  name?: string;
  controls?: DynBaseConfig<TMode>[];
  modes?: DynControlModes<TMode>;
}
Enter fullscreen mode Exit fullscreen mode

Beyond the base hierarchy, the library introduces the concept of "modes". A mode is essentially a partial override layer that can be applied to the main control configuration depending on the current context. The simple-form demo illustrates this idea: it defines a display mode that injects readonly: true into every dynamic control, and each control responds by adjusting its layout or appearance. Because modes are just plain string values, the system remains open-ended — you can invent any mode name that fits your use case.

Global mode overrides are declared in the DynFormConfig like so:

const config: DynFormConfig<'edit'|'display'> = {
  modes: {
    display: {
      params: { readonly: true }
Enter fullscreen mode Exit fullscreen mode

Per-control overrides are also possible. For instance, a RADIO button can be replaced by an INPUT field when the form switches to display mode:

createMatConfig('RADIO', {
  name: 'account',
  params: { label: 'Create Account', color: 'primary' },
  modes: {
    display: {
      control: 'INPUT',
      params: { color: 'accent' },
Enter fullscreen mode Exit fullscreen mode

In this scenario, the control type is swapped out, but the params are merged — so the original label remains intact even in display mode.

Feedback WANTED

This quick tour only scratches the surface of what the library can do, but we hope it's enough to pique your interest. We'd love for you to get involved in shaping its future — whether that means sharing your perspective on the GitHub Discussions about upcoming features, submitting a Pull Request that adds or improves controls for Material, TaigaUI, or any other UI kit, or filing an Issue when something doesn't behave as expected.

Several design questions are still open. For example, there's no settled convention for handling validations and rendering the associated error messages, and the same goes for conditionally showing or hiding a control. Both topics have active discussion threads where we're gathering input to converge on a clean solution.

We may follow up with more articles that dig into the internals, with an eye toward evaluating and refining the architecture we've settled on.

With all that said, go ahead and try it out!

// PS. We are hiring!