16

I'm using angular 9 with Angular Material and I have a custom control by implementing the ControlValueAccessor interface. Everything is working fine.

In all my submit buttons when the form is not valid, I call formGroup.markAllAsTouched because all the angular material fields become in red. In this way the user can better understand which controls are not valid.

I need to implement the same behavior with my custom control. How to do that?

I've created a stackblitz project in order to better understand the situation here

yurzui
  • 205,937
  • 32
  • 433
  • 399
Gavi
  • 1,300
  • 1
  • 19
  • 39
  • Your control doesn't mark as touched? Did you implement registerOnTouched in your control? Can you share your code? – Alan Grosz May 03 '20 at 02:30
  • I'm preparing a stackblitz example. I'll back to you soon – Gavi May 03 '20 at 09:24
  • Here the example -> https://stackblitz.com/edit/angular-sh8esy – Gavi May 03 '20 at 10:23
  • First at all, I don't know why you are wrapping a mat-select in another ControlValueAccesor component, you can just share the userTypeCustomControl to the custom control and use it in the mat-select. You have a problem with classes and form control references – Alan Grosz May 03 '20 at 18:48

2 Answers2

12

There is no built-in functionality for propagating touched status to inner FormControl of custom control.

Your simple option would be checking status in ngDoCheck and once custom control becomes touched update status for inner FormControl:

ngDoCheck() {
  if (this.formControl.touched) {
    return;
  }
  if (this.controlDir.control.touched) {
    this.formControl.markAsTouched();
  }
}

Forked Stackblitz

Personally, I don't like such kind of implementations with ControlValueAccessor. I would rather use the same FormControl. This can be done by adding viewProviders with ControlValueAccessor provider to your custom control:

custom-control.component.ts

@Component({
  selector: 'my-custom-control',
  template: `
    <mat-form-field id="userType">
      <mat-label>My Custom Component</mat-label>
      <mat-select [formControlName]="controlName" (blur)="onTouched()">
        <mat-option *ngFor="let current of userTypes" [value]="current.id">{{current.name}}</mat-option>
      </mat-select>
    </mat-form-field>

  `,
   viewProviders: [{
    provide: ControlContainer,
    useFactory: (container: ControlContainer) => container,
    deps: [[new SkipSelf(), ControlContainer]],
 }]
})
export class MyCustomControl {
  @Input() controlName: string;

  userTypes: LookupModel[] = [
      new LookupModel(1, 'first'),
      new LookupModel(2, 'second')
  ];
}

parent html

<form [formGroup]="form">
  <my-custom-control controlName="userTypeCustomControl"></my-custom-control>

Stackblitz Example

yurzui
  • 205,937
  • 32
  • 433
  • 399
  • 2
    Is there a problem with the ControlValueAccessor pattern, because of which you opt for the second solution, or is it just a personal preference? – Avius Dec 02 '20 at 20:24
-1

Another chance is to use the following approach

Custom control code

@Component({
  selector: 'cvl-advertising-type',
  templateUrl: './advertising-type.component.html',
  styleUrls: ['./advertising-type.component.scss'],
})
export class AdvertisingTypeComponent implements OnInit, ControlValueAccessor {
  advertisingTypes: ReadonlyArray<LookupModel>;

  onChange = (value: number) => {
  };
  onTouched = () => {
  };

  constructor(private lookupService: LookupService,
              @Self() public controlDir: NgControl) {
    controlDir.valueAccessor = this;
  }

  ngOnInit(): void {
    this.advertisingTypes = this.lookupService.advertisingTypes;
  }

  registerOnChange(fn: (value: number) => void): void {
    this.controlDir.control.valueChanges.subscribe(fn);
    this.onChange = fn;
  }

  writeValue(value: number): void {
    this.controlDir.control.setValue(value);
    this.onChange(value);
  }

  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  setDisabledState(isDisabled: boolean): void {
    if (isDisabled) {
      this.controlDir.control.disable();
    } else {
      this.controlDir.control.enable();
    }
  }
}

and parent html form

<cvl-advertising-type [formControl]="form.controls.advertisingType"></cvl-advertising-type>

I've found this solution after viewing this video

Last but not least problem is that I'm not understanding how to test it with a shallow test because I've the following error

Error: NodeInjector: NOT_FOUND [NgControl]
Gavi
  • 1,300
  • 1
  • 19
  • 39
  • In order to test it properly you can either use wrapper component or add `@Optional` decorator for `NgControl` – yurzui May 07 '20 at 13:05