Angular Forms

January 13, 2023

Reactive Forms

Custom Validators

Control Value Accessor

Use Case

  • Non-native form control elements
  • Custom styling / functionality
  • Control wrapped with related elements
  • Parser / formatter directives

References

Details of CVA (Control Value Accessor)

writeValue: called when the form control is instantiated, when setValue, or patchValue are called. Called when programatic changes from model to view are called (outside CVA)

registerOnChange(fn): let the parent know a value changed. e.g. name.valueChanges.subscribe on the parent. onChange is called when the CVA needs to propagate view to model changes upward.

registerOnTouched(fn): let the parent know a component was interacted with, like validation. e.g. name.touched on the parent. Adds the class 'ng-touched'

setDisabledState: optional called when the form is instantiated if the disabled key is present and when .enabled() or .disabled() are called.

Tips (from Jennifer's talk)

  • Keep wrapper components dumb
  • Just input/output form values
  • Leave validation logic to the parent form component
  • CVA can be used with any form API

Implementation

  1. Add a lookup to the NgControl to the custructor of your custom control.

    NOTE: Alternate approach is using NG_VALUE_ACCESSOR

    // @Optional is required for unit testing to work
    constructor(@Optional() @Self() public controlDir: NgControl) {
        controlDir.valueAccessor = this;
    }
    
  2. Implement the ControlValueAccessor interface

    @Component({
      standalone: true,
      imports: [CommonModule, ReactiveFormsModule, FormsModule],
      providers: [],
      selector: "my-custom-field",
      styleUrls: ["./my-custom-field.component.scss"],
      templateUrl: "./my-custom-field.component.html",
      changeDetection: ChangeDetectionStrategy.OnPush,
      encapsulation: ViewEncapsulation.None,
    })
    export class MyCustomFieldComponent
      implements ControlValueAccessor, OnInit
    {
      disabled: boolean;
      // if you need special handling use a getter/setter
      _value: string;
      // ... constructor from first step
    
      onChanged = (v: string): void => {};
      onTouched = (): void => {};
    
      writeValue(obj: any): void {
        this._value = value;
      }
    
      registerOnChange(fn: (v: string) => void): void {
        this.onChanged = fn;
      }
    
      registerOnTouched(fn: () => void): void {
        this.onTouched = fn;
      }
    
      setDisabledState?(isDisabled: boolean): void {
        this.disabled = isDisabled;
      }
    
      sweetChange($event) {
        this.onTouched();
        this.onChanged($event.currentTarget.value);
      }
    }
    
    <input (change)="sweetChange($event)" type="text" />
    
  3. To add validators to custom field

    ngOnInit(): void {
        const control = this.controlDir.control
        let validators = control.validator
          ? [control.validator, Validators.required]
          : Validators.required
        control.setValidators(validators)
        control.updateValueAndValidity()
    }
    
  4. Bind to the reactive form with formControlName and configuring a formGroup.

    <form [formGroup]="myForm">
      <my-custom-field formControlName="dynamicText"></my-custom-field>
    </form>
    
    @Component({
        standalone: true,
        imports: [
            CommonModule,
            ReactiveFormsModule,
            MyCustomFieldComponent
        ],
        selector: 'my-form',
        styleUrls: ['./my-form.component.scss'],
        templateUrl: './my-form.component.html',
        changeDetection: ChangeDetectionStrategy.OnPush,
        encapsulation: ViewEncapsulation.None,
    })
    export class MyFormComponent implements OnInit {
        myForm: FormGroup;
    
        constructor(private fb: FormBuilder) {}
    
        ngOnInit() {
            this.myForm: FormGroup = this.formBuilder.group(
            {
                name: ['', [Validators.required]],
                dynamicText: ['', [Validators.required]]
            },
            { validators: customFormValidator }
        }
    }
    

Issues

Infinite loop with writeValue and set value

Composition