Build dynamic Angular forms on-the-fly
Tags: angular, webdev, javascript, tutorial
Published: 2019-12-10 13:20:12

Forms never been a simple thing to deal with in Angular projects: you need to design each of them "properly" in the markup but also in the component with `FormControls` and make sure everything fits well together.
You also have to keep in mind that it will probably change frequently to meet the rapidly changing business and regulatory requirements.

We will see how to create on-the-fly forms with metadata that describes the business object model.

# The metadata
The metadata will feed our system to indicate what will be:
* the values
* the field name
* the field type
* the validation conditions
* other things like placeholders, patterns and so on...

It will be structured in JSON, but you can obviously use the language you want: JSON+LD, csv, XML or whatever format you like.

The data source also could be an API, a file or any other available source of data.

In JSON, it will look like this (you can obviously adapt it to your needs):

```typescript
// question-base.ts

export class QuestionBase<T> {
  value: T;
  key: string;
  label: string;
  required: boolean;
  order: number;
  controlType: string;
  placeholder: string;
  iterable: boolean;

  ...
}

```

This will be a skeleton for every other kind of elements we would create like:
* input
* textarea
* select
* any other form field...

Each of these form elements will share the same `Class` and extends it for their proper needs. For example `option` will only be useful for `<select>` element:

```typescript
// question-dropdown.ts

import { QuestionBase } from './question-base';

export class DropdownQuestion extends QuestionBase<string> {
  controlType = 'dropdown';
  options: { key: string, value: string }[] = [];

  constructor(options: {} = {}) {
    super(options);
    this.options = options['options'] || [];
  }
}
```

# The component
In order to make the code flexible, reliable, easily testable and maintainable, it has been spared in two parts. Firstly, there is the component (`app-dynamic-form`) that will always be called in app's components as a wrapper:

```html
<!-- app.component.html -->

<app-dynamic-form #dynamicForm
                  [questions]="questions"></app-dynamic-form>
```

and then, the `app-question` component that will be called and repeated by `app-dynamic-form` in order to create each separate form field:

```html
<!-- dynamic-form.component.html -->

...
<div *ngFor="let question of questions"
     class="form-row">
  <app-question [question]="question"
                [form]="form"></app-question>
</div>
...
```

# Make it iterable (repeatable)
As you can see above, `app-question` is wrapped inside an `ngFor` that loops over a collection of `questions`, which is nothing else than an array of `QuestionBase ` as demonstrated at the beginning of this article.

Inside this component, there's an `ngSwitch`. Its job is to display the right HTMLElement depending on the type of field given in the object:


```html
<!-- dynamic-form-question.component.html -->

<div [ngSwitch]="question.controlType">

    <input *ngSwitchCase="'textbox'"
            [formControl]="questionControl(index)"
            [placeholder]="question.placeholder"
            [attr.min]="question['min']"
            [attr.max]="question['max']"
            [attr.pattern]="question['pattern']"
            [id]="questionId(index)"
            [type]="question['type']">

    <select [id]="question.key"
            *ngSwitchCase="'dropdown'"
            [formControl]="questionControl(index)">
    <option value=""
            disabled
            *ngIf="!!question.placeholder"
            selected>{{ question.placeholder }}</option>
    <option *ngFor="let opt of question['options']"
            [value]="opt.key">{{ opt.value }}</option>
    </select>

    ...

</div>
```

You may have noticed the way we're passing attributes values like `[attr.min]="question['min']"` to elements with `options` attributes assigned in the `constructor`:

```typescript
// question-dropdown.ts

import { QuestionBase } from './question-base';

export class TextboxQuestion extends QuestionBase<string> {
  type: string;
  min: number | string;
  ...

  constructor(options: {} = {}) {
    super(options);
    this.type = options['type'] || 'text';
    this.min = options['min'];
    ...
}
```

But there's not only `FormControl`s to display, `FormArray`'s nice too! So let's go with some content projection:
 
```html
<!-- dynamic-form-question.component.html -->

<div *ngIf="question.iterable; else formTmpl">
    <div *ngFor="let field of questionArray.controls; 
                 let i=index; first as isFirst last as isLast">

        <ng-container [ngTemplateOutlet]="formTmpl"
                    [ngTemplateOutletContext]="{index: i}"></ng-container>

        <button *ngIf="question.iterable && questionArray.controls.length > 1"
                (click)="removeQuestion(i)"
                type="button">-</button>

        <button *ngIf="question.iterable && isLast"
                (click)="addQuestion()"
                type="button">+</button>

    </div>
</div>
```
You can see that this line `<div *ngIf="question.iterable; else formTmpl">` is the one who decides to display either a collection of `FormArray` or a simple `FormControl` so it is wrapped in an `ng-template`. I'm passing current index with `let-index="index"` given that this is the only way to know in which iteration step we are:

```html
<!-- dynamic-form-question.component.html -->
  ..
  <ng-template #formTmpl
               let-index="index">
    <label [attr.for]="questionId(index)">{{ questionLabel(index) }}</label>

    <div [ngSwitch]="question.controlType">
    ...
```

The challenge here is to keep the "link" with the right `question` element (the one we're iterating on) because with this configuration there will be `questions` in a `question`. Types and classes will remain the same at this point because the only way to determine if a `question` is iterable is to check the `iterable` property of the `question`.

Thanks to the `index` property injected with `<ng-template #formTmpl let-index="index">`, we can easily retrieve it in `ngTemplateOutletContext` with:

```html
<ng-container [ngTemplateOutlet]="formTmpl"
              [ngTemplateOutletContext]="{index: i}"></ng-container>
```
and do the job on the right iteration of the collection.


# Demo & code
All of the source code is available on Github and a demo is already available if you are just curious to see the awesomeness of the dynamic forms!

![A preview of the demo](https://thepracticaldev.s3.amazonaws.com/i/czo9qakt9pc1tjuokkug.png)

{% github maximelafarie/angular-dynamic-forms no-readme %}

<center><strong>🔥<a href="https://maximelafarie.com/angular-dynamic-forms" target="_blank">Demo available here</a>🔥</strong></center>



---
Credits

<sup><sub>Photo by [Patrick Langwallner](https://unsplash.com/@patresinger?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/search/photos/liberty-statue?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)</sub></sup>
<sup><sub>Many thanks to [@manekinekko](https://twitter.com/manekinekko) for rereading & correction</sub></sup>