phl
phl

Reputation: 342

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 :

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 !

Upvotes: 13

Views: 7748

Answers (3)

Jesse Sanders
Jesse Sanders

Reputation: 521

Here is an updated version for RC4. I also renamed it to npControl for my purposes.

import { Directive, Host, OnDestroy, Input, OnInit } from '@angular/core';
import { ControlContainer } from '@angular/forms';

@Directive({
  selector: '[npControl]'
})
export class NPControlDirective implements OnInit, OnDestroy {
  @Input() npControl: string;

  constructor(@Host() private _parent: ControlContainer
  ) { }

  ngOnInit(): void {
    console.log('include ', this.npControl);
    setTimeout(() => this.formDirective.form.include(this.npControl));
  }

  ngOnDestroy(): void {
    console.log('exclude ', this.npControl);
    this.formDirective.form.exclude(this.npControl);
  }

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

Upvotes: 0

Christian Zosel
Christian Zosel

Reputation: 1424

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.

Upvotes: 2

Alex
Alex

Reputation: 546

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();

Upvotes: 8

Related Questions