0

What I want

If a user click on the submit button and did not fill all the inputs, I will mark all input as touched, since there is a required validator on the form, the empty inputs are supposed to get a red border.

Problem

When I mark all the inputs of the form as touched, the custom input does not change, the classes were not added:

enter image description here

Only the basic input have the classes, here is the result of the html, only the basic input is showing the red borders:

enter image description here

Input component

import {AfterContentChecked, Component, Input, OnInit, Optional, Self} from '@angular/core';
import {ControlValueAccessor, NgControl, UntypedFormControl, UntypedFormGroup} from '@angular/forms';

@Component({
  selector: 'app-input',
  templateUrl: './input.component.html',
  styleUrls: ['./input.component.css'],
  providers: [],
})
export class InputComponent implements ControlValueAccessor, OnInit {
  group = new UntypedFormGroup({
    input: new UntypedFormControl(''),
  });
  touched: boolean = false;
  @Input() label: string = '';
  private ngControl: NgControl;

  constructor(@Optional() @Self() ngControl: NgControl) {
    this.ngControl = ngControl;
    if (this.ngControl) {
      this.ngControl.valueAccessor = this;
    }
  }

  _value: string = '';

  get value(): string {
    return this._value;
  }

  @Input()
  set value(value: string) {
    this._value = value;
    this.input.setValue(value, {emitEvent: false});
  }

  _disabled: boolean = false;

  get disabled(): boolean {
    return this._disabled;
  }

  @Input()
  set disabled(disabled: boolean) {
    this._disabled = disabled;
    if (disabled) {
      this.input.disable();
    } else {
      this.input.enable();
    }
  }

  get input() {
    return this.group.get('input') as UntypedFormControl;
  }


  ngOnInit(): void {
    if (this.ngControl !== null) {
      this.ngControl.control?.statusChanges.subscribe((status) => {
        if (status === 'INVALID') {
          this.input.markAsDirty();
          this.input.markAsTouched();
          this.input.setErrors({incorrect: true});
        }
      });
    } else {
      console.warn('no formcontrol');
    }
  }

  onTouched = () => {
  };

  writeValue(value: string): void {
    this.input.setValue(value, {emitEvent: false});
  }

  registerOnChange(fn: any): void {
    this.input.valueChanges.subscribe((value) => {
      this.markAsTouched();
      fn(value);
    });
  }

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

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

  setDisabledState?(isDisabled: boolean): void {
    if (isDisabled) {
      this.input.disable();
    } else {
      this.input.enable();
    }
  }

}
<span [formGroup]="group">
  <label [innerHTML]="label" ></label>
        <input
          class="w-full" formControlName="input"
          ngDefaultControl
          type="text">
</span>

App omponent

import { Component } from '@angular/core';
import {UntypedFormControl, UntypedFormGroup, Validators} from "@angular/forms";

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'test-input';
  myForm = new UntypedFormGroup({
    inputBasic: new UntypedFormControl('',[Validators.required]),
    inputCustom: new UntypedFormControl('',[Validators.required]),
  })

  submit(){
    if(this.myForm.valid){
      console.log('valid')
    }else{
      this.myForm.markAllAsTouched();
      // If I add the next line the input is updated and the style is good
      // this.myForm.get('inputCustom')?.setErrors({errorMessage:'Error'})
    }
  }
}
<form [formGroup]="myForm" (submit)="submit()">
    <div>
      Basic input <input formControlName="inputBasic"/>
    </div>
  <div>
    <app-input label="Custom input" formControlName="inputCustom"></app-input>
  </div>
  <button type="submit">Submit</button>
</form>

CSS in both component

.ng-invalid.ng-touched{
  border-color:red;
}
Gregory Boutte
  • 584
  • 6
  • 26
  • 1
    You may need to run `updateValueAndValidity()` after you call `markAllAsTouched`. I'm not sure whether `myForm.updateValueAndValidity()` will work, or whether you would need to iterate through the controls and call `control.updateValueAndValidity()` for each. If that doesn't work, it would be really useful if you could provide a StackBlitz containing the above code so we can poke around further – nate-kumar Jan 26 '23 at 09:19
  • It worked adding `this.myForm.get('inputCustom')?.updateValueAndValidity()`, after the mark as touched. So I would need to loop on the control to do it for all controls. You can post an answer if you want. – Gregory Boutte Jan 26 '23 at 09:50
  • Do you have an idea on how to do it on the other way ? When I reset the form and mark as untouched the control and then update the value and validity the control is still red. Here is a stackblitz: https://stackblitz.com/edit/angular-ivy-g1mg9t?file=src/app/app.component.ts – Gregory Boutte Jan 26 '23 at 09:50

1 Answers1

0

When we have a custom form control we need understand that the ng-invalid and .ng-touched is applied to the whole component, and who is invalid/touched is the ngControl

As we know the ngControl we can use in .html

<input [class.ng-touched]="ngControl?.touched"
       [class.ng-invalid]="ngControl?.invalid"
       ...
>

BTW: it's a bit sledgehammer to crack a nut use a custom form control to create an input with a label (or severals input with a label and an error) when we can use a simple component and viewProviders:[{ provide: ControlContainer, useExisting: FormGroupDirective }], see, e.g. this SO

Eliseo
  • 50,109
  • 4
  • 29
  • 67