1
votes

I've got the following child component with a form and I need to display error message under my control when parent component submit main form.

import {
  Component, ChangeDetectorRef, forwardRef,
  NgModule, OnInit
} from '@angular/core';
import {
  FormGroup, FormControl, FormBuilder, Validators,
  NG_VALIDATORS, Validator, AbstractControl, ValidationErrors
} from '@angular/forms';
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';

export const ADDRESS_VALUE_ACCESSOR: any = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => AddressFormComponent),
  multi: true
};

export const ADDRESS_VALIDATORS: any = {
  provide: NG_VALIDATORS,
  useExisting: forwardRef(() => AddressFormComponent),
  multi: true
};

@Component({
  selector: 'app-address-form',
  templateUrl: 'address-form.component.html',
  providers: [ADDRESS_VALUE_ACCESSOR, ADDRESS_VALIDATORS],
})
export class AddressFormComponent implements OnInit, ControlValueAccessor, Validator {

  addressForm: FormGroup;

  private innerValue: any;

  onModelTouched: Function = () => { };
  onModelChange: Function = () => { };

  constructor(
    private _fb: FormBuilder,
    private ref: ChangeDetectorRef,

  ) { }

  ngOnInit() {

    this.addressForm = this._fb.group({
      'via': new FormControl('', Validators.required),
      'civico': new FormControl(''),
      'cap': new FormControl(''),
      'comune': new FormControl(''),
      'provincia': new FormControl(''),
      'regione': new FormControl(''),
      'stato': new FormControl(''),
      'frazione': new FormControl('')
    });

  }

  // get accessor
  get value(): any {
    return this.innerValue;
  }

  // set accessor including call the onchange callback
  set value(v: any) {
    if (v !== this.innerValue) {
      this.innerValue = v;
      this.onModelChange();
    }
  }

  // Set touched on blur
  onBlur() {
    this.onModelTouched();
  }

  writeValue(value: any): void {
    this.value = value;

    if (this.value != null) {
      this.addressForm.get('via').setValue(this.value.via);
      this.addressForm.get('civico').setValue(this.value.civico);
      this.addressForm.get('cap').setValue(this.value.cap);
      this.addressForm.get('comune').setValue(this.value.comune);
      this.addressForm.get('provincia').setValue(this.value.provincia);
      this.addressForm.get('frazione').setValue(this.value.frazione);
      this.addressForm.get('stato').setValue(this.value.stato);
      this.addressForm.get('regione').setValue(this.value.regione);
    }

    this.ref.markForCheck();
  }

  registerOnChange(fn: Function): void {

    this.addressForm.valueChanges.subscribe(() => {
      fn(this.addressForm.value);
    });

    this.onModelChange = fn;
  }

  registerOnTouched(fn: Function): void {
    this.onModelTouched = fn;
  }

  validate(c: AbstractControl): ValidationErrors | null {
    return this.addressForm.valid ? null : { subformerror: 'Problems in subform!' };
  }

  registerOnValidatorChange(fn: () => void): void {
    this.addressForm.statusChanges.subscribe(fn);
  }
}

@NgModule({
  imports: [

    CommonModule,
    FormsModule,
    ReactiveFormsModule

  ],
  exports: [AddressFormComponent],
  declarations: [
    AddressFormComponent
  ],
  entryComponents: [AddressFormComponent],
  providers: [

  ]
})

export class AddressFormModule { }

View:

<form [formGroup]="addressForm">

  <div class="row">
      <div class="form-group col-md-10">
          <label for="txtVia">Via</label>
          <input type="text" pInputText class="form-control" id="txtVia"
              formControlName="via">
              <div *ngIf="!addressForm.controls['via'].valid && (addressForm.controls['via'].dirty || addressForm.controls['via'].touched)"
              class="alert alert-danger">
              Campo obbligatorio
          </div>
      </div>
      <div class="form-group col-md-2">
          <label for="txtCivico">Civico</label>
          <input type="text" pInputText class="form-control" id="txtCivico"
              formControlName="civico">
      </div>
  </div>
  <div class="row">
      <div class="form-group col-md-3">
          <label for="txtCap">Cap</label>
          <input type="text" pInputText class="form-control" id="txtCap"
              formControlName="cap">
      </div>
      <div class="form-group col-md-6">
          <label for="txtComune">Comune</label>
          <input type="text" pInputText class="form-control" id="txtComune"
              formControlName="comune">
      </div>
      <div class="form-group col-md-3">
          <label for="txtProvincia">Provincia</label>
          <input type="text" pInputText class="form-control" id="txtProvincia"
              formControlName="provincia">
      </div>
  </div>
  <div class="form-group">
      <label for="txtFrazione">Frazione</label>
      <input type="text" pInputText class="form-control" id="txtFrazione"
          formControlName="frazione">
  </div>
  <div class="row">
      <div class="form-group col-md-6">
          <label for="txtRegione">Regione</label>
          <input type="text" pInputText class="form-control" id="txtRegione"
              formControlName="regione">
      </div>
      <div class="form-group col-md-6">
          <label for="txtStato">Stato</label>
          <input type="text" pInputText class="form-control" id="txtStato"
              formControlName="stato">
      </div>
  </div>

</form>

I bind it to a parent form this way:

view:

<form (submit)="onConfirm($event)" *ngIf="aziendaform" [formGroup]="aziendaform">
    <app-address-form formControlName="indirizzoSpedizione"></app-address-form>
</form>

code:

import { Component, Input, Output, EventEmitter, OnInit, SimpleChanges, OnChanges, ChangeDetectorRef } from '@angular/core';
import { FormGroup, FormBuilder, Validators, FormControl } from '@angular/forms';

@Component({
    selector: 'app-azienda-form',
    templateUrl: 'aziende-form.component.html'
})
export class AziendeFormComponent implements OnInit, OnChanges {

    aziendaform: FormGroup;


    constructor(
        private fb: FormBuilder,
        private cdr: ChangeDetectorRef
    ) {
    }

    ngOnChanges(changes: SimpleChanges) {

    }

    ngOnInit(): void {

        this.aziendaform = this.fb.group({          
            'indirizzoSpedizione': new FormControl(null)         
        });

        this.cdr.detectChanges();
    }

    validateAllFormFields(formGroup: FormGroup) {
        Object.keys(formGroup.controls).forEach(field => {
            const control = formGroup.get(field);
            if (control instanceof FormControl) {
                control.markAsTouched({ onlySelf: true });
            } else if (control instanceof FormGroup) {
                this.validateAllFormFields(control);
            }
        });
    }

    onConfirm(event) {
        event.preventDefault();

        if (this.aziendaform.valid) {
            /*
                do somithing
            */
        } else {
            this.validateAllFormFields(this.aziendaform);
        }
    }

}

When I press confirm button the form is invalid but in the child component there are no error messages. It seems that markAsTouched does not propagate to child form.

Can someone help me?

update

I prepared a StackBlitz here: https://stackblitz.com/edit/angular-4qcsox

As you can see confirm and reset button does not affect child component.

1
can you add app-azienda-form.html as well? So you have a form and a child component where you want to display the error?Mac_W
I have updated my question. I have a form and a child component where I want to display the error.Pizzoni Alessio
So if you enter the textbox then leave it, but don't submit, does the error show?Learning2Code
Yes, the error appearsPizzoni Alessio
What about if you do nothing, then submit, then enter one of the other text boxes and leave it?Learning2Code

1 Answers

2
votes

If you step through validateAllFormFields() you will see that the indirizzoSpedizione remains null, the child controls are never wired up.

I got it working by building an empty form group in the parent component:

   this.aziendaform = this.fb.group({
        'indirizzoSpedizione':new FormGroup({})
    });

then having the child component accept a FormGroup as Input

export class AddressFormComponent implements OnInit, ControlValueAccessor, Validator {

@Input() addressForm: FormGroup;

and adding the controls via addControl

ngOnInit() {

    this.addressForm.addControl('via', new FormControl('', Validators.required));

The parent component then passes in the empty formGroup

<app-address-form  [addressForm]="aziendaform.controls['indirizzoSpedizione']"></app-address-form>

Here is a solution for the stackBlitz you put up https://stackblitz.com/edit/angular-stm9kw