2

I have a textbox control that implements ControlValueAccessor. In the form, I put a required validator on the the form field name:

 this.formGroup = this.fb.group({
        name: ['', Validators.required]
    });

In the template I use the control like this:

<input-text formControlName="name"></input>

The Angular required validation works but the inner textbox has no required attribute. I want to style this textbox if it is required, how do I accomplish this?

Mcanic
  • 1,304
  • 16
  • 22
  • 1
    Did you try the injector solution : https://stackoverflow.com/a/51126965/8945135 – ibenjelloun Oct 23 '18 at 06:34
  • tried and yes now I can inspect the errors array, but I want to know on beforehand if the field is required, if it was not yet validated, and also if it was validated and valid. how can I know, inside a ControlValueAccessor implementation, if the control has a required validator? – Mcanic Oct 23 '18 at 07:38
  • I think you should not think about what errors you have from inside the component, you just need to check if there are errors (required or other validation errors) and change the component style. This way your component is generic and can be used in other use cases with different validators. – ibenjelloun Oct 23 '18 at 07:43

3 Answers3

0

You need to check for the value change of form control like below

this.control.valueChanges.subscribe(
            (res) => {
                this.setErrorMessage(this.control);
            }
        )

And then inside setErrorMessage check control.errors

if (control.errors) {
  for (let propertyName in control.errors) {
    if(propertyName == "required") {
      // you can do things here to your input field by using element reference
    }
  }
}

I had a created to function to know if the input field is dirt=y or not

//Validate single field
export function isInputFieldDirty(controlName: string, formGroupObject: FormGroup) {
if (formGroupObject.get(controlName)) {
    return !formGroupObject.get(controlName).pristine &&
        ((formGroupObject.get(controlName).untouched && formGroupObject.get(controlName).dirty && formGroupObject.get(controlName).invalid) ||
            (formGroupObject.get(controlName).touched && formGroupObject.get(controlName).invalid))
        ? true : false;
}

}

djain4
  • 257
  • 2
  • 13
  • 1
    Please let me know why its deprecated ?? – djain4 Oct 23 '18 at 07:01
  • Thanks I can inspect the errors array, but I want to know on beforehand if the field is required, if it was not yet validated, and also if it was validated and valid. how can I know, inside a ControlValueAccessor implementation, if the control has a required validator? – Mcanic Oct 23 '18 at 07:39
  • 1
    To check if the control is valid u can use control.valid. I am not sure about ControlValueAccessor . Also why do you want to know about the validators on a control ? I have added a function see if that helps – djain4 Oct 23 '18 at 07:46
  • Because I want to show somehow which fields are mandatory as soon as the form is shown. Otherwise, the user has to click save first and then discovers that some fields are required, I think it's better to show on beforehand which fields are required. Am I thinking wrong? – Mcanic Oct 23 '18 at 07:49
  • 1
    Yes you are thinking it right, please check this link : https://github.com/angular/angular/issues/13461, in this "rodolfojnn" has given a solution, you can modify it as per your need – djain4 Oct 23 '18 at 07:59
0

If you want to style the the field if the contrl has required validator you can use this utility:

  isRequired(formControl: AbstractControl): boolean {
    return this.hasRequiredField(formControl);
  }
  hasRequiredField = (abstractControl: AbstractControl): boolean => {
    // caso formControl
    if (abstractControl.validator) {
      const validator = abstractControl.validator({} as AbstractControl);
      if (validator && validator.required) {
        return true;
      }
    } // caso formGroup
    if (abstractControl['controls']) {
      for (const controlName in abstractControl['controls']) {
        if (abstractControl['controls'][controlName]) {
          if (this.hasRequiredField(abstractControl['controls'][controlName])) {
            return true;
          }
        }
      }
    }
    return false;
  }

in your controller

isRequired(formControlName){
    isRequired(formControlName: string): boolean {
        return this.utils.isRequired(this.form.get(formControlName));
    }
}

if you want to show message when field is not valid and apply a style you can use simply "valid" property For showing message error when the field is not valid an object is returnet to hasError('required')

Now if you want to style

<label> Name {{isRequired('name') ? '*' :'' }} </label>
<input-text formControlName="name" [ngClass]="{'required': isRequired('name'), 'notValid' : !this.form.get('name').valid  }"></input>

<span class="help-block error" *ngIf="((form.get('name').touched || form.get('name').dirty) && !form.get('name').valid)">
        <span *ngIf="form.get('name').hasError('required')">
            {{ 'ERROR_MSG.REQUIRED' | translate }}
        </span>
        <span *ngIf="form.get('name').hasError('maxlength')">
            {{ 'ERROR_MSG.MAXLENGTH' | translate }} {{getError('maxlength').requiredLength}}
        </span>
        <span *ngIf="form.get('name').hasError('minlength')">
            {{ 'ERROR_MSG.MINLENGTH' | translate }} {{getError('minlength').requiredLength}}
        </span>

        <span *ngIf="form.get('name').hasError('myCustomError')">
            {{ 'ERROR_MSG.CUSTOM' | translate }}
        </span>
    </span>
</div>

getError(error: string) {
    return this.form.controls['name'].errors[error];
  }
raffaeleambrosio
  • 235
  • 2
  • 11
0

2023 answer

Inject the parent component's form control into the custom control, then call hasValidator(...) on the parent control like this:

  // your custom input component...

  public get isRequired(): boolean {
    return (
      !!this.parent &&
      !!this.parent.control &&
      this.parent.control.hasValidator(Validators.required)
    );
  }

  constructor(@Self() @Optional() private parent: NgControl) {
    this.parent.valueAccessor = this;
  }

  // ...

Full example

This example shows how to show an asterisk when the required validator is present.

SignupPage template:

<form [formGroup]="fg">
    <app-text-input
      [label]="'signup.name' | translate"
      formControlName="name"
    ></app-text-input>
    <app-text-input
      [label]="'signup.email' | translate"
      formControlName="email"
    ></app-text-input>
</form>

SignupPage component:

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

@Component({
  selector: 'app-signup',
  templateUrl: './signup.page.html',
  styleUrls: ['./signup.page.scss'],
})
export class SignupPage {
  public readonly fg = new FormGroup({
    name: new FormControl<string>('', [Validators.required]),
    email: new FormControl<string>('', [Validators.required, Validators.email]),
  });
}

TextInput template:

<input [formControl]="control" (blur)="onBlur()" />
<label *ngIf="label">
  {{ label }}
  <ng-container *ngIf="isRequired">*</ng-container>
</label>

TextInput component:

import {
  Component,
  HostBinding,
  Input,
  OnDestroy,
  OnInit,
  Optional,
  Self,
} from '@angular/core';
import {
  ControlValueAccessor,
  FormControl,
  NgControl,
  Validators,
} from '@angular/forms';
import { Subject, takeUntil, tap } from 'rxjs';

@Component({
  selector: 'app-text-input',
  templateUrl: './text-input.component.html',
  styleUrls: ['./text-input.component.scss'],
})
export class TextInputComponent
  implements OnInit, OnDestroy, ControlValueAccessor
{
  @Input()
  public label?: string;

  public get isRequired(): boolean {
    return (
      !!this.parent &&
      !!this.parent.control &&
      this.parent.control.hasValidator(Validators.required)
    );
  }

  public readonly control = new FormControl<string>('');

  private onTouched?: () => void;
  private onChanges?: (v: string | null) => void;
  private readonly onDestroy$ = new Subject<void>();

  constructor(@Self() @Optional() private parent: NgControl) {
    this.parent.valueAccessor = this;
  }

  public ngOnInit(): void {
    this.control.valueChanges
      .pipe(
        tap((e) => {
          if (this.onChanges) {
            this.onChanges(e);
          }
        }),
        takeUntil(this.onDestroy$)
      )
      .subscribe();
  }

  public ngOnDestroy(): void {
    this.onDestroy$.next();
    this.onDestroy$.complete();
  }

  public registerOnChange(fn: any): void {
    this.onChanges = fn;
  }

  public registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  public writeValue(value: string | null): void {
    this.control.setValue(value, { emitEvent: false });
  }

  public setDisabledState(value: boolean): void {
    if (value) {
      this.control.disable();
    } else {
      this.control.enable();
    }
  }

  public onBlur(): void {
    if (this.onTouched) {
      this.onTouched();
    }
  }
}