I created a family of components to use as form controls with the ControlValueAccessor. When applying formControlName, formGroupName or formGroupArray to my components to pass in the form controls I get the error telling me
validators.map is not a function
This is how my component is set up
@Component({
selector: 'view-box-form-component',
templateUrl: './view-box-form.component.html',
styleUrls: ['./view-box-form.component.css'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(()=>ViewBoxFormComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(()=>ViewBoxFormComponent)
}
]
})
export class ViewBoxFormComponent implements OnInit, ControlValueAccessor {
ViewBoxFormData: FormGroup;
constructor() { }
ngOnInit() {}
public onTouched : ()=> void =()=>{};
writeValue(val: any):void{ val && this.ViewBoxFormData.setValue(val, {emitEvent: false}); console.log(val); }
registerOnChange(fn: any): void{ this.ViewBoxFormData.valueChanges.subscribe(fn); console.log(fn); }
registerOnTouched(fn: any): void{ this.onTouched = fn; }
setDisabledState?(isDisabled: boolean): void{ isDisabled ? this.ViewBoxFormData.disable() : this.ViewBoxFormData.enable(); }
validate(c: AbstractControl): ValidationErrors | null{
return this.ViewBoxFormData.valid ? null : {invalidForm:{valid: false, message: 'field invalid'}};
}
}
and this is the template for that component
<section [formGroup]="ViewBoxFormData" class="text-control-section">
<label class="control-label">
<p class="smallText"> x: </p>
<input type="text" class="text-control smallText" formControlName="x" />
</label>
<label class="control-label">
<p class="smallText"> y: </p>
<input type="y" class="text-control smallText" formControlName="y" />
</label>
<label class="control-label">
<p class="smallText"> width: </p>
<input type="text" class="text-control smallText" formControlName="width" />
</label>
<label class="control-label">
<p class="smallText"> height: </p>
<input type="text" class="text-control smallText" formControlName="height" />
</label>
</section>
Inside the template to the parent component I implement the component like this
<form [formGroup]="SvgFormData" (ngSubmit)="onSubmit()">
<view-box-form-component formGroupName="viewBox"></view-box-form-component>
</form>
I also have a couple other components I need to pass FormArrays into which I'm attempting to do with formArrayName="someControl". Instead of creating the form inside the components I generate it with functions based on JSON objects. The function for creating the form group for the view box data looks like this.
export function CreateViewBoxForm(data?: ViewBoxParameters): FormGroup{
return new FormGroup({
x : new FormControl((data ? data.x : ''), Validators.required),
y : new FormControl((data ? data.y : ''), Validators.required),
width : new FormControl((data ? data.width : ''), Validators.required),
height : new FormControl((data ? data.height : ''), Validators.required)
}) as FormGroup;
}
When I console.log() the completed form data everything is properly defined with all the proper values, I just can't figure out how to get them to pass into my components properly. Dos anybody see what I'm doing wrong or if there's something more that I need to do to get this to work properly?
UPDATE
I decided to add a stackblitz of the entire mechanism so far so you guys can get a better idea for what I need to accomplish with my component structure.
stackblitz demo
I think you'll have to add multi: true to your NG_VALIDATORS token.
Also, it looks like your ViewBoxFormComponent is not a simple FormControl, because inside your component you have another FormGroup instance. IMO, this is not idiomatically correct.
FormControlDirective, FormControlName, FormGroupName, FormArrayName, FormGroupDirective are form-control based directives and they, when combined, form an AbstractControlDirective tree(because each one inherits from AbstractControlDirective). These directives are the bridge between the the view layer(handled by ControlValueAccessor) and the model layer(handle by AbstractControl - the base class for FormControl, FormGroup, FormArray).
The root of such tree is FormGroupDirective(usually bound to a form element: <form [formGroup]="formGroupInstance">). With that in mind, your AbstractControl tree would look like this:
FG <- [formGroup]="SvgFormData"
|
FC <- formGroupName="viewBox"
/ | \
/ | \
FC FC FC (height, width, x etc...)
The FormControl should have no descendants. In this case, because your custom component(ViewBoxFormComponent) creates another tree. This is not wrong, but with this approach, you'll have to properly handle the both trees in order to not get unexpected results.
The FormGroup instance bound to the <section [formGroup]="ViewBoxFormData" class="text-control-section"> will not be registered as a descendant of SvgFormData because the FormGroupDirective does not behave like like FormGroupName:
constructor(
@Optional() @Self() @Inject(NG_VALIDATORS) private _validators: any[],
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators: any[]) {
super();
}
constructor(
@Optional() @Host() @SkipSelf() parent: ControlContainer, // `ControlContainer` - token for `FormGroupDirective`/`FormGroupName`/`FormArrayName`
@Optional() @Self() @Inject(NG_VALIDATORS) validators: any[],
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: any[]) {
super();
this._parent = parent;
this._validators = validators;
this._asyncValidators = asyncValidators;
}
If your custom component is not meant to contain a simple FormControl instance(it handles only a standalone FormControl instance with the help of formControl or ngModel directives), you should register it as descendant form group.
This can be achieved with this approach:
// The shape of the form is defined in one place!
// The children must only comply with the shape defined here
this.SvgFormData = this.fb.group({
ViewBoxFormData: this.fb.group({
height: this.fb.control(''),
width: this.fb.control(''),
/* ... */
})
})
<form [formGroup]="SvgFormData" (ngSubmit)="onSubmit()">
<!-- this will be a child form group -->
<view-box-form-component></view-box-form-component>
</form>
@Component({
selector: 'view-box-form-component',
templateUrl: './view-box-form.component.html',
styleUrls: ['./view-box-form.component.css'],
providers: [
{
provide: NG_VALIDATORS,
useExisting: forwardRef(()=>ViewBoxFormComponent),
multi: true,
}
],
viewProviders: [
{ provide: ControlContainer, useExisting: FormGroupDirective }
]
})
export class ViewBoxFormComponent implements OnInit {
constructor() { }
ngOnInit() {}
validate(c: AbstractControl): ValidationErrors | null{
/* ... */
}
}
As you can see, there is no need to provide CONTROL_VALUE_ACCESSOR anymore, because the form's shape will be defined in only one place(i.e: the parent form container). All you have to do in your child form container is to provide the right paths that correlate to the shape defined in the parent:
<section formGroupName="ViewBoxFormData" class="text-control-section">
<label class="control-label">
<p class="smallText"> x: </p>
<input type="text" class="text-control smallText" formControlName="x" />
</label>
<label class="control-label">
<p class="smallText"> y: </p>
<input type="y" class="text-control smallText" formControlName="y" />
</label>
<label class="control-label">
<p class="smallText"> width: </p>
<input type="text" class="text-control smallText" formControlName="width" />
</label>
<label class="control-label">
<p class="smallText"> height: </p>
<input type="text" class="text-control smallText" formControlName="height" />
</label>
</section>
What viewProviders does is to let you use a token(in this case ControlContainer) that is declared for the host element:
// FormGroupName
constructor (@Optional() @Host() @SkipSelf() parent: ControlContainer,) {}
Note: the same approach can be applied to FormArrayName.
You can find more about AbstractControl tree and how things work together in A thorough exploration of Angular Forms.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With