问题
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
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!
回答1:
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
回答2:
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 staticparent
to many dynamicchild->grandChild
relationships built fromformArray
, then trying to dynamically linkgrandChild
form to parentformArrayIndex
that was passed through thechild
to thegrandChild
. 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 inmain-form <--> cva-form-array
however,formControl
will work forchild-child to child level
, as you are passing a singularformControl
into yourapp-cva-form
, from yourapp-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 parentformArray
.
回答3:
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
来源:https://stackoverflow.com/questions/55574410/nested-custom-formarray-component-doesnt-bind-with-child-form-with-formarraynam