How to bind model to formbuilder using reactive forms in angular8

Viewed 3198

i have used reactive forms in my application and binding to html using formcontrolName. But now instead of declaring the properties inside the formbuilder, i need to call a model and assign that model to the formbuilder and use it in html. I been through many links but didnt find anywhere how to work based on my requirement.

HTML:

<div class="tab-pane fade h-100 active show" id="tab-user_1" role="tabpanel" aria-labelledby="user_1"
    *ngIf="userInfoForm" [formGroup]="userInfoForm">
    <div class="row">
      <div class="col-6">
        <div class="form-group">
          <label for="">Agent Code <span class="text-danger">*</span></label>
          <input type="text" formControlName="agentCode" class="form-control">
        </div>
      </div>
</div>

TS:

 this.userInfoForm = this.FB.group({
        Id: 0,
        agentCode: this.agentCode,
        userName: [''],
        firstName: [''],
        middleName: '',
        lastName: [''],
        department: [''],
      });
      this.userInfoForm.controls['agentCode'].disable();

model.ts:

export class UserFormDetails {
   Id: number;
   agentCode: number;
   userName: string;
   firstName: string;
   middleName: string;
   lastName: string;
   department: string;
}

DEMO

3 Answers

based on the comments, it sounds like you want a dynamic form based on a model... there is no "out of the box" solution to do this. You can write your own form model:

interface MyFormModelField {
  initial?: {
    value: any,
    disabled: boolean
  },
  controlOptions?: AbstractControlOptions
  // anything you find useful really?
}

export type MyFormModel<T> = Record<keyof T, MyFormModelField>

then implement it like so:

const USER_FORM_MODEL: MyFormModel<UserFormDetails> = {
    Id: { },
    agentCode: {
        initial: {
          value: '',
          disabled: true
        },
        controlOptions: {
          validators: Validators.required
        }
    },
    userName: { },
    firstName: { },
    middleName: { },
    lastName: { },
    department: { },
}

then you could write a service for producing form groups based on an instance of the form model...

@Injectable({providedIn: 'root'})
export class MyFormBuilder {
  constructor(private: fb) { }

  buildForm<T>(formModel: MyFormModel<T>) {
    const fg = Object.keys(formModel).reduce((grp, k) => {
      const fld = formModel[k];
      return Object.assign(grp, {
        [k]: [fld.initial || '', fld.controlOptions]
      })
    }, {});
    return this.fb.group(fg);
  }
}

now, anytime you make changes to the UserFormDetails interface, typescript will yell at you to update the corresponding form model with the field information... and while updates won't flow "automatically" from a model update, you at least have some type safety in your forms. I'd personally stop here.

This next part isn't perfect, and I'm not a fan of it, but if you really want the next step to have it be more or less automatic from the class definition, you could write a function to build a default form model from a class, like this:

function buildDefaultFormModel<T>(model: new () => T) {
  return Object.keys(new model()).reduce((form, k) => {
      return Object.assign(form, {[k]: {}})
  }, {} as MyFormModel<T>)
}

this requires 2 things, 1, the class your building a form for MUST have a default specified for each field, fields without defaults wont't be included. (This introduces a hidden dependency on fields having default values, which is why I don't like this method, however, you CAN configure ts to yell at you if you write a class that has properties without defaults or constructor assignments):

class UserFormDetails {
   Id: number = 0;
   agentCode: number = 0;
   userName: string = '';
   firstName: string = '';
   middleName: string = '';
   lastName: string = '';
   department: string = '';
}

2, the object constructor must take no parameters. but TS will yell at you if you try to misuse it in this case.

the function could be used like this:

const USER_FORM_MODEL: MyFormModel<UserFormDetails> = {
  ...buildDefaultFormModel(UserFormDetails),
  ...{
    agentCode: {
      initial: {
        value: '',
        disabled: true
      },
      controlOptions: {
        validators: Validators.required
      }
    },
  }
}

this way you can use default fields, and also provide an override for specific fields.

finally, you could also leverage this form model to build a template, but that's a bigger task.

here's a blitz with this all implemented (and I went ahead and cleaned up the controller code): https://stackblitz.com/edit/github-ck3u8u-qtnasc?file=src%2Fapp%2Fuser-table%2Fuser-table.validators.ts

You have to use patchValue to bind you model in the FormGroup form control.

this.userInfoForm = this.FB.group({
        Id: 0,
        agentCode: this.agentCode,
        userName: [''],
        firstName: [''],
        middleName: '',
        lastName: [''],
        department: [''],
      });

this is correct. When you get UserFormDetails object you have to pass it to form you have to do :

 this.userInfoForm.patchValue({
   'Id': UserFormDetails.id 
    ...
})

and when you had to pass the form controls value to object:

 this.UserFormDetail.id = this.userInfoForm.get('Id').value

etc...

I think this might be the solution you are looking for:

private initUserForm() {
   // mapping the object into the form model
   this.userInfoForm = this.FB.group({ ...this.userDetails });

   // if you want to add validators or disable buttons...
   this.userInfoForm.controls.agentCode.setValidators(Validators.required); // adding validations
   this.userInfoForm.controls.agentCode.disable();
   // console.log(this.userInfoForm.controls);
}

This way covers your need of "binding" which is not a real binding as the framework doesn't allow such thing, but it allows you to map from the object model/values to create the reactive form model.


See how after adding an extraField, the forms pick up that control and its value:

enter image description here

Related