How to Create a Dynamic Reactive Form from Database Fields

Good Day,
I am trying to create a dynamic reactive form, but I am struggling.
The concept is that the fields info will later be retrieved from a database (for now hardcoded for simplicity), and then the form is built based on this array.

My setup is as follows:

dynamicform.module.ts

export class DynamicForm {
    constructor(
        public fieldname: string,
        public fielddescription: string,
        public fieldtype: string,
        public required: string
    ) {}
}

fetch-form.service.ts

...
...
export class FetchFormService {

  constructor() { }
  private _dynamicForm: DynamicForm[] = [
    {
      fieldname: 'fieldname_1',
      fielddescription: 'Field Desc 1',
      fieldtype: 'text',
      required: '1'
    },
    {
      fieldname: 'fieldname_2',
      fielddescription: 'Field Desc 2',
      fieldtype: 'text',
      required: '1'
    },
    {
      fieldname: 'fieldname_3',
      fielddescription: 'Field Desc 3',
      fieldtype: 'text',
      required: '1'
    }
  ];

  get dynForm() {
    return [...this._dynamicForm];
  }
}

dynamic-form.page.ts

...
...
...

export class DynamicFormPage implements OnInit {
  loadedForm: DynamicForm[];
  dynamicForm: FormGroup;

...
...
...

then the init function:

ngOnInit() {

    this.loadedForm = this.fetchService.dynForm;

    this.dynamicForm = new FormGroup({
      fieldname_1: new FormControl(null, {
        updateOn: 'blur',
        validators: [Validators.required]
      }),
      fieldname_2: new FormControl(null, {
        updateOn: 'blur',
        validators: [Validators.required]
      }),
      fieldname_3: new FormControl(null, {
        updateOn: 'blur',
        validators: [Validators.required]
      })
    });
  }

and lastly I have the
dynamic-form.page.html

<ion-header>
  <ion-toolbar>
    <ion-title>Dynamic Form</ion-title>
    <ion-buttons slot="primary">
      <ion-button (click)="submitDynamicForm()">
        <ion-icon name="checkmark" slot="icon-only"></ion-icon>
      </ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

<ion-content>
  <form [formGroup]="dynamicForm">
    <ion-grid>
      <ion-row>
        <ion-col size-sm="6" offset-sm="3">
          <ion-item>
            <ion-label>
              DYNAMIC FORM
            </ion-label>
          </ion-item>
        </ion-col>
      </ion-row>

      <div *ngFor="let formItem of loadedForm">
        <ion-row>
          <ion-col size-sm="6" offset-sm="3">
            <ion-item lines="none">
              <ion-label position="floating">
                {{formItem.fielddescription}}
              </ion-label>
              <ion-input 
                type="{{formItem.fieldtype}}"
                formControlName="{{formItem.fieldname}}"
                autocomplete
                autocorrect
              ></ion-input>
            </ion-item>
          </ion-col>
        </ion-row>
      </div>
    </ion-grid>
  </form>

</ion-content>

the result:

then to note, currently the array is hardcoded in the fetch-form.service file, but in time it will be fetched with HTTP. Hardcoded now for simplicity.

ok so a couple of questions:

  1. Is this the correct way of building a dynamic form?
  2. How do I dynamically set the ‘fieldname’ in the FormGroup?
    image

Would anyone be so kind as to provide me with some guidance and examples?

Thank you in advance!

Kind Regards

Even so, it is crucial that this knowledge remains on a strict need-to-know basis, by which I mean that get dynForm has written a check that it won’t be able to cash in the future. Leaving aside my opinion that magical get/set functions in JavaScript are a misfeature to begin with, it can’t be synchronous even now, so I would change it to:

formFields(): Observable<DynamicFormField[]> {
  return of(this._dynamicForm);
}

DynamicForm is renamed simply for documentation purposes: its name implies it’s a whole form, when really it’s just a single field. Now you can swap out for fetching from HTTP without having to change any code outside of FetchFormService; the way it’s written now client code is going to have to change in the future.

Is this the correct way of building a dynamic form?

Can’t speak to “correct”, but the fundamental idea is how I would do it, with the caveat that I wouldn’t be surprised if the whole thing ends up being more of a PITA than it’s worth.

How do I dynamically set the ‘fieldname’ in the FormGroup?

this.formService.formFields().pipe(
  map(fields => {
    let ctrls: { [name: string]: FormControl } = {};
    fields.forEach(field => {
      ctrls[field.fieldname] = new FormControl(...);
    });
    return new FormGroup(ctrls);
  })
  .subscribe(fg => this.dynamicForm = fg);

Hi,

Thank you, I have now changed it as per your suggestion.

However, I am not fully understanding the code for the fieldnames:

I have adjusted as follows:

this.fetchService.formFields().pipe(
      map(fields => {
        let ctrls: { [name: string]: FormControl } = {};
        fields.forEach(field => {
          ctrls[field.fieldname] = new FormControl(null, {
                updateOn: 'blur',
                validators: [Validators.required]
              });
        });
        return new FormGroup(ctrls);
      })
      .subscribe(fg => this.dynamicForm = fg);
  }

I have placed this now inside the ngOnInit() of the dynamic-form.page.ts file.

Is this correct?

What I do not fully understand though is that first, it gives me an error on the forEach section as well as subscribe. What am I missing here?

and secondly in the old code in this function, I set

this.loadedForm = this.fetchService.dynForm;

and then used this in the dynamic-form.page.html file to loop:

*ngFor="let formItem of loadedForm"

Now that it is replaced by the code provided, how will I reference from the HTML?

I apologize for the noob questions… struggling to get my head around all this :frowning:

Thank you for the help!

Perhaps the return value type of formFields()? It needs to be declared to return a Observable<DynamicFormField[]>.

…and this is partly why I suggested that you not be surprised if this whole journey ended up being more trouble than it’s worth. You could loop through Object.keys(this.loadedForm.controls), but the order isn’t guaranteed, so you’re going to have to somehow keep a separate array storing (at least) the names of each of the fields, and then do something like:

<ion-item *ngFor="let fn of fieldNames">
  <ion-input [formControlName]="fn">
</ion-item>

I tried something like this once, and ended up deciding that all I had ended up achieving is shifting from writing Angular templates to writing JSON, and creating a few hard-to-read classes that didn’t actually end up being as reusable as I thought they were going to be. So I decided that unless there is an existing external DSL, I was just going to write the forms out as ordinary Angular templates.

On the concept side then of this discussion, I would seem that this is quite a big restriction or limitation of angular / ionic. Where l want to use this is to have a mobile app for an existing Php process that runs and works like this. It gets field info from the DB and then creates the user forms based on that. @rapropos I value your opinion and if you had to accomplish the same ie a mobile app form where the fields are completely populated by DB entries, what route or option or alternative would you suggest I take?

If I had to, I would take the approach we’re discussing here. However, what happened next for me, after we got to the point we are now, the client decided that it was also important to dictate the layout of the form fields: which ones were how wide, which got put together on multiple rows, then validation, and it gradually became clear that we were basically reinventing the framework, so I convinced the client that they would get more maintainable results more accurately tailored to their specifications if we just wrote the apps normally.

The situation where this sort of strategy does IMHO make sense is if there is a natural DSLesque way of representing the form, such as a questionnaire with prompts and answers, where the layout can be somewhat constrained. But when the representation is as generic as you seem to be describing here, well all I can say is that it didn’t go so well when I tried it.