4

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!

3
  • Not clear to me are you asking about background form? Commented Apr 10, 2019 at 7:42
  • yes. if you look at jsons. you will see that the main-form (background form) is out of sync with others. Commented Apr 10, 2019 at 7:43
  • Have a look at posetd answer Commented Apr 10, 2019 at 8:13

3 Answers 3

11
+100

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

Sign up to request clarification or add additional context in comments.

16 Comments

Thanks for the answer! can I use Interface instead of the const? I don't want the form-array to be bound to main form. Or do you think I should have the const in different file?
hello, I tried to put the solution in my project. I try to load initial data that already exists. I followed the same code as yours. but I noticed when I initialize with data it doesn't transfer to children and when I try to add another routes it puts index in front of array objects. like this "routes": { "0": { "regionId": "" ... "municipalityId": "" } }. this might be the problem with initialization too. do you know what might be the solution?
@Vato, I updated the answer and the stackblitz, I hope this help. About use an Interface nor a const, I tryed, but with no result :(, but of course the interfaces and the constans can be outside the main-form
@Eliseo, thanks it works great. but it still adds index in front of arrays once you add address. it makes arrays { 0: {} 1: {} ... } etc. before you add the array looks fine [{ },{ },{ }]
to avoid ExpressionChanged... it's util sometimes use setTimeOut(()=>{...your code..})
|
0

I believe the issue here is that formArrayName is not an input for NG_VALUE_ACCESSOR/DefaultValueAccessor.

Also note:

Her examples are static parent->multiple children... meaning 1 to many and not dynamic. You are attempting static parent to many dynamic child->grandChild relationships built from formArray, then trying to dynamically link grandChild form to parent formArrayIndex that was passed through the child to the grandChild. Your stackblitz deviates from the structure she is teaching, and certainly introduces some new challenges not covered in the lecture.

Exploring how to iterate over the FormArray at the parent level and instantiate your child->grandchild relationship from within that loop may be a possible solution, this way you are not passing the entire array down, only the formGroup that would apply.

<h1>MAIN FORM</h1>
    {{ requestForm.value | json }}
    <div *ngFor="let route of requestForm.get('routes').controls">
        <app-cva-form-array formControl="route" (onChildFormValueChange)="onFormChange($event)"></app-cva-form-array>
    </div>

Selectors

  • input:not([type=checkbox])[formControlName]
  • textarea[formControlName]
  • input:not([type=checkbox])[formControl]
  • textarea[formControl]
  • input:not([type=checkbox])[ngModel]
  • textarea[ngModel]
  • [ngDefaultControl]

https://angular.io/api/forms/DefaultValueAccessor#selectors


Your only options for input are formControlName,formControl, ngModel and ngDefaultControl...

This is the reason formArrayName will not work in main-form <--> cva-form-array however, formControl will work for child-child to child level, as you are passing a singular formControl into your app-cva-form, from your app-cva-form-array via the *ngFor loop.

<mat-card *ngFor="let route of getForm.controls;
   <app-cva-form [formControl]="route" (blur)="onTouched()"></app-cva-form>

I believe the key to understand here is that formArray is only an organizational container for its children... it will not do what you are wanting it to in this scenario without help from additional logic.

There currently doesn't appear to be the necessary functionality to accept formArray as an input, iterate/dynamically manage the array, and link changes back to the parent formArray.

Comments

-1

You need to pass the updated form data from Child Component to Parent Component. I have used this.form.valueChanges() method to detect the changes and then emit the Form value to the parent component.

Parent Component:

HTML Code:

<app-cva-form-array formArrayName="routes" (onChildFormValueChange)="onFormChange($event)"></app-cva-form-array>

TS Code:

public onFormChange(form): void {
    this.requestForm = form;
}

Child Component:

HTML Code:

No change:)

TS Code:

@Output() onChildFormValueChange: EventEmitter<any> = new EventEmitter<any>();

registerEvent() {
    this.form.valueChanges.subscribe(() => {
      this.onFormValueChange()
    });
}

public onFormValueChange(): void {
    this.onChildFormValueChange.emit(this.form);
}

and call registerEvent method in the constructor like:

constructor(){
  this.registerEvent();
}

Working_Stackblitz

5 Comments

this gives only one way binding. although I know how to do 2 way, Its still not what I'm looking for. if you check how cva-form-array binds to cva-form, I want to achieve similar binding for main-form <--> cva-form-array. the goal of using cva is to simplify code and not use extra functions.
I added external resources about the origin of code at the bottom of question.
@Vato Nope it's two-way binding
If you change the main-form the cva-form-array isn't changing. if you write regionName: new FormControl('asdfa'), in main-form it leaves children component regionnames null.
stackblitz.com/edit/angular-b6nm8c here if you add array from parent it isn't bount to child only vice versa. but anyways, as I said none of the functions should be required using cva.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.