0

I'm having trouble with conditional validation using template forms in Angular. I've created a custom EmailInputComponent:

<div class="form-group" provide-parent-form>
  <label for="email">Email<span *ngIf="required">*</span></label>
  <input id="email"
         class="form-control"
         name="email"
         type="email"
         [(ngModel)]="emailAddress"
         #email="ngModel"
         maxlength="255"
         validateEmail
         [required]="required ? '' : null"/>
  <error [model]="email" [referencedValue]="emailAddress"></error>
</div>

which is hosted inside a parent MyFormComponent:

<form #form="ngForm" name="form" (ngSubmit)="onSubmit($event)">
  <fieldset>
    <email [(emailAddress)]="model.email" [required]="emailRequired()"></email> 
    <!-- select component here -->
  </fieldset>    
  <button type="submit" [disabled]="!form.form.valid">Send</button>
</form>

The form also contains a SelectComponent where users can choose their preferred way of contact. If users select "email", the email input becomes mandatory.

As you can, see there is some logic going on in the parents emailRequired function that dynamically calculates whether an email input is mandatory or not based on the currently selected preferred way of contact. Whenever this selected value changes I need to somehow trigger the email input validators. How can I do that?

Using @ViewChild I managed to get a hold of the EmailInputComponent from the MyFormComponent. But I don't know how to proceed now...

Robert
  • 1,710
  • 2
  • 18
  • 35
  • 1
    Have you tried using [AbstractControl#updateValueAndValidity](https://angular.io/api/forms/AbstractControl#updateValueAndValidity)? Since you're using `required` as an `@Input` you might want to to add this to the child component's `ngOnChanges` lifecycle hook. – naeramarth7 Oct 04 '17 at 18:39
  • This has nothing to do with your question, but I would suggest you consider using a reactive form, this would be easier to handle that way. Just a kind suggestion :) – AT82 Oct 04 '17 at 18:47
  • Is your `EmailInputComponent` implementing ControlValueAccessor? See [this answer](https://stackoverflow.com/questions/46413347/how-to-implement-automatic-form-validation-in-form-with-nested-component/46420972#46420972) for more details – Garth Mason Oct 05 '17 at 03:19
  • @GarthMason: No, I used the workaround from https://github.com/angular/angular/issues/9600#issuecomment-317774127 – Robert Oct 06 '17 at 11:30
  • @AJT_82 I don't like all the boiler plate TS code that comes with reactive forms. That's why I want to stick to template driven ones. – Robert Oct 06 '17 at 11:31
  • @naeramarth7 Thanks for the hint. I've seen that before, however it was unclear to me how to get a reference to the actual form control in order to call it. Meanwhile I found a way (cp. my solution below). – Robert Oct 06 '17 at 11:33
  • @Robert, well under the hood of templatedriven forms we are using formcontrols, formgroups etc tho, but well, but it was only a suggestion, so you go with whatever you feel like :) – AT82 Oct 06 '17 at 12:04
  • @Robert - thanks for the link, hadn't seen that workaround. – Garth Mason Oct 09 '17 at 00:27

2 Answers2

0

I don't think you need to specify attr in front to refer to the required property.

Try the simple,

 [required]="required ? '' : null"

And if it still didn't work, it is likely because of the '@Input' required property not getting updated in the EmailInputComponent. So as @naeramarth7 suggested look for it in the ngOnChanges hook of the EmailInputComponent and update.

ngOnChanges(changes: SimpleChange) {
 for (let prop in changes) {
  if(prop === 'required') { 
   this.required = changes[prop].currentValue;
  }
 }
}

make sure, when you use it, to add implements OnChanges to the EmailInputComponent declaration and also to import SimpleChange and OnChanges from '@angular/core' inside the email-input.component.ts

amal
  • 3,140
  • 1
  • 13
  • 20
  • Thanks for your post. You're right - I don't need the attr prefix. I edited my initial post. However, this is not my point. Binding to the required attribute works well. What I really want to achieve is to trigger the email component validators. – Robert Oct 06 '17 at 07:35
0

Meanwhile I found a solution. My child component now references the according FormGroup.

<email [(emailAddress)]="model.email" 
       [formGroup]="form.form" 
       [required]="emailRequired()">
</email>

Via the FormGroup, you can then reach to the child components actual FormControl and call updateValueAndValidity inside the setter of the validator flag (set required in my case):

import {Component, EventEmitter, Input, Output} from '@angular/core';
import {FormGroup} from '@angular/forms';

@Component({
  selector: 'email',
  templateUrl: './email.component.html',
  styleUrls: ['./email.component.css']
})
export class EmailInputComponent {

  private _emailAddress: string;

  public _required = true;

  @Input()
  private formGroup: FormGroup;

  @Output()
  emailAddressChange = new EventEmitter();

  constructor() { }

  @Input()
  get emailAddress(): string {
    return this._emailAddress;
  }

  set emailAddress(value: string) {
    this._emailAddress = value;
    this.emailAddressChange.emit(this._emailAddress);
  }

  @Input()
  get required(): boolean {
    return this._required;
  }

  set required(value: boolean) {
    this._required = value;
    // this is where the magic happens
    const refEmailControl = this.formGroup.controls.email;
    if (refEmailControl) {
      refEmailControl.updateValueAndValidity(); // updates the validity state      
      refEmailControl.markAsTouched(); // necessary in my case to make UI render error message
    }
  }
}
Robert
  • 1,710
  • 2
  • 18
  • 35