Home > database >  conditional validation of Angular reactive forms
conditional validation of Angular reactive forms

Time:12-28

I am new to angular and working on reactive form.

I have a html table in which by looping through I have generated controls Table

I want to add validation based on following cases

  1. When this page loads than by default Save button should be disabled( which I have achieved by used [disabled]="!myform.valid"
  2. Save button should enable only when user enter value in any of text boxes or select check box in that particular row. Checkbox select and add value in text box should not allow to user. Either checkbox should be selected or user can enter value in any of text-boxes.

I tried this to achieve

IsFormValid(){return (!!this.myfm.get('myform').value);}// but this returning always true.

Here it my code

    myfm:FormGroup;
  ngOnInit() {
  debugger;
  this.myfm = this.fb.group({
  myform: this.fb.array([this.addformFileds()])
  }); 
}

 addformFileds(): FormGroup {
  return this.fb.group({
  NoTask: ['', Validators.required],// only check box is required either checkbox should click 
 or enter value in any text-boxes
                                    
  task1: ['', null],    
  task2: ['', null],
  task3: ['', null],
  task4: ['', null],
  task5: ['', null],
  task6: ['', null],
  task7: ['', null],
  task8: ['', null],
  task9: ['', null],
  task10: ['', null],
  task11: ['', null],
  task12: ['', null] 
});
}

 //adding more rows on click of Add More Rows button
addEntried():void{
this.idAddNewRow=true; //indicator that new rows is being created 
(<FormGroup>this.myfm.get('myform')).push(this.addEntried());
}

I know if is bit tricky but still didn't get solution for this. Any help of this will very helpful for me.

My component.html code

 <form #frmImp='NgForm' [formGroup]="myfm"]>
        <div >
        <div  style="align-items: center"> 
          <button type="button"  data-dismiss="modal" aria-label="Close">
            <span aria-hidden="true">&times;</span>
          </button>
        </div>
        <div >       
   <!--<table  style="overflow-x: true;"> to disable the scroll bar if screen resolution is less than 1920x1080-->
            <table  >
      <thead>
          <tr style="border-bottom-style: solid"><td id=""   colspan="15" style="text-align: center"><b>Imp Task Details</b></td>
          </tr> 
          <tr>
            <th style="text-align: center;"> </th>
            <th style="text-align: center;"> </th>
            <th style="text-align: center;">No Task</th>
            <th style="text-align: center;">Task 1</th>
            <th style="text-align: center;">Task 2</th>
            <th style="text-align: center;">Task 3</th>
            <th style="text-align: center;">Task 4</th>
            <th style="text-align: center;">Task 5</th>
            <th style="text-align: center;">Task 6</th>
            <th style="text-align: center;">Task 7</th>
            <th style="text-align: center;">Task 8</th>
            <th style="text-align: center;">Task 9</th>
            <th style="text-align: center;">Task 10</th>
            <th style="text-align: center;">Task 11</th>
            <th style="text-align: center;">Task 12</th> 
       
          </tr>
        </thead>
        <tbody formArrayName="myform" **ngFor="let frm of myfm.get('myform')['controls']; let i=index">
          <tr [[formGroupName]="i"]>
            <td></td>
            <td></td>
            <td><input [id]="'txt' i" type="checkbox" formControlName="NoTask" style="margin-left: 30px;"/></td> 
            <td  ><input [id]="'txt' i" type="tex"  formControlName="task1"   placeholder="0"></td>
            <td ><input [id]="'txt' i" type="tex"    formControlName="task2" ></td>
            <td ><input [id]="'txt' i" type="tex"    formControlName="task3" ></td>
            <td ><input [id]="'txt' i" type="tex"    formControlName="task4" ></td>
            <td ><input [id]="'txt' i" type="tex"    formControlName="task5" ></td>
            <td ><input [id]="'txt' i" type="tex"    formControlName="task6" ></td>
            <td ><input [id]="'txt' i" type="tex"    formControlName="task7" ></td>
            <td ><input [id]="'txt' i" type="tex"    formControlName="task8" ></td>
            <td ><input [id]="'txt' i" type="tex"    formControlName="task9" ></td>
            <td ><input [id]="'txt' i" type="tex"   formControlName="task10" ></td>
            <td ><input [id]="'txt' i" type="tex"   formControlName="task11" ></td>
            <td ><input [id]="'txt' i" type="tex"    formControlName="task12" ></td>  
          </tr>
           
        </tbody>
    </table> 
  </div>
        <div >
          <button type="button"  data-dismiss="modal">Close</button>
          <button type="submit"  *ngIf="isAvailable" (click)="addformFileds()">Add More Rows </button> 
          <button type="submit"  (click)=SaveImpDetails(frmImp)>Save </button> 
        </div>
      </div>
    </form>


**component.ts** file code
SaveImpDetails(){
   this.baseSrvc.post(ApiResource.SaveImpDetails, 
    JSON.stringify(body)).subscribe((result=>{
if(result){
   this.alertService.showMessage("Success!", "Details has been 
    recorded 
    successfuly.")
 }if(result.isError){
 this.alert.showMessage("Error!! while saving details");
}
  }));
   }
   

CodePudding user response:

Update:

After understanding the whole question, we could resolve it by the listening form changes and checking the form value on each change.

Here is the reproduction example(You can see the same on the Stackblitz):

  isFormValid$: Observable<boolean>;

  constructor(private fb: FormBuilder) {}

  ngOnInit() {
    this._generateFormAndInitListener();
  }

  // always call this one. for form generation
  private _generateFormAndInitListener() {
    this._generateForm();
    this._initListeners();
  }

  private _generateForm() {
    this.myfm = this.fb.group({
      myform: this.fb.array([this.addformFileds()]),
    });
  }

  private _initListeners() {
    this._initFormValidListener();
  }

  private _initFormValidListener() {
    this.isFormValid$ = this.myfm.valueChanges.pipe(
      debounceTime(200),
      map(() => !!singleWhere(this.myfm, (item) => item.value))
    );
  }

and the helper function


/**
 * find and return the first control for which
 * the `callback` function returns `true`
 *
 * loop over the `group` controls
 * and return the `control`
 * if the `callback` function returns `true` for the `control`
 *
 * @param group - is the form
 * @param callback - is the callback function
 *
 * @return `boolean`
 */
function singleWhere(
  group: AbstractControl,
  callback: (control: AbstractControl, index: number) => boolean
): AbstractControl {
  if (!group) {
    return null;
  }
  if (group instanceof FormControl) {
    return callback(group, 0) ? group : null;
  }

  const keys = Object.keys((group as FormGroup).controls);

  for (let i = 0; i < keys.length; i  ) {
    const control = group?.get(keys[i]);

    if (singleWhere(control, callback)) {
      return group;
    }
  }

  return null;
}

And finally in the template, add disable attribute by this condition

 [disabled]="!(isFormValid$ | async)"


try to disable your save button by checking the reactive form touched property. The form is touched when the form is changed by the user actions on the reactive form controls(typing, toggling).

Please NOTE: your checkboxes or inputs must be connected with your reactive forms by formControl or formControlName(If this example will not work for you, then please share the HTML code as well)

[disabled]="!form.touched" 
// or
[disabled]="form.untouched" 

Here is the reproduction example https://stackblitz.com/edit/angular-bootstrap-4-starter-4kmswc?file=src/app/app.component.ts

CodePudding user response:

Why not use a FormArray for the tasks? As you has a formArray and inside a formArray you should use two functions:

  //you can defined your formArray like
  formArray=this.fb.array([0,1,2,3,4,5,6,7].map(_=>this.addformFields()))

  getGroup(i)
  {
    return this.formArray.at(i) as FormGroup
  }

  tasks(i)
  {
    return this.getGroup(i).get('tasks') as FormArray
  }

Your function addformFields becomes like

  addformFields(): FormGroup {
    return this.fb.group({
      noTask:false,
      tasks:this.fb.array([0,1,2,3,4,5,6,7,8,9,10,11].map(_=>this.fb.control(0)))
    },{validator:this.someValue()})
  }

And the validator is like

  someValue(){
    return (control:AbstractControl)=>{
      const group=control as FormGroup;
      let valid=control.get('noTask').value;
      (control.get('tasks') as FormArray).controls.forEach(x=>{
        valid=valid ||  x.value!=0
      })
      return valid?null:{error:"You shoul indicate one task or that there' not task"}
    }
  }

The .html to control the FormArray

<table >
  <tr>
    <th>Section</th>
    <th *ngFor="let i of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]">Task {{ i }}</th>
  </tr>
  <tbody>
    <tr
      *ngFor="let group of formArray.controls; let i = index"
      [formGroup]="getGroup(i)"
    >
      <td><input type="checkbox" formControlName="noTask" /></td>
      <ng-container formArrayName="tasks">
        <td *ngFor="let control of tasks(i).controls; let j = index">
          <input
            [attr.disabled]="getGroup(i).get('noTask').value ? true : null"
            [formControlName]="j"
          />
        </td>
      </ng-container>
    </tr>
  </tbody>
</table>

See that if, e.g. you defined a .css like

tr.ng-invalid.ng-touched input{
  border:1px solid red;
}
tr.ng-invalid.ng-touched input:focus{
  border-color:   red;
  border-radius: .25rem;
  border-width: 2px;
  outline: 0;
  box-shadow: 0 0 0 .15rem rgba(255,0,0,.25);
}

A stackblitz

  • Related