17

There is a custom input component and it is used in a reactive form with validation:

@Component({
    moduleId: module.id.toString(),
    selector: 'custom-select',
    templateUrl: 'custom-select.component.html',
    styleUrls: ['custom-select.component.css']
})
export class CustomSelectComponent {
    @Input() public items: SelectModel[];
    public model: SelectModel;
    constructor(private customSelectService: CustomSelectService) {
        this.customSelectService.Selected.subscribe((data: SelectModel) => {
            this.model = data;
        });
    }
    public newSelect(select: SelectModel): void {
        this.customSelectService.updateSelected(select);
    }
}

which works fine, I am using custom-select in a reactive form and want to validate it like below:

<custom-select id="country" [items]="selectItems" formControlName="country"></custom-select>
<div *ngIf=" myFrom.controls['country'].invalid && (myFrom.controls['country'].dirty 
             || myFrom.controls['country'].touched) " class="ha-control-alert">
    <div *ngIf="myFrom.controls['country'].hasError('required')">Country is required</div>
</div>

this is how I declare the form in component

this.myFrom = this.formBuilder.group({
    country: [null, Validators.required],
})

but when I add formControlName for validations, it gets error which says No value accessor for form control with name: 'country'. How should I handle this?

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
Paridokht
  • 1,374
  • 6
  • 20
  • 45
  • `value accessor` need to be implemented in custom inputs. can you post `custom-select` – Rajez Sep 10 '17 at 14:23
  • 5
    You need to implement [`ControlValueAccessor`](https://angular.io/api/forms/ControlValueAccessor), then it will work with both kinds of form. @Rajez that's already in the post – jonrsharpe Sep 10 '17 at 14:25
  • @Rajez I've already posted, bad formatting maybe – Paridokht Sep 10 '17 at 14:26
  • @jonrsharpe how should I implement it???, I mean what should be implemented in its methods?? – Paridokht Sep 10 '17 at 14:27
  • 2
    Please read the documentation I already linked to, which tells you which methods to implement and provides example implementations. – jonrsharpe Sep 10 '17 at 14:28
  • 8
    https://blog.thoughtram.io/angular/2016/07/27/custom-form-controls-in-angular-2.html it shows how to create custom form controls – Rajez Sep 10 '17 at 14:31
  • @jonrsharpe Am I missing something in that Angular docs link? I don't see any examples. I have this problem with most of the Angular docs... nonexistent examples and lack of real, descriptive help. Basically it's just a dump of the intellisense my editor gives and nothing else :( Am I doing something wrong? Rajez's last link was perfect (thanks!). – pbristow Jul 09 '18 at 21:52

2 Answers2

5

STEPS

  1. Add the provider for NG_VALUE_ACCESSOR in the decorator
  2. Implement the ControlValueAccessor in the class
  3. Create the onChange function like this onChange = (value: boolean) => {};
  4. Add the registerOnChange, writeValue and registerOnTouched methods of the ControlValueAccessor interface
  5. In the method that will be changed the value of select, call the onChange function passing as parameter the value of select.
        import ...
        import { Component, forwardRef } from '@angular/core';
        import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
        
    
        @Component({
            moduleId: module.id.toString(),
            selector: 'custom-select',
            templateUrl: 'custom-select.component.html',
            styleUrls: ['custom-select.component.css'],
            // STEP 1
            providers: [{
              provide: NG_VALUE_ACCESSOR,
              multi: true,
              useExisting: forwardRef(() => CustomSelectComponent)
            }]
        })
        // STEP 2
        export class CustomSelectComponent implements ControlValueAccessor {
            // STEP 3
            onChange = (value: SelectModel) => {};
            @Input() public items: SelectModel[];
            public model: SelectModel;
            constructor(private customSelectService: CustomSelectService) {
                this.customSelectService.Selected.subscribe((data: SelectModel) => {
                    this.model = data;
                });
            }
            public newSelect(select: SelectModel): void {
                // STEP 5
                this.onChange(select);
                this.customSelectService.updateSelected(select);
            }
            // STEP 4
            registerOnChange(fn: (value: SelectModel) => void): void {
                this.onChange = fn;
            }
            writeValue() {}
            registerOnTouched(){}
        }

Don't forget to add the formControlName in the selector.

Quethzel Diaz
  • 621
  • 1
  • 11
  • 26
2

Add this to the providers of the child:

viewProviders: [{
  provide: ControlContainer, 
  useExisting: FormGroupDirective 
}]

The reason it fails in the first place is it doesn't look in the parent scope for FormGroup, so this just passes it down to the child.

Then you need to trick it into thinking your control has a valueAccessor - which it doesn't need because you just created a sort of 'shim'.

export const NOOP_VALUE_ACCESSOR: ControlValueAccessor = 
{
    writeValue(): void {},
    registerOnChange(): void {},
    registerOnTouched(): void {}
};

Then in the constructor for your child:

constructor(@Self() @Optional() public ngControl: NgControl) 
{
    if (this.ngControl)
    {
        this.ngControl.valueAccessor = NOOP_VALUE_ACCESSOR;
    }
}

Then you can use the formControlName as normal inside the child.

If you need to pass the formControlName from the parent component (which you probably would if it was meant to be a reusable control) just create a string @Input property and do this in the child:

    <input matInput [placeholder]="desc" 
           [formControlName]="formControlName">
Simon_Weaver
  • 140,023
  • 84
  • 646
  • 689
  • Thanks to https://stackoverflow.com/a/65177817/16940 for the NOOP portion of this answer. – Simon_Weaver Jul 12 '21 at 23:31
  • Hi @Simon_Weaver. Does this code work if "matInput" is put inside a nested formGroup of the main form? I tried this code and it doesn't seems to work in that case – Matt Apr 08 '22 at 06:02
  • @Simon_Weaver is there any way to get this to work with [(ngModel]) bindings? – Hafnernuss Jan 11 '23 at 18:25
  • @Hafnernuss I actually very rarely use those bindings so I can't immediately say. Hope you figured something out :-) – Simon_Weaver Jan 20 '23 at 20:42