2

I have a nested form component with an <input> field set up to work with formControlName="nested". The validators are set on the parent FormControl, like so:

  form = this.fb.group({
    value: ['', [Validators.required, Validators.minLength(3)]],
    nested: ['', [Validators.required, Validators.minLength(3)]],
  });

I want to propagate the status from the parent FormControl to the nested <input> so it reacts in the same way as a regular, non-nested <input>, i.e. when just touching it and clicking submit (which does control.markAsTouched()), the status is set to INVALID and the CSS style ng-invalid is set.

I managed to get a partial win after reading this SO post with the following code that subscribes to status changes of the parent control, but "touching" the nested input will revert it to VALID and clicking Submit will not set the ng-invalid style.

@ViewChild('tbNgModel', {static: false}) tbNgModel: NgModel;

private isAlive = true;

constructor(@Optional() @Self() public ngControl: NgControl, private cdr: ChangeDetectorRef) {
    if (this.ngControl != null) {
        this.ngControl.valueAccessor = this;
    }
}

ngAfterViewInit(): void {
    this.ngControl.statusChanges.pipe(
        takeWhile(() => this.isAlive)
    ).subscribe(status => {
        console.log('Status changed: ', status, 'Errors: ', this.ngControl.errors);
        this.tbNgModel.control.setErrors(this.ngControl.errors);
        this.cdr.detectChanges();
    });

    this.ngControl.control.updateValueAndValidity(); // to force emit initial value
}

Stackblitz reproduction of my problem

How can I truly propagate the status to the nested control, all while setting the validators only on the parent FormControl?

Final Solution

From @Eliseo's answer and this other SO post, here is the implementation I ended up doing (it works for me, but there may be a better way still)

component.ts

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

...

    getValidationCss() {
        if (!this.ngControl) return {};

        return {
            'ng-invalid': this.ngControl.invalid,
            'is-invalid': this.ngControl.invalid && this.ngControl.touched,
            'ng-valid': this.ngControl.valid,
            'ng-touched': this.ngControl.touched,
            'ng-untouched': this.ngControl.untouched,
            'ng-pristine': this.ngControl.pristine,
            'ng-dirty': this.ngControl.dirty,
        };
    }

component.html

...
<input #tb class="form-control" [ngClass]="getValidationCss()" ...>
...
dstj
  • 4,800
  • 2
  • 39
  • 61

1 Answers1

1

Dstj, the things must be more simple. see the stackblitz

just in our input we can use [ngClass]

<input [ngClass]="{'ng-touched':ngControl.control.touched,'ng-invalid':ngControl.control.invalid}" 
type="text" class="form-control" [(ngModel)]="value" 
       (ngModelChange)="propagateChange($event)"
       (blur)="touched()"
 > 

Where ngControl is the ngControl inject in the constructor

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

See that is ngControl who is valid/invalid touched/untouched....

Updated add (blur) to mark as touched

Updated2 using ngDoCheck

Other solution is used ngDoCheck,

Our component

  value: string;
  customClass=null;
  onChange:boolean=false;

    constructor(@Self() public ngControl: NgControl,private el:ElementRef) {
        if (this.ngControl != null) {
            this.ngControl.valueAccessor = this;
        }
    }
  ngDoCheck()
  {
    if (!this.onChange)
    {
      this.onChange=true;
      //it's necesary a setTimeOut to give Angular a chance
      setTimeout(()=>{
        this.customClass=this.el.nativeElement.getAttribute('class');
        this.onChange=false;
      })
    }
  }
  change(value:any)
  {
    this.propagateChange(value);
    this.touched(null)
  }

The .html

<input #tbNgModel="ngModel" [ngClass]="customClass" type="text" class="form-control" 
     [(ngModel)]="value" 
     (ngModelChange)="change($event)" 
     (blur)="touched()"> 
Eliseo
  • 50,109
  • 4
  • 29
  • 67
  • Thanks, but your example doesn't apply `ng-invalid` if you just "cycle" through (touch without changing anything). It seems likes it's missing a "refresh" somewhere as it's set to `ng-valid` even though the `ngControl.control.invalid=true` – dstj Jul 10 '19 at 17:26
  • ::glups:: I forgot indicate when is touched, You can use (blur)="touched()". just corrected in stackblitz and in the answer. And yes, the control is invalid, but the input is valid – Eliseo Jul 10 '19 at 18:00
  • Thanks. I still feel having to recode the application of the ng-* CSS classes and the `(blur)` event is weird, but it does work... ;) – dstj Jul 10 '19 at 18:13
  • Upon further tests, it only semi-works because the state in the input's underlying `ngModel` remains VALID, so I get `class="ng-valid ng-invalid"` both together. I need the status to be INVALID because in my real app, I also have a custom directive looking for `control.invalid` status to apply boostrap's `is-invalid` CSS class. It doesn't work if the underlying `ngModel` stays VALID. See https://stackblitz.com/edit/angular-d28achf-xawuef – dstj Jul 10 '19 at 19:22
  • 1
    After thinking about the problem, I decided to use DoCheck to copy the class attributes to the input, see my updated answer and the stackblitz https://stackblitz.com/edit/angular-d28achf-ram9ct?file=src%2Fapp%2Fnested.component.html – Eliseo Jul 11 '19 at 07:07