Search code examples
javascriptangulartypescriptangular-reactive-formsangular-forms

Angular Reactive form validation and data formation issue


Trying to add validation at least to select minimum one item from mat-select multipe option. Currently showing error message on page load until user selects one item from select option, which is working fine.

expectation:
When i select all or single select, error message should disappear. (expected and working fine).

But what happening:
Required error message is not showing when i deselect the selected single item.

Don't know what I'm doing wrong.

skillForm.component.ts

skillList = [
    { skillId: '0', skillName: 'JS' },
    { skillId: '1', skillName: 'TS' },
    { skillId: '2', skillName: 'JAVA' },
    { skillId: '3', skillName: '.Net' },
];

@ViewChild('pickAllCourse') private pickAllCourse: MatOption;
trainerForm = FormGroup;

constructor(public formBuilder: FormBuilder) { }

this.trainerForm = this.formBuilder.group({
    selectedSkills :['', Validators.required, Validators.minLength(1)]
})

pickAll(): void {
    if(this.pickAllCourse.selected) {
    this.trainerForm.controls.selectedSkills.patchValue([...this.skillList.map((item) => item.deviceId), 0]);
    } else {
        this.trainerForm.controls.selectedSkills.patchValue([]);
    }
}


selectOneItem(all): any {
    if (this.pickAllCourse.selected) {
        this.pickAllCourse.deselect();
        return false;
    }
    if (this.trainerForm.controls.selectedSkills.value.length === this.skillList.length) {
        this.pickAllCourse.select();
    }
}

onSubmit(): void{
    console.log('form value', this.trainerForm.value)
    
    // 
}

skillForm.component.html

    <mat-form-field class="selectedSkills">
        <mat-select multiple ngDefaultControl formControlName="selectedSkills"
            placeholder="Select Device Type">
            <mat-option #pickAllCourse (click)="pickAll()" [value]="0">All</mat-option>
            
        <mat-option *ngFor="let i of skillList" [value]="i.deviceId"
            (click)="selectOneItem(pickAllCourse.viewValue)">{{ i.skillName }}
        </mat-option>
        
        </mat-select>
<span class="text-danger" *ngIf="trainerForm.controls['selectedSkills '].invalid ">This field is required</span>
    </mat-form-field>

Additionally, i need help on how to construct the object like below when submit the form for backend.

skillList: [
    {skillId: '0'},
    {skillId: '1'}
];

when i do console.log the this.trainerForm.value, I'm seeing skillList: ['0']


Solution

  • Issue(s) & Concern(s)

    Issue 1:

    There is typo error for extra spacing on trainerForm.controls['selectedSkills '].

    <span class="text-danger" *ngIf="trainerForm.controls['selectedSkills '].invalid ">This field is required</span>
    

    Issue 2:

    If the form control(s) requires multiple Validators, you should group them with an Array.

    this.trainerForm = this.formBuilder.group({
      selectedSkills :['', Validators.required, Validators.minLength(1)]
    })
    

    Change to:

    this.trainerForm = this.formBuilder.group({
      selectedSkills :['', [Validators.required, Validators.minLength(1)]]
    })
    

    Concern 1

    From HTML and Typescript part, the selectedSkills will return an array of number, but not an array of Object. As you use item.deviceId (return string) and deviceId is not exist in Object for skillLists. I assume that you are using item.skillId.

    pickAll(): void {
      if(this.pickAllCourse.selected) {
        this.trainerForm.controls.selectedSkills.patchValue([...this.skillList.map((item) => item.deviceId), 0]);
      } else {
        this.trainerForm.controls.selectedSkills.patchValue([]);
      }
    }
    
    <mat-option *ngFor="let i of skillList" [value]="i.deviceId"
        (click)="selectOneItem(pickAllCourse.viewValue)">{{ i.skillName }}
    </mat-option>
    

    Hence, when you console.log(this.trainerForm.value), it will display:

    { selectedSkills: [1, 2, 3] }
    

    Solution

    1. For <mat-option> generated with *ngFor, set [value]="{ skillId: i.skillId }" to return selected value as object.
    2. Add compareWith for your <mat-select>. Purpose for comparing this.trainerForm.controls.selectedSkills with [value]="{ skillId: i.skillId }" to check/uncheck the options when select/deselect All.
    <mat-form-field class="selectedSkills">
        <mat-select
          multiple
          ngDefaultControl
          formControlName="selectedSkills"
          placeholder="Select Device Type"
          [compareWith]="compareFn"
        >
          <mat-option #pickAllCourse (click)="pickAll()" [value]="0"
            >All</mat-option
          >
    
          <mat-option
            *ngFor="let i of skillList"
            [value]="{ skillId: i.skillId }"
            (click)="selectOneItem(pickAllCourse.viewValue)"
            >{{ i.skillName }}
          </mat-option>
        </mat-select>
        <span
          class="text-danger"
          *ngIf="trainerForm.controls['selectedSkills'].invalid"
          >This field is required</span
        >
    </mat-form-field>
    
    1. Set multiple Validators in array [] as mentioned in Issue 2.
    2. pickAll() to patchValue for this.trainerForm.controls.selectedSkills as { skillId: item.skillId } Object.
    3. onSubmit() before pass the form value to API, make sure you filter skillSets value with { skillId: item.skillId } Object only.
    4. compareFn is for comparing the selected skillSets value with each <mat-option> value. Hence, when Select All, all the <mat-option> will be selected and vice versa as (2).
    trainerForm: FormGroup;
    
    ngOnInit() {
      this.trainerForm = this.formBuilder.group({
        selectedSkills: ['', [Validators.required, Validators.minLength(1)]],
      });
    }
    
    pickAll(): void {
      if (this.pickAllCourse.selected) {
        this.trainerForm.controls.selectedSkills.patchValue([
          ...this.skillList.map((item) => {
            return {
              skillId: item.skillId
            };
          }),
          0,
        ]);
      } else {
        this.trainerForm.controls.selectedSkills.patchValue([]);
      }
    }
    
    onSubmit(): void {
      console.log('form value', this.trainerForm.value);
    
      let postFormValue: any = {
        ...this.trainerForm.value,
        selectedSkills: this.trainerForm.value.selectedSkills.filter(
          (x: any) => typeof x === 'object'
        ),
      };
    
      console.log(postFormValue);
    }
    
    compareFn(obj1: any, obj2: any): boolean {
      return obj1 && obj2 ? obj1.skillId === obj2.skillId : obj1 === obj2;
    }
    

    Sample Solution on StackBlitz