How can I generate FormControls dynamically inside a FormGroup?

Viewed 125

I am receiving an object from the db that contains a few elements with the following format:

info = [{
    idcentro: "8227",
    namecentro: "Centro Paris", 
    address: "C/ Paris, 127",
    dias: [
      {
        dia: "0",
        horafinal: "06:00",
        horainicio: "17:00",
        salas: [
          {
            id: 0,
            nombre: "sala 1",
            intervalos: [
                ["09:00", "12:30", "10"],
                ["13:00", "21:30", "20"]
            ]
          }
        ]
      },
      {
        dia: "1",
        horafinal: "09:00",
        horainicio: "16:30",
        salas: [
          {
            id: 0,
            nombre: "sala 1",
            intervalos: [
                ["09:00", "12:30", "10"]
            ]
          },
          {
            id: 1,
            nombre: "sala 2",
            intervalos: [
                ["09:00", "20:30", "20"]
            ]
          }
        ]
      },
      {
        dia: "2",
        horafinal: "09:00",
        horainicio: "20:30",
        salas: [
          {
            id: 0,
            nombre: "sala 1",
            intervalos: [
                ["09:00", "12:30", "10"],
                ["12:45", "18:30", "15"]
            ]
          },
          {
            id: 1,
            nombre: "sala 2",
            intervalos: [
                ["09:00", "20:30", "20"]
            ]
          }
        ]
      }
    ]
  }];

To work with those "dias" array and include inputs to modify each value in case the user wants to, I've manually created a FormGroup containing a formControl for each of those days (the real object contains up to 8 days).

    this.openingHoursForm = new FormGroup({
      day0OpeningTime: new FormControl(""),
      day0ClosingTime: new FormControl(""),
      day1OpeningTime: new FormControl(""),
      day1ClosingTime: new FormControl(""),
      day2OpeningTime: new FormControl(""),
      day2ClosingTime: new FormControl(""),
      day3OpeningTime: new FormControl(""),
      day3ClosingTime: new FormControl(""),
      day4OpeningTime: new FormControl(""),
      day4ClosingTime: new FormControl(""),
      day5OpeningTime: new FormControl(""),
      day5ClosingTime: new FormControl(""),
      day6OpeningTime: new FormControl(""),
      day6ClosingTime: new FormControl(""),
      day7OpeningTime: new FormControl(""),
      day7ClosingTime: new FormControl(""),
      day8OpeningTime: new FormControl(""),
      day8ClosingTime: new FormControl("")
    });

And then, in the HTML, I am creating two inputs for each of those those formControlNames:

<div class="dayOpeningHours" title="">
  <div>Day 0</div>
   <div class="opens">
     <label for="">De:
       <input class="form-control" type="time" formControlName="day0OpeningTime" name="day0OpeningTime">
     </label>
   </div>
   <div class="closes">
     <label for="">A:
       <input class="form-control" formControlName="day0ClosingTime" name="day0ClosingTime">
     </label>
   </div>
</div>

As you can see, this is becoming very unmaintainable. I have tried to create the markup (in the html) and formControlNames (in the ts) dynamically by looping through each "dia" of the 'dias' array, but I haven't achieved anything.

Is there a way I can generate the HTML and the declaration of those Form Controls inside the same formGroup? I have been reading about FormArray and AbstractControl but I can not really understand how to apply that to my code.

Can someone help me out or give me some tips to find the right documentation?

Thank you

2 Answers

The better option would be to use a FormArray instead. You can also inject FormBuilder from @angular/forms

See Below

constructor (private fb: FormBuilder) { }
diasForm = this.fb.group({
  openingHours: this.fb.array([])
})
get openingHours (): FormArray {
  return this.diasForm.get('openingHours') as FormArray;
}

The next task will be to create the form Lets assume we have dias as an array

dias = [
      {
        dia: "0",
        horafinal: "06:00",
        horainicio: "17:00",
      },
      {
        dia: "1",
        horafinal: "06:00",
        horainicio: "17:00",
      },
      ...
]

I have removed other properties for simplicity In the ngOnInit we can set the value of our form

ngOnInit() {
  this.dias.forEach(dia => {
    this.openingHours.push(
      this.fb.group({
        horafinal: [dia.horafinal, []],
        horainicio: [dia.horainicio, []]
      })
    )
  })
}

In the Html

<form [formGroup]='diasForm'>
  <div formArrayName='openingHours'>
    <div *ngFor='let item of openingHours.controls; let i = index' 
       [formGroupName]='i'>
         <input formControlName='horafinal' />
         <input formControlName='horainicio' />
    </div>
  </div>
</form>

The above should match the structure of our formGroup

diasForm( [formGroup] ) ---> openingHours(formArray) ---> i ( [formGroup] ) ---> horafinal(formControl)

Please see this demo on stackblitz

This way you can have a dynamic number of days

it's a large an complex object, but the "way" to control a json object is always the same: the properties simples are FormControl, the arrays are FormArrays and we use FormGroup to group controls.

So carefully, make four functions to create the form

  createForm(data: any = null) {
    data = data || {idcentro: null,namecentro: null,address: null,dias: null};
    return new FormGroup({
      idcentro: new FormControl(data.idcentro),
      namecentro: new FormControl(data.namecentro),
      address: new FormControl(data.address),
      dias: new FormArray(
        data.dias ? data.dias.map(x => this.createFormDias(x)) : []
      )
    });
  }
  createFormDias(data: any = null) {
    data = data || {dia: null,horafinal: null,horainicio: null,salas: null};
    return new FormGroup({
      dia: new FormControl(data.dia),
      horafinal: new FormControl(data.horafinal),
      horainicio: new FormControl(data.horainicio),
      salas: new FormArray(
        data.salas ? data.salas.map(x => this.createFormSalas(x)) : []
      )
    });
  }
createFormSalas(data: any = null) {
    data = data || { id: null, nombre: null, intervalos: null };
    return new FormGroup({
      id: new FormControl(data.id),
      nombre: new FormControl(data.nombre),
      intervalos:new FormArray(
        data.intervalos?this.createFormIntervalos(data.intervalos):[])
    });
  }
  createFormIntervalos(data:any=null)
  {
    return data.map(x=>new FormArray(x.map(y=>new FormControl(y))))
  }

And four functions more to get the formArrays

  get diasArray()
  {
    return this.form.get('dias') as FormArray
  }
  getSalas(index)
  {
    return this.diasArray.at(index).get('salas') as FormArray
  }
  getIntervalos(index1,index2)
  {
    return this.getSalas(index1).at(index2).get('intervalos') as FormArray
  }
  getHoras(index1,index2,index3)
  {
    return this.getIntervalos(index1,index2).at(index3) as FormArray
  }

To control in .html you need take account when a FormArray is a FormArray of FormGroups and when is a FormArray of FormControls

<form [formGroup]="form">
  <div>idcentro:<input formControlName="idcentro" /></div>
  <div>namecentro:<input formControlName="namecentro" /></div>
  <div>address:<input formControlName="address" /></div>
  <div formArrayName="dias">
    <div
      *ngFor="let grDias of diasArray.controls;let i=index"
      [formGroupName]="i"
    >
      <div>dia:<input formControlName="dia" /></div>
      <div>horafinal:<input formControlName="horafinal" /></div>
      <div>horainicio:<input formControlName="horainicio" /></div>
      <div formArrayName="salas">
        <div
          *ngFor="let grSalas of getSalas(i).controls;let j=index"
          [formGroupName]="j"
        >
          <div>id:<input formControlName="id" /></div>
          <div>nombre:<input formControlName="nombre" /></div>
          <div formArrayName="intervalos">
            <div
              *ngFor="let grIntervalos of getIntervalos(i,j).controls;let k=index"
            >
              <div [formArrayName]="k">
                <div *ngFor="let grHoras of getHoras(i,j,k).controls;let m=index">
                  <input [formControlName]="m">
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</form>

The ugly stackblitz

See that a FormArray of FormGroup we use

<div formGroupName="nameOfArray">
  <div *ngFor="let group of formArray.controls;let i=index" [formGroupName]="i">
     ..here..
     <input formControlName="controlName">
  </div>
</div>

And a FormGroup of FormControls

<div formGroupName="nameOfArray">
  <div *ngFor="let group of formArray.controls;let i=index" >
     <input [formControlName]="i">
  </div>
</div>
Related