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?
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