Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Receiving "validators.map is not a function" error on my custom form control

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

like image 996
Optiq Avatar asked Feb 09 '26 22:02

Optiq


1 Answers

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:

FormGroupDirective

constructor(
      @Optional() @Self() @Inject(NG_VALIDATORS) private _validators: any[],
      @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators: any[]) {
    super();
  }

FormGroupName

  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:

parent.component.ts

// 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(''),
  /* ... */
  })
})

parent.component.html

<form [formGroup]="SvgFormData" (ngSubmit)="onSubmit()">

    <!-- this will be a child form group -->
    <view-box-form-component></view-box-form-component>

</form>

view-box-form-component.component.ts

@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:

view-box-form-component.component.html

<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.

like image 101
Andrei Gătej Avatar answered Feb 12 '26 16:02

Andrei Gătej



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!