ולידציה ושגיאות ב-Reactive Forms

שימוש בטפסים מחייב הרבה פעמים אימות והצגת שגיאות. את זה נראה במאמר הבא.

המאמר הוא המשך לחלק הקודם של טפסים באנגולר.

יצרנו טופס פשוט עם שדה אחד והדפסנו למסך נתונים על הטופס.

כרגע אנגולר מסמן לנו שהטופס תקין מכיוון שאין לו חוקים שהוא צריך לעמוד בהם ולכן לא משנה מה נכניס הטופס יהיה תקין. נכניס לטופס הגבלות ונראה איך אנגולר מתייחס אליהן. נייבא את ספריית Validators.

הוספת Validators לטופס

קובץ card-form.component.ts

import { FormGroup, FormControl, Validators } from '@angular/forms'

כמשתנה שני ל-FormControl שלנו נעביר מערך ובתוכו נכניס את התנאים שאנחנו רוצים שהשדה יקיים. למשל Validators.required יוודא שהמשתמש הכניס ערך לשדה.

cardForm = new FormGroup({
    name: new FormControl('', [Validators.required])
});

כדי לראות רשימה שלימה של ה-Validators הזמינים, אפשר ללחוץ Ctrl+קליק על אובייקט Validators. למשל אנחנו יכולים לקבוע ערך מינימלי ומקסימלי לערך הטופס, לוודא שהערך שהוכנס הוא email ועוד. נוודא שהשם שהמשתמש מכניס הוא בעל לפחות 3 תווים.

cardForm = new FormGroup({
    name: new FormControl('', [
      Validators.required,
      Validators.minLength(3)
    ])
});

קבלת שגיאות מהטופס

כרגע אם משתמש מכניס פחות מ-3 תווים לשדה, התטופס לא תקין, אבל המשתמש לא יודע למה הוא לא תקין. את השגיאה צריך להציג למשתמש על מנת שיוכל להכניס שדה תקין. כדי לראות מה קורה בשדה שלנו, נדפיס למסך את השגיאות של השדה.

קובץ card-form.component.html

<div>Form Contents: {{ cardForm.value | json }}</div>
<div>Form is valid: {{ cardForm.valid }}</div>
<div>Error of name: {{ cardForm.controls.name.errors | json }}</div>

נראה את הדפסת השגיאה של השדה.

מה שנקבל זה שם ה-validator וזה שהערך שלו הוא true. אין מחרוזת שגיאה להציג. ברגע שנכניס ערך לשדה, וה-validator של required קבל את מה שהוא רצה נקבל את השגיאות הבאות, במקרה שלנו של אורך שדה מינימלי.

אחרי שנספק את כל הדרישות, ערך אובייקט השגיאות יהיה null.

הצגת שגיאות למשתמש

כדי להציג את השגיאות נוסיף טקסט שמתאר את השגיאה. אנחנו רוצים להציג את הטקסט הזה רק אם המשתמש לא עומד בתנאי שהצבנו לשדה. בדוגמא, אם יש לנו את הערך required באובייקט error, אנחנו רוצים להציג את השגיאה למשתמש.

יש אפשרות לבדוק עם ngIf האם השגיאה קיימת ולפי זה להציג את ה-div. מכיוון שאני רוצה להציג שגיאה מובנית של bootstrap אשתמש בבדיקה האם יש שגיאה כדי לשים את ה-class המתאים לשדה. שדה עם class של is-invalid יציג את השגיאה שאחריו.

<form [formGroup]="cardForm">
    <input formControlName="name"
    [ngClass]="{ 'is-invalid': cardForm.controls.name.errors?.required }"/>
    
    <div class="invalid-feedback">
      Please enter a value.
    </div>
</form>

יש כמה שגיאות שיכולות לקרות עם השדה הזה בבדיקות של TypeScript. הדרך הפשוטה לפתור אותן זה על ידי שינוי הדרך שבה TS בודק את הקבצים. בקובץ tsconfig.json שינוי ערכים של noPropertyAccessFromIndexSignature, strict, strictTemplates ל-false ישחרר את TS מבדיקה של השדות. לא בטוח שזאת הדרך הכי טובה לעשות את זה, אם יש לכם הצעות אחרות תציעו בתגובות.

סימן השאלה אחרי errors בא כדי להגיד לאנגולר לבדוק השדה רק אם יש errors. זה כדי למנוע את הפעלת בדיקה required במקרה שאין שגיאות ואז האובייקט מחזיר null.

זה עובד טוב במקרה שיש שגיאה אחת, במקרה שלנו שיש לי גם שגיאה של required וגם של אורך מינימלי, אני אשתמש ב-ngIf ואשנה את הקוד כך:

<form [formGroup]="cardForm">
    <input formControlName="name" />

    <ng-container *ngIf="cardForm.controls.name.errors">
      <p class="text-danger" *ngIf="cardForm.controls.name.errors.required">
        Please enter a value.
      </p>
      <p class="text-danger" *ngIf="cardForm.controls.name.errors.minlength">
        Please enter at least 3 characters.
      </p>
    </ng-container>
</form>

הסתרת שגיאות

בקוד שיש לנו עכשיו, נרגע שנכנסים לטופס יש לנו הצגה של השגיאה. בדרך כלל נרצה שהשגיאה תופיע אחרי שהמשתמש התחיל להכניס את המידע לשדה של הטופס. כדי לראות עוד תכונות שיש ל-formControl שלנו אפשר להציג אותו בקונסול.

קובץ card-form.component.ts

constructor(){
    console.log(this.cardForm.controls.name);
}

בתוך _rawValidators יש לנו כל מיני ערכים כמו valid שאומר לנו האם הטופס valid. ה-validators שהוספנו עד עכשיו הם סינכרונים, הם באים מהצד של הלקוח ויכולים להעריך מיידית האם הערך שלהם הוא true או false. כל פעם שהמשתמש משנה את הערך, יש הערכה מיידית של השדה.

יש גם validators א-סינכרונים,שמשתמשים ב-API כדי להעריך את הנכונות שלהם. למשל אם משתמש נרשם לאתר עם שם משתמש ואנחנו רוצים לבדוק האם שם המשתמש ייחודי, נצטרך לפנות ל-DB כדי לבדוק את זה. הפעולה הזאת היא א-סינכרונית. פעולה כזאת לוקחת זמן, ובינתיים כשה-validator הזה עושה את העבודה שלו, התכונה pending תקבל את הערך true.

תכונה נוספת ששימושית עבורנו היא touched, הערך שלה יהיה true אם משתמש הקליק על השדה ויצא ממנו. את מסר השגיאה נראה להציג אם משתמש הכניס ערך ויצא מהשדה.

קובץ card-form.component.html

<ng-container *ngIf="cardForm.controls.name.touched && cardForm.controls.name.errors">
      <p class="text-danger" *ngIf="cardForm.controls.name.errors.required">
        Please enter a value.
      </p>
      <p class="text-danger" *ngIf="cardForm.controls.name.errors.minlength">
        Please enter at least 3 characters.
      </p>
</ng-container>

אפשר למצוא את הפירוט של כל האפשרויות בקישור: Angular AbstractControl.

שימוש חוזר בשדות טפסים

הבעיה עם מה שעשינו עד עכשיו היא שזה עובד, אבל עבור כל שדה נצטרך לכתוב את כל הקוד הזה. במקום לשכפל את הקוד נבנה קומפוננטה לשימוש רב פעמי. ניצור קומפוננטה חדשה ונקרא לה Input. את כל קוד ה-html ששייך לשדה ה-input נעביר לקובץ הקומפוננטה שיצרנו.

קובץ input.component.html

<input formControlName="name" />

<ng-container
  *ngIf="cardForm.controls.name.touched && cardForm.controls.name.errors"
>
  <p class="text-danger" *ngIf="cardForm.controls.name.errors.required">
    Please enter a value.
  </p>
  <p class="text-danger" *ngIf="cardForm.controls.name.errors.minlength">
    Please enter at least 3 characters.
  </p>
</ng-container>

נקבל שגיאות מכיוון שאין לנו גישה לאובייקט הטופס. מעבר לזה יש לנו חיבור בין השם formControlName ל-formControl שלו ב-class של הטופס ועכשיו הם בשתי קומפוננטות נפרדות.

צעד ראשון, נקרא לקומפוננטת השדה בתוך הטופס.

קובץ card-form.component.html

<form [formGroup]="cardForm">
    <app-input></app-input>
</form>

עכשיו נעביר את ה-formControl המתאים לקומפוננטה.

<form [formGroup]="cardForm">
    <app-input [control]="cardForm.get('name')"></app-input>
</form>

עכשיו קומפוננטת ה-Input צריכה לקבל את ה-formControl כקלט.

קובץ input.component.ts

export class InputComponent {
  @Input() control: FormControl;
}

עכשיו צריך לסדר את התבנית כך שהמידע יתקבל מתוך המשתנה שהעברנו. אם נעזוב לרגע את השגיאות ונשים אותם בהערה, ככה נעביר את הפקד לטופס.

קובץ input.component.html

<input [formControl]="control" />

יתכן שתתקבל שגיאה של casting. הפתרון שאני מצאתי הוא להוסיף פונקציית casting בקובץ card-form ולשלוח את ה-formControl אחרי המרה בצורה הזאת.

קובץ card-form.component.ts

formCcontrol(control){
    return control as FormControl
}

קובץ card-form.component.html

<form [formGroup]="cardForm">
    <app-input [control]="formCcontrol(cardForm.get('name'))"></app-input>
</form>

עכשיו נשאר לסדר את הקריאה להודעות השגיאה.

קובץ input.component.html

<input [formControl]="control" />

<ng-container *ngIf="control.touched && control.errors">
  <p class="text-danger" *ngIf="control.errors.required">
    Please enter a value.
  </p>
  <p class="text-danger" *ngIf="control.errors.minlength">
    Please enter at least {{ control.errors.minlength.requiredLength }} characters.
  </p>
</ng-container>

עכשיו הכל עובד.

מכיוון שזהו רכיב שאנחנו הולכים להשתמש בו בכל פעם כשנרצה להוסיף תיבת טקסט, נרצה להתייחס לכל השגיאות האפשריות בקומפוננטת ה-input.

קובץ input.component.html

<input [formControl]="control" />

<ng-container
  *ngIf="control.touched && control.errors"
>
  <p class="text-danger" *ngIf="control.errors.required">
    Please enter a value.
  </p>
  <p class="text-danger" *ngIf="control.errors.minlength">
    Please enter at least {{ control.errors.minlength.requiredLength }} characters.
  </p>
  <p class="text-danger" *ngIf="control.errors.maxlength">
    Please enter maximum of {{ control.errors.maxlength.requiredLength }} characters.
  </p>
  <p class="text-danger" *ngIf="control.errors.pattern">
    Invalid format.
  </p>
</ng-container>

אפשר כמובן להוסיף טיפול בשגיאות נוספות, להכביר משתנה Input עם label לפקד, אפשר לשים מסגרת אדומה לפקד אם הוא לא valid ועוד.