0

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

Joey Gough
  • 2,753
  • 2
  • 21
  • 42

1 Answers1

0

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]]
    });
  }
}
G. Tranter
  • 16,766
  • 1
  • 48
  • 68
  • not quite - spot the difference between your `my-form-field.html` and my example - in my example I had `formGroup.get(formControl).hasError('server-validation')` baked into it - I want to avoid rewriting this. – Joey Gough Feb 06 '19 at 14:59
  • I was giving an example, not a final solution, showing how to make error content reusable. I expected you'd be able to figure out what you need to make the example into the solution that fits your needs. Replace the text "Server validation failed" with `{{formGroup.get('name').getError('serverValidation')}}` assuming you meant `getError` and not `hasError` that you wrote. – G. Tranter Feb 06 '19 at 15:32
  • Yes I got formGroup undefined - sorry it took so long to get around to this - was busy - I am trying to implement with controlValueAccessor atm. – Joey Gough Feb 06 '19 at 15:34
  • I stil lhavent managed it - but i found this very interesting - https://stackoverflow.com/a/44732530/8896573 – Joey Gough Feb 07 '19 at 09:08
  • 1
    There are better ways to do it. Inject an NgControl object into the constructor `constructor(@Optional() @Self() public ngControl: NgControl)` and assign the custom component which implements `ControlValueAccessor` as the control's accessor `if (this.ngControl) this.ngControl.valueAccessor = this;`. This will allow you to use any type of form directive - reactive or template, `ngModel`, `formControl`, or `formControlName`. If you try to wrap a `MatInput` inside your component and want it to be the actual field, you have to jump through a whole lot more hoops. – G. Tranter Feb 07 '19 at 15:42
  • nice one - thanks - thats cool - also documented here https://material.angular.io/guide/creating-a-custom-form-field-control#-code-ngcontrol-code- – Joey Gough Feb 08 '19 at 17:14