A new approach to have Dynamic Forms in Angular
Tags: angular, dynamic, forms, npmpackage
Published: 2021-04-15 14:03:00

---
canonical_url: https://dev.to/myndpm/a-new-approach-to-have-dynamic-forms-in-angular-5d11
---

> **TL;DR**
> Go to [Stackblitz](https://stackblitz.com/edit/myndpm-dyn-forms?file=src/app/simple-form/simple.form.ts) and witness the power of `@myndpm/dyn-forms`, check its synthetic [source code](https://github.com/myndpm/open-source/tree/master/libs/forms) and join the [GitHub Discussions](https://github.com/myndpm/open-source/discussions) to design the upcoming features based on our experiences with Angular Forms.

As in most companies, at *Mynd* we build forms, filters, tables and display *views* for different purposes. We handle a ton of entities and we have custom components in our Design System to satisfy our needs. In this complex scenario, avoid boilerplate is a must, and to speed up the development process and facilitate the implementation and maintenance of these *views*, we built some base libraries to abstract the requirements into configuration objects that enable us to *easily modify* a form, a filter, a table, without touching a view template (most of the times).

So the question is: can we implement a standard, flexible enough layer to do this job and be shared with the Angular Community?

# A bit of History

This challenge has been addressed by [many developers and companies](https://ngx.tools/#/search?q=dynamic%20forms) in many ways, we even have an [official documentation guide](https://angular.io/guide/dynamic-form) on this topic; some approaches ends up with a template processing different types of fields with a `ngSwitch`, others vary on the entrypoint component depending on the desired UI framework, or their config objects are not standardized and uses different field names for the same task on different controls. They are not completely generic, typed and/or extensible.

The ideal scenario is to have a strictly typed and serializable configuration object, so we are able store it in the state or the database without problems, as well as the ability to share some recipes with the community for common use-cases without complex functions involved, just a JSON object; there are a lot of good ideas out there, and we're in the process of [discussing](https://github.com/myndpm/open-source/discussions) the best possible solutions for each topic.

Technically speaking, the challenge is to translate a Config Object (`JSON`) into a functional Form (`FormGroup`) being able to build any required nested structure, composing Control (inputs, selects, etc) into Containers to group them and customize the layout (cards, panels, etc).

## What's New?

`@myndpm/dyn-forms` is not just a "dynamic" forms library providing you a finite set of controls, or limiting your creativity and possibilities in any way. This library aims to be a quite *generic* and *lightweight* layer on the top of Angular's Form Framework, allowing us to build, extend and maintain our forms from their metadata, giving us more time to focus our attention on the business-logic requirements, custom validations, etc.

Moreover, we keep the control of our *model* and the Angular Form, manipulating the supported methods of `FormGroup`, `FormArray` and `FormControl`, giving the responsibility of building the form hierarchy and its presentation to the library, but patching and listening any `valueChange` as we are used to.

## Creating a DynForm

All we need is to import `DynFormsModule` to our `NgModule` and also provide the `DynControls` that we need in our form. As a demostrative implementation, we mocked `DynFormsMaterialModule` at `@myndpm/dyn-forms/ui-material` to enable you right now to see how it works with some basic components:
```typescript
import {
  DynFormsMaterialModule
} from '@myndpm/dyn-forms/ui-material';

@NgModule({
  imports: [
    DynFormsMaterialModule.forFeature()
```
This package also provides a typed `createMatConfig` *Factory Method* that (hopefully) will facilitate the development experience while creating configuration objects, by supporting type-checks (with overloads for the different controls):
```typescript
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 },
        }),
        ...
```
now you're ready to invoke the Dynamic Form in your template
```html
<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>
```
and voilá!
[![simple-form demo at Stackblitz](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/je3a0w9gf3nibih0enu9.png)](https://stackblitz.com/edit/myndpm-dyn-forms?file=src/app/simple-form/simple.form.ts)

## Where the magic happens

The main feature is the ability to plug-in new Dynamic Form Controls, provide customized ones for some particular requirements, or integrate third-party components into our forms, with ease!

For this matter, Angular's `InjectionTokens` are the way to apply the [Dependency Inversion Principle](https://dev.to/this-is-angular/plugin-architecture-for-angular-libraries-using-dependency-injection-5d8k), so we do not rely on the controls provided by a single library anymore, but any `NgModule` (like `DynFormsMaterialModule`) can provide new controls via the `DYN_CONTROL_TOKEN` by registering the component to be loaded dynamically (`DynControl`) with an "ID" (`INPUT`, `RADIO`, `SELECT`, etc).

From there the Dynamic Form Registry can let the `Factory` know what component it should load for a given "ID"
```typescript
@Injectable()
export class DynFormRegistry {
  constructor(
    @Inject(DYN_CONTROLS_TOKEN) controls: ControlProvider[]
  )
```
it's super hard to name these kind of "id" and "type" fields, so trying to keep the context clear, the `ControlProvider` interface consists of:
```typescript
export interface InjectedControl {
  control: DynControlType;
  instance: DynInstanceType;
  component: Type<AbstractDynControl>;
}
```
1. the `control` identificator is the 'string' to reference the dynamic control from the Config Object
2. the `instance` is the type of `AbstractControl` that it will create in the form hierarchy (`FormGroup`, `FormArray` or `FormControl`), and
3. the `component` which should extend any of the Dynamic Control classes (`DynFormGroup`, `DynFormArray`, `DynFormControl` or `DynFormContainer`) implementing the simple contract [explained here](https://github.com/myndpm/open-source/tree/master/libs/forms#extending).

## Configuration Object Typing

You can define your Form with an array of `controls` which can have some sub`controls`; with this nested structure you can build any hierarchy to satisfy your needs (like in the example). This configuration unit is specified by the `DynBaseConfig` interface which follows a simple Tree structure:
```typescript
export interface DynBaseConfig<TMode, TParams> {
  name?: string;
  controls?: DynBaseConfig<TMode>[];
  modes?: DynControlModes<TMode>;
}
```

The form also supports different "modes". Modes are partial overrides that we can apply to the main Control Configuration depending on a particular situation. In the [simple-form demo](https://mynd.dev/demos/dyn-forms/simple-form) we show an example of this: a `display` mode where we define a `readonly: true` parameter to be passed to all the dynamic controls, and they react changing their layout or styles. These "modes" are just a custom `string`, so the configuration is open to any kind of `mode` that you'd like to define.

In the `DynFormConfig` you can specify the global override for each mode:
```typescript
const config: DynFormConfig<'edit'|'display'> = {
  modes: {
    display: {
      params: { readonly: true }
```
and you can also override the configuration of a single control for a given a mode, like this `RADIO` button being changed to an `INPUT` control when we switch the form to `display` mode:
```typescript
createMatConfig('RADIO', {
  name: 'account',
  params: { label: 'Create Account', color: 'primary' },
  modes: {
    display: {
      control: 'INPUT',
      params: { color: 'accent' },
```
In this case, the `control` will be overriden but the `params` will be merged and we will have the original label in the `display` mode.

## Feedback WANTED

With this brief introduction to this powerful library, we hope that you join its design/development efforts by sharing your experience/ideas/point of view in the [GitHub Discussions](https://github.com/myndpm/open-source/discussions) opened for the upcoming features, creating Pull Request extending or adding new Material/TaigaUI/any controls, or reporting [Issues](https://github.com/myndpm/open-source/issues) that you find.

There are some challenges to be addressed, like a standard way to handle the Validations and show the respective Error message; handle the visibility of a control depending on some conditions; these topics have opened discussions to collect ideas and figure out a solution.

We might write more articles explaining the internals to analyze and improve the chosen architecture.

Without further ado, [enjoy it](https://www.npmjs.com/package/@myndpm/dyn-forms)!

*// PS. We are hiring!*