טפסים באנגולר – המשך

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

הטופס נראה כך:

ואלו הקבצים:

קובץ card-form.component.ts

export class CardFormComponent {
  cardForm = new FormGroup({
    name: new FormControl('', [Validators.required, Validators.minLength(3)]),
    cardNumber: new FormControl(''),
    expiration: new FormControl(''),
    securityCode: new FormControl('')
  });

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

  formCcontrol(control){
    return control as FormControl
  }
}

קובץ card-form.component.html

<div class="container mt-5 mb-5">
  <form [formGroup]="cardForm">
    <app-input [control]="formCcontrol(cardForm.get('name'))" [label]="'Name'"></app-input>
    <app-input [control]="formCcontrol(cardForm.get('cardNumber'))" [label]="'Card Number'">
    </app-input>
    <app-input [control]="formCcontrol(cardForm.get('expiration'))" [label]="'Expiration Date'">
    </app-input>
    <app-input [control]="formCcontrol(cardForm.get('securityCode'))" [label]="'Security Code'">
    </app-input>
  </form>

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

קובץ input.component.ts

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

קובץ input.component.html

<div class="mb-3">
  <label class="form-label">{{ label }}</label>
  <input [formControl]="control" class="form-control"/>
</div>

<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>

הגשת הטופס – Submit

נוסיף לטופס את כפתור ה-submit. נרצה פעולה שתפעל כאשר לוחצים על הכפתור וגם נרצה לאפשר את הפעולה שלו רק כשכל השדות יהיו תקינים.

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

קובץ card-form.component.html

<form [formGroup]="cardForm" (ngSubmit)="onSubmit()">
    <app-input [control]="formCcontrol(cardForm.get('name'))" [label]="'Name'"></app-input>
    <app-input [control]="formCcontrol(cardForm.get('cardNumber'))" [label]="'Card Number'">
    </app-input>
    <app-input [control]="formCcontrol(cardForm.get('expiration'))" [label]="'Expiration Date'">
    </app-input>
    <app-input [control]="formCcontrol(cardForm.get('securityCode'))" [label]="'Security Code'">
    </app-input>
    <button
      type="submit"
      class="btn btn-primary"
      [disabled]="cardForm.invalid">Submit</button>
  </form>

קובץ card-form.component.ts

export class CardFormComponent {
  cardForm = new FormGroup({
    name: new FormControl('', [
      Validators.required,
      Validators.minLength(3)
    ]),
    cardNumber: new FormControl('', [
      Validators.required,
      Validators.minLength(16),
      Validators.maxLength(16)
    ]),
    expiration: new FormControl('', [
      Validators.required,
      Validators.pattern(/^(0[1-9]|1[0-2])\/\d{2}$/)
    ]),
    securityCode: new FormControl('', [
      Validators.required,
      Validators.minLength(3),
      Validators.maxLength(3)
    ])
  });

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

  formCcontrol(control){
    return control as FormControl
  }

  onSubmit(){
  }
}

מסיכה – Masking

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

דרך אחת יכולה להיות להראות את הפורמט ב-label של השדה.

<app-input 
    [control]="formCcontrol(cardForm.get('expiration'))" 
    [label]="'Expiration Date (MM/YY)'">
</app-input>

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

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

ng g class common/DateFormControl

הפונקציה setValue של class FormControl קןלטת כשיש שינויים בתוכן הפקד. ערך ה-input נלקח ונשלח לפונקציה הזאת. אנחנו נדרוס את הפונקציה ונכתוב לה קוד חדש. כדי לא להפסיד את מה שהפונקציה setValue צריכה לעשות, נקרא לה בעצמה בתוך הפונקציה שאנחנו דורסים.

קובץ data-form-control.ts

import { FormControl } from "@angular/forms";

export class DateFormControl extends FormControl{
  override setValue(value: string, options: any){
    super.setValue(value, options);
  }
}

נחליף את ה-FormControl של תוקף הכרטיס ב-class החדש שיצרנו.

קובץ card-form.component.ts

expiration: new DateFormControl('', [
      Validators.required,
      Validators.pattern(/^(0[1-9]|1[0-2])\/\d{2}$/)
]),

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

הפונקציה setValue המקורית נראית ככה:

setValue(value: TValue, options?: {
        onlySelf?: boolean;
        emitEvent?: boolean;
        emitModelToViewChange?: boolean;
        emitViewToModelChange?: boolean;
}): void;

אחת האפשרויות שהפונקציה מקבלת היא: emitModelToViewChange שאומרת שבכל פעם כשאנחנו משנים את ערך ה-input דרך הפונקציה setValue נעדכן גם את התצוגה של הפקד, אחרת כל שינוי שנעשה יקלט בפקד, אבל המשתמש לא יראה את זה.

למשל, אם נגדיר את setValue כך:

export class DateFormControl extends FormControl{
  override setValue(value: string, options: any){
    super.setValue(value + *, options);
  }
}

לכל ערך של ה-input תתוסף * בסוף, אבל זה לא יהיה שקוף למשתמש.

אם נשתמש באפשרות של emitModelToViewChange נראה על המסך אחרי כל קלט את ה-* כתוספת.

export class DateFormControl extends FormControl{
  override setValue(value: string, options: any){
    super.setValue(value + '*', {...options, emitModelToViewChange: true});
  }
}

יש כמה דברים אנחנו רוצים לטפל בהם. מקרה אחד הוא הקלדה של תווים לא רצויים, למשל אני לא רוצה אותיות בשדה של התוקף.

// If the user is trying to enter a value that doesn't
//  match the pattern, revert to the previous value
if(value.match(/[^0-9|\/]/gi)){
  super.setValue(this.value, {...options, emitModelToViewChange: true});
  return;
}

הדבר הבא הוא לדאוג שלא יהיו יות5 מ-5 תווים. 2 לחודש, 2 לשנה ו-/ ביניהם.

if(value.length > 5){
  super.setValue(this.value, {...options, emitModelToViewChange: true});
  return;
}

עכשיו נשים את התו / אחרי 2 הספרות הראשונות.

if(value.length === 2){
  super.setValue(value + '/', {...options, emitModelToViewChange: true});
  return
}

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

if(value.length === 2 && this.value.length === 3){
  super.setValue(value, {...options, emitModelToViewChange: true});
  return;
}

כל הקוד של ה-class נראה ככה.

קובץ data-form-control.ts

import { FormControl } from "@angular/forms";

export class DateFormControl extends FormControl{
  override setValue(value: string, options: any){
    // If the user is trying to enter a value that doesn't match the pattern,
    // revert to the previous value
    if(value.match(/[^0-9|\/]/gi)){
      super.setValue(this.value, {...options, emitModelToViewChange: true});
      return;
    }

    if(value.length > 5){
      super.setValue(this.value, {...options, emitModelToViewChange: true});
      return;
    }

    if(value.length === 2 && this.value.length === 3){
      super.setValue(value, {...options, emitModelToViewChange: true});
      return;
    }

    if(value.length === 2){
      super.setValue(value + '/', {...options, emitModelToViewChange: true});
      return
    }
  }
}

ספריית מסכה

יש אפשרות להשתמש בספריית מסיכות מוכנה. ספריית ngx-mask היא אחת כזאת.

איפוס טופס – Reset

הדבר הבא להוסיף לטופס הוא כפתור שיאפס אותו וינקה את הערכים שמוכנסים. נוסיף את הכפתור לטופס.

קובץ card-form.component.html

<button
      type="button"
      class="btn btn-warninh"
      (click)="onResetClick()">Reset
</button>

נייצר את הפונקציה שמטפלת באיפוס. נקבל reference לאובייקט הטופס שבתוכו יש reset.

קובץ card-form.component.ts

onResetClick(){
    this.cardForm.reset();
}

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

קובץ data-form-control.ts

override setValue(value: string | null, options: any){
    if(!value){
      super.setValue('', {...options, emitModelToViewChange: true});
      return;
    }

    // If the user is trying to enter a value that doesn't match the pattern,
    // revert to the previous value
    if(value.match(/[^0-9|\/]/gi)){
      super.setValue(this.value, {...options, emitModelToViewChange: true});
      return;
    }

    if(value.length > 5){
      super.setValue(this.value, {...options, emitModelToViewChange: true});
      return;
    }

    if(value.length === 2 && this.value.length === 3){
      super.setValue(value, {...options, emitModelToViewChange: true});
      return;
    }

    if(value.length === 2){
      super.setValue(value + '/', {...options, emitModelToViewChange: true});
      return
    }
  }

ניווט במאמר

מאמרים אחרונים

Weekly Tutorial