5

I have a simple Angular (v8.0) component which just contains an input with the required attribute set. I have a template driven form which contains that component and binds it to an initial value. When the app starts the status of the input is valid, because it's bound to an initial value. But the form is incorrectly invalid. After making any change to the input value the form status is then correct, and remains correct from then on.

I'm returning the input's validation status in my validate method implementation and this seems to be the source of the problem. It works properly if I return an object I construct myself based on the current value of the control. However I would rather avoid this because I'll need other validation states in future and I don't want to have manually check for them all.

I've tried adding calls to updateValueAndValidity at various places in the code but this has no effect.

How can I make the initial validation status of the form take into account the status of the nested custom component?

Here's the code for the component:

import { AfterViewInit, Component, ViewChild } from "@angular/core";
import { ControlValueAccessor, NG_VALUE_ACCESSOR, NG_VALIDATORS, FormControl, Validator, NgModel } from "@angular/forms";

@Component({
  selector: "my-input",
  template: '<input [(ngModel)]="value" required #model="ngModel" />',
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: MyInputComponent,
      multi: true
    },
    {
      provide: NG_VALIDATORS,
      useExisting: MyInputComponent,
      multi: true
    }
  ]
})
export class MyInputComponent implements ControlValueAccessor, Validator {
  onChange: any = () => {};
  onTouch: any = () => {};

  @ViewChild("model", { static: true }) model: NgModel;

  private _value: string = "";

  set value(x: string) {
    if (this._value !== x) {
      this._value = x;
      this.onChange(x);
    }
  }
  get value() {
    return this._value;
  }

  writeValue(obj: any): void {
    this.value = obj;
  }
  registerOnChange(fn: any): void {
    this.onChange = fn;
  }
  registerOnTouched(fn: any): void {
    this.onTouch = fn;
  }

  validate(c: FormControl) {
    return this.model.errors;
    //return this.value ? null : { required: true }; // this works!
  }
}

And here's the code for the outer form which contains it:

import { Component } from "@angular/core";

@Component({
  selector: "my-app",
  template: `
    <form #frm="ngForm">
      <my-input [(ngModel)]="myValue" name="myInput"></my-input>
    </form>
  `
})
export class AppComponent {
  myValue: string = "hello";
}

Here's a StackBlitz: https://stackblitz.com/edit/angular-lp3xpa. Add or remove a character to the input and the form status changes to valid.

Simon C
  • 109
  • 1
  • 8
  • I removed validate function from your myInputComponent component and just added required attribute in `` and it works as expected. you can control your validations from outside. No need to add validators – Ashish Jan 13 '20 at 18:14
  • @Ashish thanks yes that works but only for the form status. The internal label in the control is no longer correct (see [here](https://stackblitz.com/edit/angular-pnamzt)). – Simon C Jan 14 '20 at 08:55
  • 1
    I have added the changes, please check now https://stackblitz.com/edit/angular-xmscak – Ashish Jan 14 '20 at 09:15
  • @Ashish Yes I see what you mean. That works, but it means each instance of `myInputComponent` must include the `required` attribute. My original intention was to hide the validation inside `myInputComponent` without including the attribute, but still have that bubble up to the form. Is that possible? – Simon C Jan 14 '20 at 12:07
  • Thats never a good idea. When you create a shared components, you should always take all these things into consideration. By doing what i suggest you keep the option to later change the logic. You can take any example from material components or any other lib. – Ashish Jan 14 '20 at 16:43

1 Answers1

0

It sounds like the same issue as this question, which suggests a workaround of manually triggering another validation from writeValue using setTimeout. This seems to fix the problem.

In MyInputComonent:

  writeValue(obj: any): void {
    this.value = obj;
    setTimeout(() => { this.onChange(this.value); }, 0);
  }

See here.

But this seems like a hack.

Simon C
  • 109
  • 1
  • 8