angular material reactive forms pattern - how to make a component for this pattern using ControlValueAccessor

1.8k views Asked by At

working with angular materials mat-form-field and reactive forms. In a project I have a recurring pattern that looks like this

// ts
this.formGroup = this.formBuilder.group({
    name: ['', ServerValidation]
})

<!-- html -->
<div [formGroup]="formGroup">
  <mat-form-field>
    <input
      matInput
      formControlName="name"
      [placeholder]="'Name'"
      name="name"
     />
    <mat-error
     *ngIf="
       formGroup
       .get('name')
       .hasError('serverValidation')
       "
      >
      {{
       formGroup
       .get("name")
       .getError("serverValidation")
      }}
     </mat-error>
   </mat-form-field>
</div>

This is a high level - accepting that I can receive validation errors from the server - how can i repeat this http template pattern in a component? I have a hunch that I should utilise ControlValueAccessor - but do not know how to do so.

The implementation I imagine might look something like this

<!-- html -->
<div [formGroup]="formGroup">
  <serverValidatedInput formControlName="'name'">
    <mat-error>error message for client side validation</mat-error>
  </serverValidatedInput>
</div>

So essentially I want to use this custom component like a regular material input (more-or-less), except that it comes with the server validation error by default. Could anyone give me some direction here - thanks. :)

1

There are 1 answers

6
G. Tranter On

There are two ways to do this - an easy way and a difficult way. The difficult way is to implement ControlValueAccessor and this buys you more flexibility in how the component can be used. The easy way is to just pass things through your component to the actual form elements inside. If you don't need flexibility in how this component is used, take the easy way.

First though, you need to get away from the idea of using mat-error outside of mat-form-control. It simply won't work, and you don't need it to work. Leave it inside the form field and provide the content for it instead. Along with that, apply your error logic to the content of mat-error, not to the mat-error itself. And remember that you don't need logic to display mat-error - form field automatically takes care of that when the form control has an error. You only need logic to determine what the error content should be.

A simple wrapper for mat-form-field would look something like this:

my-form-field.html

<mat-form-field>
  <input matInput type="text" [placeholder]="placeholder" [formControl]="myFormControl" required>
  <mat-error>
    <ng-content></ng-content>
  </mat-error>
</mat-form-field>

my-form-field.ts

import {Component, Input} from '@angular/core';
import {FormControl} from '@angular/forms';

@Component({
  selector: 'my-form-field',
  templateUrl: 'my-form-field.html'
})
export class MyFormField {
  @Input() myFormControl: FormControl;
  @Input() placeholder: string;
}

Usage

custom-form-field-example.html

<form [formGroup]="formGroup">
  <my-form-field placeholder="Name" [myFormControl]="formGroup.get('name')">
    <ng-container *ngIf="formGroup.get('name').hasError('required')">
     This field is required
    </ng-container>
    <ng-container *ngIf="formGroup.get('name').hasError('serverValidation')">
      Server validation failed
    </ng-container>
  </my-form-field>
</form>

custom-form-field-example.ts

import {Component} from '@angular/core';
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
import {ServerValidation} from '...';

@Component({
  selector: 'custom-form-field-example',
  templateUrl: 'custom-form-field-example.html'
})
export class CustomFormFieldExample {
  formGroup: FormGroup;

  constructor(formBuilder: FormBuilder) {
    this.formGroup = formBuilder.group({
      name: ['', [Validators.required, ServerValidation]]
    });
  }
}