I am trying to develop a template for custom Forms. The goal ist, that the Form gets build dynamically from a XML-File (this far I just create the Input-Elements in the App-Component for testing reasons). The Inputs are sometimes nested in different Groups.
Therefor I have a Form-Group Component and a generic Form-Element Component. I implemented the ControlValueAccessor and everything worked as a charm.
But now, as I want to implement different types of custom FormControls, I am trying to convert the Form-Element Component to a generic Wrapper-Component. The Form-Element Component then calls a Child-Component depending on the type of the given Form-Element Model provided by the XML (texture, checkbox, switch, etc.).
Therefor the ControlValueAccessor is now part of the Child-Component ("TextInputComponent").
But after implementing the Child-Component, this architecture doesn't works as expected. When I am adding a value to the Inputfield by typing into it (part of the Child-Component "TextInputComponent") I always get the ERROR TypeError: this.onChange is not a function.
I know, that the problem is, that the registerOnChange-Function is not called (-> so Angular is not parsing the components ControlValueAccessor as needed).
The funny part is, that the values of the fields in the FormGroup of Angular are still updating as they have to.
How can I get rid of the error?
Thank you for any help!
Form-Group Component - HTML (FormGroup):
<div class="row border">
<div class="col">
<p>{{formGroupModel.id}}</p>
<form [formGroup]="curFormGroup">
<ng-container *ngFor="let item of formGroupModel.Elements">
<hyc-formgroup *ngIf="utils.getInstanceType(item) === 'FormGroupModel'"
[parentFormGroup]="curFormGroup"
[formGroupModel]="item">
</hyc-formgroup>
<hyc-formelement *ngIf="utils.getInstanceType(item) === 'FormElementModel'"
[parentFormGroup]="curFormGroup"
[formElementModel]="item">
</hyc-formelement>
</ng-container>
</form>
</div>
</div>
Form-Group Component - TS:
import { Component, forwardRef, Input, OnInit } from "@angular/core";
import { ControlValueAccessor, FormArray, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from "@angular/forms";
import { Utils } from "src/app/UTILS/INSTANCE_TYPE";
import { FormElementModel } from "../form-element/form-element.model";
import { FormGroupModel } from "./form-group.model";
@Component({
selector: 'hyc-formgroup',
templateUrl: './form-group.component.html',
styleUrls: ['./form-group.component.css'],
})
export class FormGroupComponent implements OnInit {
@Input() public formGroupModel : any;
@Input() public parentFormGroup: FormGroup;
constructor (
public formBuilder : FormBuilder,
public utils : Utils
) {}
ngOnInit(): void {
this.initFormGroup();
}
initFormGroup() {
this.parentFormGroup.addControl(
this.formGroupModel.id, this.formBuilder.group({})
);
}
get curFormGroup() {
return this.parentFormGroup.get(this.formGroupModel.id) as FormGroup;
}
}
Form-Element Component - HTML (Wrapper-Element):
<ng-container *ngIf="formElementModel.type == 'text'">
<hyc-text-input [parentFormGroup]="parentFormGroup"
[formElementModel]="formElementModel">
</hyc-text-input>
</ng-container>
Form-Element Component - TS:
import { Component, Input } from "@angular/core";
import { FormGroup } from "@angular/forms";
@Component({
selector: 'hyc-formelement',
templateUrl: './form-element.component.html',
styleUrls: ['./form-element.component.css'],
})
export class FormElementComponent {
@Input() parentFormGroup : FormGroup;
@Input() formElementModel : any;
public value : any;
constructor() {}
}
TextInput-Component - HTML (Actual Form Control):
<input type="text"
class="form-control my-1"
placeholder="{{formElementModel.id}}"
[value]="value"
(input)="_onChange( $event )"
[formControl]="formControlObj"/>
TextInput-Component - TS:
import { Component, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Utils } from "src/app/UTILS/INSTANCE_TYPE";
@Component({
selector: 'hyc-text-input',
templateUrl: './text-input.component.html',
styleUrls: ['./text-input.component.css'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: TextInputComponent,
multi: true
}
]
})
export class TextInputComponent implements OnInit, ControlValueAccessor {
@Input() formElementModel;
@Input() parentFormGroup;
formControlObj;
public value : any;
public onChange: (value: any) => void;
constructor (
public formBuilder : FormBuilder,
public utils : Utils
) { }
ngOnInit(): void {
this.formElementModel.defaultValue ? this.value = this.formElementModel.defaultValue : this.value = null;
this.formControlObj = this.formBuilder.control( this.value );
this.parentFormGroup.addControl( this.formElementModel.id, this.formControlObj );
}
_onChange( event: Event) {
const value = (<HTMLInputElement>event.target).value;
this.onChange(value);
this.value = value;
}
writeValue(value: any): void {
if(!value || value == null) return
this.value = value;
}
registerOnChange(fn: (value : any) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState?(isDisabled: boolean): void {
}
}
Form with View of filled FormGroup-Element at the bottom
Maybe the Component where it all is nested could be also interesting for you:
FormPage-Component - HTML:
<div class="row">
<div class="col">
<div class="text-center">
<h1>{{formNavi.Titel}}</h1>
<h2> {{formNavi.CurPageObject.Name}}</h2>
<p> Mit * markierte Felder sind Pflicht</p>
</div>
<form [formGroup]="curPageFormGroup">
<ng-container *ngFor="let item of formNavi.CurPageObject.Elements">
<hyc-formgroup *ngIf="utils.getInstanceType(item) == 'FormGroupModel'"
[formGroupName]="item.id"
[parentFormGroup]="curPageFormGroup"
[formGroupModel]="item">
</hyc-formgroup>
<hyc-formelement *ngIf="utils.getInstanceType(item) == 'FormElementModel'"
[parentFormGroup]="curPageFormGroup"
[formElementModel]="item">
</hyc-formelement>
</ng-container>
</form>
</div>
</div>
<pre> {{wrapperFormGroup.value | json}} </pre>
<button (click)="onClickTest()">TEST</button>
<button (click)="formNavi.nextPage()">NEXT PAGE</button>
FormPage-Component - TS:
import { Component, Input, OnInit } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { Subscription } from "rxjs";
import { NavigationService } from "../services/navigation.service";
import { Utils } from "../UTILS/INSTANCE_TYPE";
@Component({
selector: "hyc-formpage",
templateUrl: "./form-page.component.html",
styleUrls: ["./form-page.component.css"]
})
export class FormPageComponent implements OnInit {
@Input() wrapperFormGroup : FormGroup;
curPageValue : Subscription;
constructor (
public formNavi : NavigationService,
public formBuilder : FormBuilder,
public utils : Utils
) { }
ngOnInit(): void {
}
get curPageFormGroup() {
return this.wrapperFormGroup.get(this.formNavi.CurPageObject.ID) as FormGroup;
}
}