Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

nested custom FormArray component doesn't bind with child form with FormArrayName

I tried to have 2 nested forms using CVA. the problem is the second from isn't initialized with data when I bind it to a formControl.

Stackblitz

enter image description here

I have MAIN-FORM:

this.requestForm = this.fb.group({
  garageId: 0,
  routes: new FormArray([
    new FormGroup({
      addressPointId: new FormControl,
      municipalityId: new FormControl,
      regionId: new FormControl,
      rvId: new FormControl,
      sequenceNumber: new FormControl,
      settlementId: new FormControl,
      regionName: new FormControl,
      municipalityName: new FormControl,
      settlementName: new FormControl,
      description: new FormControl,
    })
  ]),
  endDateTime: 0,
});

In main-form html I bind routes to with formArrayName.

 <app-cva-form-array formArrayName="routes"></app-cva-form-array>

Component CVA-FORM-ARRAY has.

form = new FormArray([
new FormGroup({
  addressPointId: new FormControl,
  municipalityId: new FormControl,
  regionId: new FormControl,
  rvId: new FormControl,
  sequenceNumber: new FormControl,
  settlementId: new FormControl,
  regionName: new FormControl,
  municipalityName: new FormControl,
  settlementName: new FormControl,
  description: new FormControl,
})
]);

Everything from here works just fine. I bind each formGroup in the array to child component CVA-FORM.

<app-cva-form [formControl]="route" (blur)="onTouched()"></app-cva-form>

CVA-FORM for each formGroup I created separate component in case I want to use component itself not the whole array.

  form: FormGroup = new FormGroup({
    regionName: new FormControl,
    regionId: new FormControl,
    municipalityName: new FormControl,
    municipalityId: new FormControl,
    sequenceNumber: new FormControl,
    settlementName: new FormControl,
    settlementId: new FormControl,
    addressPointId: new FormControl,
    description: new FormControl,
    rvId: new FormControl,
  });

the main-form <--to--> app-cva-form-array binding doesn't work for some reason.

The idea of these forms comes from kara's talk on angulaconnect. here are her slides.

help plz!

like image 935
Vato Avatar asked Oct 24 '25 10:10

Vato


1 Answers

When you use "custom Form Control", you need take account that you feed the cursom Form Control with a Form Control (not FormArray, not FormGroup). The FormControl has as value an array or an object, but you need not confussed about this.(*)

You can see in work in stackblitz

That's your form is like

//in main.form
this.requestForm = new FormGroup({
  garageId: new FormControl(0),
  routes: new FormControl(routes), //<--routes will be an array of object
  endDateTime: new FormControl(0)
})

//in cva-form-array
this.form=new FormArray([new FormControl(...)]); //<-this.form is a 
                             //formArray of FormControls NOT of formGroup

//finally in your cva-form
this.form=new FormGroup({});
this.form=formGroup({
      addressPointId: new FormControl(),
      municipalityId: new FormControl(),
      ...
})

I've create a const to export to simply the code. MY const expor is

export const dataI = {
  addressPointId: "",
  municipalityId: "",
  regionId: "",
  rvId: "",
  sequenceNumber: "",
  settlementId: "",
  regionName: "",
  municipalityName: "",
  settlementName: "",
  description: "",
}

So, in mainForm we have

  ngOnInit() {
    let routes:any[]=[];
    routes.push({...dataI});
    this.requestForm = new FormGroup({
      garageId: new FormControl(0),
      routes: new FormControl(routes),
      endDateTime: new FormControl(0)
    })
  }
<mat-card [formGroup]="requestForm" style="background: #8E8D8A">
    <app-cva-form-array formControlName="routes"></app-cva-form-array>
</mat-card>

In cvc-form array create the formArray when we give value

  writeValue(v: any) {
    this.form=new FormArray([]);
    for (let value of v)
        this.form.push(new FormControl(value))

    this.form.valueChanges.subscribe(res=>
    {
      if (this.onChange)
        this.onChange(this.form.value)
    })
  }

    <form [formGroup]="form" >
        <mat-card *ngFor="let route of form.controls; 
            let routeIndex = index; let routeLast = last;">
           <button (click)="deleteRoute(routeIndex)">
             cancel
           </button>
           <app-cva-form [formControl]="route" (blur)="onTouched()"></app-cva-form>
      </form>

Finally, the cva-form

  writeValue(v: any) {
    this.form=new FormGroup({});
    Object.keys(dataI).forEach(x=>{
      this.form.addControl(x,new FormControl())
    })

    this.form.setValue(v, { emitEvent: false });
    this.form.valueChanges.subscribe(res=>{
       if (this.onChanged)
        this.onChanged(this.form.value)
    })
  }

<div [formGroup]="form">
  <mat-form-field class="locationDate">
    <input formControlName="regionName">
    <mat-autocomplete #region="matAutocomplete" 
      (optionSelected)="selectedLocation($event)">
      <mat-option *ngFor="let region of regions" 
      [value]="region">
        {{region.regionName}}
      </mat-option>
    </mat-autocomplete>
  </mat-form-field>
  <mat-form-field class="locationDate">
    <input formControlName="municipalityName" 
      [matAutocomplete]="municipality"
      (blur)="onTouched()"
      [readonly]="checked || this.form.value.regionId < 1">
   ....
   </form>

(*) Yes, we are used to seeing that a FormControl has as a value a string or a number, but no one forbids us that the value is an object or an array (for example, ng-bootstrap DatePicker stores an object {year: .. month: .., day ..}, mat-multiselect stores an array, ...)

Update Of course we can feed our control with data from a service or similar. The only thing we must take account is how we give the data. As usually I like make a function that received a data or null and return a FormControl

  getForm(data: any): FormGroup {
    data = data || {} as IData;
    return new FormGroup({
      garageId: new FormControl(data.garageId),
      routes: new FormControl(data.routes),
      endDateTime: new FormControl(data.endDateTime)
    })
  }

where IData is an interface

export interface IData {
  garageId: number;
  routes: IDetail[];
  endDateTime: any
}

and IDetail another interface

export interface IDetail {
  addressPointId: string;
  ...
  description: string;
}

Then we can have a complex data like (sorry for the large object)

let data = {
  garageId: 1,
  routes: [{
    addressPointId: "adress",
    municipalityId: "municipallyty",
    regionId: "regionId",
    rvId: "rvId",
    sequenceNumber: "sequenceNumber",
    settlementId: "settlementId",
    regionName: "regionName",
    municipalityName: "municipalityName",
    settlementName: "settlementName",
    description: "description",
  },
  {
    addressPointId: "another adress",
    municipalityId: "another municipallyty",
    regionId: "another regionId",
    rvId: "another rvId",
    sequenceNumber: "another sequenceNumber",
    settlementId: "another settlementId",
    regionName: "another regionName",
    municipalityName: "another municipalityName",
    settlementName: "another settlementName",
    description: "another description",
  }],
  endDateTime: new Date()
}

Then only need make

this.requestForm = this.getForm(data);

The stackblitz if updated

like image 175
Eliseo Avatar answered Oct 26 '25 01:10

Eliseo