Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular 2 - using ngControl with optional fields

I'm having a hard time trying to use both *ngIf inside a form, and ngFormModel to validate said form.

Use case is this : based on user input, hide or deactivate some specific fields in the form. But in case these inputs are shown, they must be validated.

When only basic validation is required, I can make do one way or another :

  • If the input is only required, it works A-OK using ngControl and required
  • If a specific format is necessary, it's possible to use the pattern attribute. It's not Angular, but it works.

But in order to implement more complex validations, I've been trying to use ngControl coupled to ngFormModel so as to use custom checks. I used pieces of code found on the following pages :

How to add form validation pattern in angular2 (and the links referenced there)

Angular2 Forms :Validations, ngControl, ngModel etc

My code is as follows :

HTML

<div>
  <h1>Rundown of the problem</h1>
  <form (ngSubmit)="submitForm()" #formState="ngForm" [ngFormModel]="myForm">
    <div class="checkbox">
      <label>
        <input type="checkbox" [(ngModel)]="model.hideField" ngControl="hideField"> Is the input below useless for you ?
      </label>
    </div>

    <div *ngIf="!model.hideField">
      <div class="form-group">
        <label for="optionalField">Potentially irrelevant field </label>
        <input type="text" class="form-control" [(ngModel)]="model.optionalField" ngControl="optionalField" required #optionalField="ngForm">
        <div [hidden]="optionalField.valid || optionalField.pristine" class="alert alert-warning">
          This input must go through myCustomValidator(), so behave.
        </div>
      </div>
    </div>

    <button type="submit" class="btn btn-primary" [disabled]="!formState.form.valid">I can't be enabled without accessing the input :(</button>
    <button type="submit" class="btn btn-default">Submit without using form.valid (boo !)</button>
  </form>
</div>

Typescript

import {Component, ChangeDetectorRef, AfterViewInit } from 'angular2/core';
import {NgForm, FormBuilder, Validators, ControlGroup, FORM_DIRECTIVES}    from 'angular2/common';

@Component({
  selector: 'accueil',
  templateUrl: 'app/accueil.component.bak.html',
  directives:[FORM_DIRECTIVES],
  providers: [FormBuilder]
})
export class AccueilComponent implements AfterViewInit {

  private myForm: ControlGroup;
  model: any;

  constructor(fb: FormBuilder, cdr: ChangeDetectorRef) {
    this.cdr = cdr ;

    this.model = {} ;
    this.myForm = fb.group({
      "hideField": [false],
      "optionalField": [this.model.optionalField, Validators.compose([this.myCustomValidator])]
    });
  }

  ngAfterViewInit() {
    // Without this, I get the "Expression has changed after it was checked" exception.
    // See also : https://stackoverflow.com/questions/34364880/expression-has-changed-after-it-was-checked
    this.cdr.detectChanges();
  }

  submitForm(){
    alert("Submitted !");
  }

  myCustomValidator(optionalField){
    // Replace "true" by "_someService.someCheckRequiringLogicOrData()"
    if(true) {
      return null;
    }

    return { "ohNoes": true };
  }
}

Even if the input is removed from the template with *ngIf, the constructor is still referencing the control. Which in turns prevents me from using the [disabled]="!formState.form.valid", as myForm is understandably INVALID.

Is what I am aiming at possible using Angular 2 ? I'm sure this is not that uncommon a use case, but then again with my current knowledge, I cannot see how I could make it work.

Thanks !

like image 429
phl Avatar asked Mar 01 '16 16:03

phl


2 Answers

What you can try to do is reset the validators on your control. Which means, when you want a new set of validators to be bound to a control because of a state change, you would redefine the validator function.

In your case, when your checkbox is checked/unchecked you want the following to occur:

  1. Set the input as optional (not required), but still validate against your custom validator.
  2. Revert the control's validators back to their original state, when the checkbox is unchecked.
  3. Validate the control again so that form.valid is updated.

See my plnkr sample based on Angular.io's Forms Guide

if (optional)
   this.heroFormModel.controls['name'].validator = Validators.minLength(3);
else
   this.heroFormModel.controls['name'].validator =
        Validators.compose([Validators.minLength(3), Validators.required]);

this.heroFormModel.controls['name'].updateValueAndValidity();
like image 142
Alex Avatar answered Nov 17 '22 06:11

Alex


I just ran into exactly the same problem, and found a workaround that relies on manually including and excluding the controls:

import {Directive, Host, SkipSelf, OnDestroy, Input, OnInit} from 'angular2/core';
import {ControlContainer} from 'angular2/common';

@Directive({
  selector: '[ngControl]'
})
export class MyControl implements OnInit, OnDestroy {
  @Input() ngControl:string;
  constructor(@Host() @SkipSelf() private _parent:ControlContainer) {}

  ngOnInit():void {
    // see https://github.com/angular/angular/issues/6005
    setTimeout(() => this.formDirective.form.include(this.ngControl));
  }

  ngOnDestroy():void {
    this.formDirective.form.exclude(this.ngControl);
  }

  get formDirective():any {
    return this._parent.formDirective;
  }
}

To make this work, all dynamic controls have to be excluded from the form initially; see the plunkr for details.

like image 45
Christian Zosel Avatar answered Nov 17 '22 07:11

Christian Zosel