2

I try to implement nested reactive forms in angular.

But when I call markAllAsTouched() and updateValueAndValidity() in the submit function the nested inputs are not touched and therefore the error message isn't displayed.

I tried a hack to touch the inputs when the outer form is touched with this code in the statusChanges subscription

if (!this.control.touched && this.parentForm.touched) {
  this.control.markAsTouched();
  this.control.updateValueAndValidity();
}

But this leads to every form been touched when I enter text in one of them and therefore displaying the error message to soon.

What am I doing wrong? Are reactive forms in angular meant to be used this way? I want to have a single button that touches and updates every nested form element in the outer form.

I provided a repo on github (https://github.com/to-cl/testform) where I recreated the error.


In appComponent there is a testForm that includes several custom input fields.

Template:

<form [formGroup]="testForm">
  <app-myinput [parentForm]="testForm" label="Comment" type="text" formControlName="comment"></app-myinput>
  <app-myinput [parentForm]="testForm" label="Name" type="text" formControlName="name"></app-myinput>
  <app-myinput [parentForm]="testForm" label="Email" type="text" formControlName="email"></app-myinput>
  <app-myinput [parentForm]="testForm" label="Password" type="text" formControlName="password"></app-myinput>
  <button style="margin: 1em;" (click)="onSubmit()">Submit</button>
</form>

Component:

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
  title = 'form-test';

  testForm!: FormGroup;

  constructor(private _formBuilder: FormBuilder) { }

  ngOnInit(): void {
    this.testForm = this._formBuilder.group({
      comment: [],
      name: [],
      email: [],
      password: [],
    });
  }

  onSubmit() {
    console.log(this.testForm.valid)
    this.testForm.markAllAsTouched();
    this.testForm.updateValueAndValidity();
  }
}

The custom input fields are components on their own:

Template:

<form [formGroup]="form" style="padding: 1em;">
    <label style="padding-right: 1em">{{label}}</label>
    <input id="value" formControlName="value" type="text" [required]="required">
    <small style="color: rgb(209, 67, 67)" *ngIf="control.touched && control.hasError('required')">
        required
    </small>
</form>

Component:

import { AfterViewInit, Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormControl, FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { Subscription } from 'rxjs';

@Component({
  selector: 'app-myinput',
  templateUrl: './myinput.component.html',
  styleUrls: ['./myinput.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => MyinputComponent),
      multi: true
    },
    {
      provide: NG_VALIDATORS,
      useExisting: forwardRef(() => MyinputComponent),
      multi: true
    }
  ]
})
export class MyinputComponent implements ControlValueAccessor, AfterViewInit, OnDestroy {

  form: FormGroup;

  subscriptions: Subscription[] = [];

  @Input()
  label: string = '';

  @Input()
  formControlName = '';

  @Input()
  parentForm!: FormGroup;

  @Input()
  required: boolean = true;

  get control() {
    return this.form.controls['value'];
  }

  get value(): any {
    return this.form.value;
  }

  set value(value: any) {
    this.control.setValue(value)
    this.onChange(value);
    this.onTouched();
  }

  writeValue(value: string): void {

  }

  constructor(private formBuilder: FormBuilder) {
    this.form = this.formBuilder.group({
      value: ['', Validators.required]
    });

    this.subscriptions.push(
      // any time the inner form changes update the parent of any change
      this.form.valueChanges.subscribe(value => {
        this.onChange(value.value);
        this.onTouched();

        this.manipulateValidators();
      })
    );
  }

  ngAfterViewInit(): void {
    this.addStatusChangeSubscription();
    this.addValueChangeSubscription();
  }

  ngOnDestroy(): void {
    this.subscriptions.forEach(s => s.unsubscribe());
  }

  addStatusChangeSubscription() {
    this.subscriptions.push(
      this.parentForm.statusChanges.subscribe(() => {
        if (this.parentForm.disabled && !this.form.disabled) {
          this.form.disable();
        }

        if (this.parentForm.enabled && !this.form.enabled) {
          this.form.enable();
        }

        if (!this.control.touched && this.parentForm.touched) {
          this.control.markAsTouched();
          this.control.updateValueAndValidity();
        }
      })
    );
  }

  addValueChangeSubscription() {
    this.subscriptions.push(
      this.parentForm.valueChanges.subscribe(value => {
        if (this.value.value !== value[this.formControlName]) {
          this.form.patchValue({ value: value[this.formControlName] }, { emitEvent: false })
        }
      })
    );
  }

  validate(_: FormControl) {
    let formInvalid: any = {};
    formInvalid[this.formControlName] = { valid: false };

    return this.form.valid ? null : formInvalid;
  }

  onChange: any = () => {
    /* Left blank intentionally */
  };
  onTouched: any = () => {
    /* Left blank intentionally */
  };

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

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

  manipulateValidators(): void {
    if (this.required && !this.form.controls['value'].hasValidator(Validators.required)) {
      this.form.controls['value'].addValidators(Validators.required);
    }
    if (!this.required && this.form.controls['value'].hasValidator(Validators.required)) {
      this.form.controls['value'].removeValidators(Validators.required);
    }
  }

}
4
  • The github code doesn't seem to be related to that, did you push everything? Commented Jul 25, 2022 at 7:22
  • Yes I forgot to push everything. I updated it now. Commented Jul 25, 2022 at 8:28
  • I don't see the issue, I can see, that after pressing the button the word required pops up after every input field. Isn't the requested behavior? Commented Jul 25, 2022 at 8:32
  • But when you edit any of the inputs all the others show the required message too. Commented Jul 25, 2022 at 8:40

1 Answer 1

1

Here you go, will mark all controls as dirty.

export function markAsDirty(control: FormGroup | AbstractControl) {
  control.markAsDirty();
  if (control instanceof FormGroup) {
    for (const key of Object.keys(control.controls)) {
      markAsDirty(control.controls[key]);
    }
  }
  if (control instanceof FormArray) {
    for (const key of Object.keys(control.controls)) {
      markAsDirty(control.controls[key]);
    }
  }
  return;
}
Sign up to request clarification or add additional context in comments.

1 Comment

I think that is exactly the behaviour that is not desired. This marks all controls dirty, but only the current form and the parent form should be touched.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.