angulartypescriptangular-reactive-formsangular-formscontrolvalueaccessor

Angular custom FormControl with updateOn blur


I have a reactive form group that is working just fine with updateOn submit and updateOn change.

But when I switch to updateOn blur, the required error is triggered directly after entering a character in my custom form control and never goes away. Also, when I submit my form, all the values of my custom form controls are empty (even though the fiels are completed).

My form TS:

activityForm = this.fb.group({
    placeName: ['', { validators: [Validators.required, Validators.maxLength(75)] }],
    description: ['', { validators: [Validators.required, Validators.minLength(25), Validators.maxLength(2000)] }],
  }, { updateOn: 'blur' })

My form HTML:

<form class="container" [formGroup]="activityForm" (ngSubmit)="onSubmit()">

    <div class="container__field">
        <p class="container__title">{{'poi.place_name' | transloco}}</p>
        <custom-input formControlName="placeName" placeholder="{{'poi.select_place_name' | transloco}}" [error]="activityForm.get('placeName')?.errors !== null && activityForm.get('placeName')!.touched"></custom-input>
        <custom-error [text]="'error.required' | transloco" *ngIf="activityForm.get('placeName')?.hasError('required') && activityForm.get('placeName')?.touched"></custom-error>
        <custom-error [text]="'error.maxlength' | transloco : { charact : this.activityForm.get('placeName')?.errors?.maxlength.requiredLength }" *ngIf="activityForm.get('placeName')?.hasError('maxlength')"></custom-error>
    </div>

    <div class="container__field">
        <p class="container__title">{{'poi.description' | transloco}}</p>
        <custom-textarea formControlName="description" placeholder="{{'poi.select_description' | transloco}}" [limit]="2000" [error]="activityForm.get('description')?.errors !== null && activityForm.get('description')!.touched"></custom-textarea>
        <custom-error [text]="'error.required' | transloco" *ngIf="activityForm.get('description')?.hasError('required') && activityForm.get('description')?.touched"></custom-error>
        <custom-error [text]="'error.maxlength' | transloco : { charact : this.activityForm.get('description')?.errors?.maxlength.requiredLength }" *ngIf="activityForm.get('description')?.hasError('maxlength')"></custom-error>
        <custom-error [text]="'error.minlength' | transloco : { charact : this.activityForm.get('description')?.errors?.minlength.requiredLength }" *ngIf="activityForm.get('description')?.hasError('minlength')"></custom-error>
    </div>

    <div class="container__button">
        <custom-button text="{{'poi.next_step' | transloco}}" color="primary" type="submit"></custom-button>
    </div>
</form>

Custom FormControl TS (the textarea one):

export class CustomTextareaComponent implements ControlValueAccessor {

  @Input() placeholder = '' // give a transloco string directly
  @Input() limit = 500
  @Input() error: boolean = false

  value: string | null = null
  currentNumberOfCharacters: number = 0
  isActive: boolean | undefined

  onChange: any = () => { }
  onTouch: any = () => { }
  touched = false
  disabled = false
  
  changes(event: Event) {
    if (this.disabled) return
    this.markAsTouched()
    this.value = event?.target ? (event?.target as HTMLTextAreaElement).value : ''
    this.currentNumberOfCharacters = this.value.length
    this.onChange(this.value)
  }


  /* Methods needed by ControlValueAccessor to transform this component into a "form friendly" component */

  registerOnChange(providedFunction: any) {
    this.onChange = providedFunction
  }

  registerOnTouched(providedFunction: any) {
    this.onTouch = providedFunction
  }

  writeValue(providedValue: any) {
    if (providedValue) {
      this.value = providedValue
      this.currentNumberOfCharacters = providedValue.length
    }
  }

  setDisabledState(providedDisabledVal: any) {
    this.disabled = providedDisabledVal
  }

  markAsTouched() {
    if (!this.touched) {
      this.onTouch()
      this.touched = true
    }
  }

}

Custom FormControl HTML (the textarea one):

<div class="plnd-textarea">
    <div class="plnd-textarea__field-container" [class.plnd-textarea__field-container--written]="value" [class.plnd-textarea__field-container--active]="isActive" [class.plnd-textarea__field-container--error]="error">
        <textarea [placeholder]="placeholder | transloco" [value]="value" (keyup)="changes($event)" (focus)="isActive=true" (focusout)="isActive=false" [maxLength]="limit" class="plnd-textarea__field"></textarea>
    </div>
    <p class="plnd-textarea__characters">{{ currentNumberOfCharacters }}/{{ limit }} {{ 'common.characters' | transloco }}</p>
</div>

So my question is: How do I make updateOn blur work with a custom FormControl?


Solution

  • You are actually missing the blur event. Also seems that there is a lot of noise in your code, don't know what for example active is for. Here is a cleaned up version:

    Template:

    <textarea [value]="value" (blur)="onBlur()" (keyup)="changes($event)"></textarea>
    

    and TS:

    value: string | null = null;
    onChange: any = () => {};
    onTouch: any = () => {};
    disabled = false;
    
    changes(event: Event) {
      if (this.disabled) return;
      this.value = event?.target
      ? (event?.target as HTMLTextAreaElement).value
      : '';
      this.onChange(this.value);
    }
    
    onBlur() {
      this.onTouch();
    }
    registerOnChange(providedFunction: any) {
      this.onChange = providedFunction;
    }
    
    registerOnTouched(providedFunction: any) {
      this.onTouch = providedFunction;
    }
    
    writeValue(providedValue: any) {
      this.value = providedValue;
    }
    
    setDisabledState(providedDisabledVal: any) {
      this.disabled = providedDisabledVal;
    }
    

    Also you need to remove markAsTouched() from your onChange function, since otherwise your error will show immediately when typing, as the field becomes touched and as you have update on blur, means that the value does not change until blur event happens.

    Here is a DEMO for your reference

    Also you can look into the following article to not reinvent the wheel when implementing ControlValueAccessor