در مطلب «
فرمهای مبتنی بر قالبها در Angular - قسمت چهارم - اعتبارسنجی ورودیها» مشاهده کردیم که Angular در روش فرمهای مبتنی بر قالبها، تنها از 4 روش بومی اعتبارسنجی مرورگرها مانند ذکر ویژگی required برای فیلدهای اجباری، ویژگیهای minlength و maxlength برای تعیین حداقل و حداکثر تعداد حروف مجاز قابل ورود در یک فیلد و از pattern برای کار با عبارات با قاعده پیشتیبانی میکند. برای بهبود این وضعیت در این مطلب قصد داریم روش تهیه اعتبارسنجهای سفارشی مخصوص حالت فرمهای مبتنی بر قالبها را بررسی کنیم.
تدارک مقدمات مثال این قسمت
این مثال، در ادامهی همین سری کار با فرمهای مبتنی بر قالبها است. به همین جهت ابتدا ماژول جدید CustomValidators را به آن اضافه میکنیم:
>ng g m CustomValidators -m app.module --routing
همچنین به فایل app.module.ts مراجعه کرده و CustomValidatorsModule را بجای CustomValidatorsRoutingModule در قسمت imports معرفی میکنیم. سپس به این ماژول جدید، کامپوننت فرم ثبت نام یک کاربر را اضافه خواهیم کرد:
>ng g c CustomValidators/user-register
که اینکار سبب به روز رسانی فایل custom-validators.module.ts و افزوده شدن UserRegisterComponent به قسمت declarations آن میشود.
در ادامه کلاس مدل معادل فرم ثبت نام کاربران را تعریف میکنیم:
>ng g cl CustomValidators/user
با این محتوا:
export class User {
constructor(
public username: string = "",
public email: string = "",
public password: string = "",
public confirmPassword: string = ""
) {}
}
در طراحی فرم HTML ایی آن نیاز است این موارد رعایت شوند:
- ورود نام کاربری اجباری بوده و باید بین 5 تا 8 حرف باشد.
- ورود ایمیل اجباری بوده و باید فرمت مناسبی نیز داشته باشد.
- ورود کلمهی عبور اجباری بوده و باید با confirmPassword تطابق داشته باشد.
- ورود «کلمهی عبور خود را مجددا وارد کنید» اجباری بوده و باید با password تطابق داشته باشد.
![]()
تعریف اعتبارسنج سفارشی ایمیلها
هرچند میتوان اعتبارسنجی ایمیلها را توسط ویژگی استاندارد pattern نیز مدیریت کرد، اما جهت بررسی نحوهی انتقال آن به یک اعتبارسنج سفارشی، کار را با ایجاد یک دایرکتیو مخصوص آن ادامه میدهیم:
>ng g d CustomValidators/EmailValidator -m custom-validators.module
این دستور علاوه بر ایجاد فایل جدید email-validator.directive.ts و تکمیل ساختار ابتدایی آن، کار به روز رسانی custom-validators.module.ts را نیز انجام میدهد. در این حالت به صورت خودکار قسمت declarations این ماژول با EmailValidatorDirective مقدار دهی میشود.
در ادامه کدهای کامل این اعتبارسنج سفارشی را مشاهده میکنید:
import { Directive } from "@angular/core";
import { AbstractControl, NG_VALIDATORS, Validator } from "@angular/forms";
@Directive({
selector:
"[appEmailValidator][formControlName],[appEmailValidator][formControl],[appEmailValidator][ngModel]",
providers: [
{
provide: NG_VALIDATORS,
useExisting: EmailValidatorDirective,
multi: true
}
]
})
export class EmailValidatorDirective implements Validator {
validate(element: AbstractControl): { [key: string]: any } {
const emailRegex = /\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/;
const valid = emailRegex.test(element.value);
return valid ? null : { appEmailValidator: true };
}
}
توضیحات تکمیلی:
- علت تعریف این اعتبارسنج به صورت یک دایرکتیو جدید این است که بتوان selector آنرا همانند ویژگیهای HTML، به فیلد ورودی اضافه کرد:
<input #email="ngModel" required appEmailValidator type="text" class="form-control"
name="email" [(ngModel)]="model.email">
- روش تعریف selector آن اندکی متفاوت است:
selector:
"[appEmailValidator][formControlName],[appEmailValidator][formControl],[appEmailValidator][ngModel]",
در اینجا مطابق
https://angular.io/guide/styleguide#style-02-08توصیه شدهاست که:
الف) نام دایرکتیو باید با یک پیشوند شروع شود و این پیشوند در فایل angular-cli.json. به app تنظیم شدهاست:
"apps": [
{
// ...
"prefix": "app",
این مساله در جهت مشخص کردن سفارشی بودن این دایرکتیو و همچنین کاهش احتمال تکرار نامها توصیه شدهاست.
ب) در اینجا formControlName، formControl و ngModel قید شدهی در کنار نام selector این دایرکتیو را نیز مشاهده میکنید. وجود آنها به این معنا است که کلاس این دایرکتیو، به المانهایی که به آنها ویژگی appEmailValidator اضافه شدهاست و همچنین آن المانها از یکی از سه نوع ذکر شده هستند، اعمال میشود و در سایر موارد بیاثر خواهد بود. البته ذکر این سه نوع، اختیاری است و صرفا میتوان نوشت:
selector: "[appEmailValidator]"
- پس از آن قسمت providers را مشاهده میکنید:
providers: [
{
provide: NG_VALIDATORS,
useExisting: EmailValidatorDirective,
multi: true
}
کار قسمت multi آن این است که EmailValidatorDirective (یا همان کلاس جاری) را به لیست NG_VALIDATORS توکار (اعتبارسنجهای توکار مبتنی بر قالبها) اضافه میکند و سبب بازنویسی هیچ موردی نخواهد شد. بنابراین وجود این قسمت در جهت تکمیل تامین کنندههای توکار Angular ضروری است.
- سپس پیاده سازی اینترفیس توکار Validator را مشاهده میکنید:
export class EmailValidatorDirective implements Validator {
این اینترفیس جزو مجموعهی فرمهای مبتنی بر قالبها است و از آن جهت نوشتن اعتبارسنجهای سفارشی میتوان استفاده کرد.
برای پیاده سازی این اینترفیس، نیاز است متد اجباری ذیل را نیز افزود و تکمیل کرد:
validate(element: AbstractControl): { [key: string]: any }
کار این متد این است که المانی را که appEmailValidator به آن اعمال شدهاست، به عنوان پارامتر متد validate در اختیار کلاس جاری قرار میدهد. به این ترتیب میتوان برای مثال به مقدار آن دسترسی یافت و سپس منطق سفارشی را پیاده سازی و یک خروجی key/value را بازگشت داد.
validate(element: AbstractControl): { [key: string]: any } {
const emailRegex = /\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/;
const valid = emailRegex.test(element.value);
return valid ? null : { appEmailValidator: true };
}
برای مثال در اینجا مقدار فیلد ایمیل element.value توسط عبارت باقاعدهی نوشته شده بررسی میشود. اگر با این الگو انطباق داشته باشد، نال بازگشت داده میشود (اعلام عدم وجود مشکلی در اعتبارسنجی) و اگر خیر، یک شیء key/value دلخواه را میتوان بازگشت داد.
- اکنون که این دایرکتیو جدید طراحی و ثبت شدهاست (در قسمت declarations فایل custom-validators.module.ts)، تنها کافی است selector آنرا به المان ورودی مدنظر اعمال کنیم تا کار اعتبارسنجی آنرا به صورت خودکار مدیریت کند:
<input #email="ngModel" required appEmailValidator type="text" class="form-control"
name="email" [(ngModel)]="model.email">
نحوهی طراحی خروجی متد validate
هنگام پیاده سازی متد validate اینترفیس Validator، هیچ قالب خاصی برای خروجی آن درنظر گرفته نشدهاست و همینقدر که این خروجی یک شیء key/value باشد، کفایت میکند. برای مثال اگر اعتبارسنج استاندارد required با شکست مواجه شود، یک چنین شیءایی را بازگشت میدهد:
و یا اگر اعتبارسنج استاندارد minlength باشکست مواجه شود، اطلاعات بیشتری را در قسمت مقدار این کلید بازگشتی، ارائه میدهد:
{ minlength : {
requiredLength : 3,
actualLength : 1
}
}
در کل اینکه چه چیزی را بازگشت دهید، بستگی به طراحی مدنظر شما دارد؛ برای نمونه در اینجا appEmailValidator (یک کلید و نام دلخواه است و هیچ الزامی ندارد که با نام selector این دایرکتیو یکی باشد)، به true تنظیم شدهاست:
{ appEmailValidator: true }
بنابراین شرط تامین نوع خروجی، برقرار است. علت true بودن آن نیز مورد ذیل است:
<div class="alert alert-danger" *ngIf="email.errors.appEmailValidator">
The entered email is not valid.</div>
در اینجا اگر false را بازگشت دهیم، هرچند email.errors دارای کلید جدید appEmailValidator شدهاست، اما ngIf سبب رندر خطای اعتبارسنجی «ایمیل وارد شده معتبر نیست.» به علت false بودن نتیجهی نهایی، نمیشود. یا حتی میتوان بجای true یک رشته و یا یک شیء با توضیحات بیشتری را نیز تنظیم کرد؛ چون value این key/value به any تنظیم شدهاست و هر چیزی را میپذیرد.
از دیدگاه اعتبارسنج فرمهای مبتنی بر قالبها، همینقدر که آرایهی email.errors دارای عضو و کلید جدیدی شد، کار به پایان رسیدهاست و اعتبارسنجی المان را شکست خورده ارزیابی میکند. مابقی آن، اطلاعاتی است که برنامه نویس ارائه میدهد (بر اساس نیازهای نمایشی برنامه).
تهیه اعتبارسنج سفارشی مقایسهی کلمات عبور با یکدیگر
در طراحی کلاس User که معادل فیلدهای فرم ثبت نام کاربران است، دو خاصیت کلمهی عبور و تائید کلمهی عبور را مشاهده میکنید:
public password: string = "",
public confirmPassword: string = ""
Angular به همراه اعتبارسنج توکاری برای بررسی یکی بودن این دو نیست. به همین جهت نمونهی سفارشی آنرا همانند EmailValidatorDirective فوق تهیه میکنیم. ابتدا یک دایرکتیو جدید را به نام EqualValidator به ماژول custom-validators اضافه میکنیم:
>ng g d CustomValidators/EqualValidator -m custom-validators.module
که سبب ایجاد فایل جدید equal-validator.directive.ts و به روز رسانی قسمت declarations فایل custom-validators.module.ts با EqualValidatorDirective نیز میشود.
در ادامه کدهای کامل آنرا در ذیل مشاهده میکنید:
import { Directive, Attribute } from "@angular/core";
import { Validator, AbstractControl, NG_VALIDATORS } from "@angular/forms";
@Directive({
selector:
"[appValidateEqual][formControlName],[appValidateEqual][formControl],[appValidateEqual][ngModel]",
providers: [
{
provide: NG_VALIDATORS,
useExisting: EqualValidatorDirective,
multi: true
}
]
})
export class EqualValidatorDirective implements Validator {
constructor(@Attribute("compare-to") public compareToControl: string) {}
validate(element: AbstractControl): { [key: string]: any } {
const selfValue = element.value;
const otherControl = element.root.get(this.compareToControl);
console.log("EqualValidatorDirective", {
thisControlValue: selfValue,
otherControlValue: otherControl ? otherControl.value : null
});
if (otherControl && selfValue !== otherControl.value) {
return {
appValidateEqual: true // Or a string such as 'Password mismatch.' or an abject.
};
}
if (
otherControl &&
otherControl.errors &&
selfValue === otherControl.value
) {
delete otherControl.errors["appValidateEqual"];
if (!Object.keys(otherControl.errors).length) {
otherControl.setErrors(null);
}
}
return null;
}
}
توضیحات تکمیلی:
- قسمت آغازین این اعتبارسنج سفارشی، مانند توضیحات EmailValidatorDirective است که در ابتدای بحث عنوان شد. این کلاس به یک Directive مزین شدهاست تا بتوان selector آنرا به المانهای HTML ایی فرم افزود (برای مثال در اینجا به دو فیلد ورود کلمات عبور). قسمت providers آن نیز تنظیم شدهاست تا EqualValidatorDirective جاری به لیست توکار NG_VALIDATORS اضافه شود.
- در ابتدای کار، پیاده سازی اینترفیس Validator، همانند قبل انجام شدهاست؛ اما چون در اینجا میخواهیم نام فیلدی را که قرار است کار مقایسه را با آن انجام دهیم نیز دریافت کنیم، ابتدا یک Attribute و سپس یک پارامتر و خاصیت عمومی دریافت کنندهی مقدار آنرا نیز افزودهایم:
export class EqualValidatorDirective implements Validator {
constructor(@Attribute("compare-to") public compareToControl: string) {}
به این ترتیب زمانیکه قرار است فیلد کلمهی عبور را تعریف کنیم، ابتدا ویژگی appValidateEqual یا همان selector این اعتبارسنج به آن اضافه شدهاست تا کار فعال سازی ابتدایی صورت گیرد:
<input #password="ngModel" required type="password" class="form-control"
appValidateEqual compare-to="confirmPassword" name="password" [(ngModel)]="model.password">
سپس Attribute یا ویژگی به نام compare-to نیز تعریف شدهاست. این compare-to همان نامی است که به Attribute@ نسبت داده شدهاست. سپس مقداری که به این ویژگی نسبت داده میشود، توسط خاصیت compareToControl دریافت خواهد شد.
در اینجا محدودیتی هم از لحاظ تعداد ویژگیها نیست و اگر قرار است این اعتبارسنج اطلاعات بیشتری را نیز دریافت کند میتوان ویژگیهای بیشتری را به سازندهی آن نسبت داد.
یک نکته:میتوان نام این ویژگی را با نام selector نیز یکی انتخاب کرد. به این ترتیب ذکر نام ویژگی آن، هم سبب فعال شدن اعتبارسنج و هم نسبت دادن مقداری به آن، سبب مقدار دهی خاصیت متناظر با آن، در سمت کلاس اعتبارسنج میگردد.
- در ابتدای این اعتبارسنج، نحوهی دسترسی به مقدار یک کنترل دیگر را نیز مشاهده میکنید:
export class EqualValidatorDirective implements Validator {
constructor(@Attribute("compare-to") public compareToControl: string) {}
validate(element: AbstractControl): { [key: string]: any } {
const selfValue = element.value;
const otherControl = element.root.get(this.compareToControl);
console.log("EqualValidatorDirective", {
thisControlValue: selfValue,
otherControlValue: otherControl ? otherControl.value : null
});
در اینجا element.value مقدار المان یا کنترل HTML جاری است که appValidateEqual به آن اعمال شدهاست.
بر اساس مقدار خاصیت compareToControl که از ویژگی compare-to دریافت میشود، میتوان به کنترل دوم، توسط element.root.get دسترسی یافت.
- در ادامهی کار، مقایسهی سادهای را مشاهده میکنید:
if (otherControl && selfValue !== otherControl.value) {
return {
appValidateEqual: true // Or a string such as 'Password mismatch.' or an abject.
};
}
اگر کنترل دوم یافت شد و همچنین مقدار آن با مقدار کنترل جاری یکی نبود، همان شیء key/value مورد انتظار متد validate، در جهت اعلام شکست اعتبارسنجی بازگشت داده میشود.
- در پایان کدهای متد validate، چنین تنظیمی نیز قرار گرفتهاست:
if (otherControl && otherControl.errors && selfValue === otherControl.value) {
delete otherControl.errors["appValidateEqual"];
if (!Object.keys(otherControl.errors).length) {
otherControl.setErrors(null);
}
}
return null;
اعتبارسنج تعریف شده، فقط به کنترلی که هم اکنون در حال کار با آن هستیم اعمال میشود. اگر پیشتر کلمهی عبوری را وارد کرده باشیم و سپس به فیلد تائید آن مراجعه کنیم، وضعیت اعتبارسنجی فیلد کلمهی عبور قبلی به حالت غیرمعتبر تنظیم شدهاست. اما پس از تکمیل فیلد تائید کلمهی عبور، هرچند وضعیت فیلد جاری معتبر است، اما هنوز وضعیت فیلد قبلی غیرمعتبر میباشد. برای رفع این مشکل، ابتدا کلید دلخواه appValidateEqual را از آن حذف میکنیم (همان کلیدی است که پیشتر در صورت مساوی نبودن مقدار فیلدها بازگشت داده شدهاست). حذف این کلید سبب نال شدن آرایهی errors یک شیء نمیشود و همانطور که پیشتر عنوان شد، Angular تنها به همین مورد توجه میکند. بنابراین در ادامه کار، setErrors یا تنظیم آرایهی errors به نال هم انجام شدهاست. در اینجا است که Angular فیلد دوم را نیز معتبر ارزیابی خواهد کرد.
تکمیل کامپوننت فرم ثبت نام کاربران
اکنون user-register.component.ts را که در ابتدای بحث اضافه کردیم، چنین تعاریفی را پیدا میکند:
import { NgForm } from "@angular/forms";
import { User } from "./../user";
import { Component, OnInit } from "@angular/core";
@Component({
selector: "app-user-register",
templateUrl: "./user-register.component.html",
styleUrls: ["./user-register.component.css"]
})
export class UserRegisterComponent implements OnInit {
model = new User();
constructor() {}
ngOnInit() {}
submitForm(form: NgForm) {
console.log(this.model);
console.log(form.value);
}
}
در اینجا تنها کار مهمی که انجام شدهاست، ارائهی خاصیت عمومی مدل، جهت استفادهی از آن در قالب HTML ایی این کامپوننت است. بنابراین به فایل user-register.component.html مراجعه کرده و آنرا نیز به صورت ذیل تکمیل میکنیم:
ابتدای فرم<div class="container"><h3>Registration Form</h3><form #form="ngForm" (submit)="submitForm(form)" novalidate>
در اینجا novalidate اضافه شدهاست تا اعتبارسنجی توکار مرورگرها با اعتبارسنجی سفارشی فرم جاری تداخل پیدا نکند. همچنین توسط یک template reference variable به وهلهی از فرم دسترسی یافته و آنرا به متد submitForm کامپوننت ارسال کردهایم.
تکمیل قسمت ورود نام کاربری<div class="form-group" [class.has-error]="username.invalid && username.touched"><label class="control-label">User Name</label><input #username="ngModel" required maxlength="8" minlength="4" type="text"
class="form-control" name="username" [(ngModel)]="model.username"><div *ngIf="username.invalid && username.touched"><div class="alert alert-info">
errors: {{ username.errors | json }}</div><div class="alert alert-danger" *ngIf="username.errors.required">
username is required.</div><div class="alert alert-danger" *ngIf="username.errors.minlength">
username should be minimum {{username.errors.minlength.requiredLength}} characters.</div><div class="alert alert-danger" *ngIf="username.errors.maxlength">
username should be max {{username.errors.maxlength.requiredLength}} characters.</div></div></div>
اعتبارسنجی فیلد نام کاربری شامل سه قسمت بررسی errors.required، errors.minlength و errors.maxlength است.
![]()
تکمیل قسمت ورود ایمیل<div class="form-group" [class.has-error]="email.invalid && email.touched"><label class="control-label">Email</label><input #email="ngModel" required appEmailValidator type="text" class="form-control"
name="email" [(ngModel)]="model.email"><div *ngIf="email.invalid && email.touched"><div class="alert alert-info">
errors: {{ email.errors | json }}</div><div class="alert alert-danger" *ngIf="email.errors.required">
email is required.</div><div class="alert alert-danger" *ngIf="email.errors.appEmailValidator">
The entered email is not valid.</div></div></div>
در اینجا نحوهی استفادهی از دایرکتیو جدید appEmailValidator را ملاحظه میکنید. این دایرکتیو ابتدا به المان فوق متصل و سپس نتیجهی آن در قسمت ngIf، برای نمایش خطای متناظری بررسی شدهاست.
![]()
تکمیل قسمتهای ورود کلمهی عبور و تائید آن<div class="form-group" [class.has-error]="password.invalid && password.touched"><label class="control-label">Password</label><input #password="ngModel" required type="password" class="form-control"
appValidateEqual compare-to="confirmPassword" name="password" [(ngModel)]="model.password"><div *ngIf="password.invalid && password.touched"><div class="alert alert-info">
errors: {{ password.errors | json }}</div><div class="alert alert-danger" *ngIf="password.errors.required">
password is required.</div><div class="alert alert-danger" *ngIf="password.errors.appValidateEqual">
Password mismatch. Please complete the confirmPassword .</div></div></div><div class="form-group" [class.has-error]="confirmPassword.invalid && confirmPassword.touched"><label class="control-label">Retype password</label><input #confirmPassword="ngModel" required type="password" class="form-control"
appValidateEqual compare-to="password" name="confirmPassword" [(ngModel)]="model.confirmPassword"><div *ngIf="confirmPassword.invalid && confirmPassword.touched"><div class="alert alert-info">
errors: {{ confirmPassword.errors | json }}</div><div class="alert alert-danger" *ngIf="confirmPassword.errors.required">
confirmPassword is required.</div><div class="alert alert-danger" *ngIf="confirmPassword.errors.appValidateEqual">
Password mismatch.</div></div></div>
در اینجا نحوهی اعمال دایرکتیو جدید appValidateEqual و همچنین ویژگی compare-to آنرا به فیلدهای کلمهی عبور و تائید آن مشاهده میکنید.
همچنین خروجی آن نیز در قسمت ngIf آخر بررسی شدهاست و سبب نمایش خطای اعتبارسنجی متناسبی میشود.
![]()
تکمیل انتهای فرم<button class="btn btn-primary" [disabled]="form.invalid" type="submit">Ok</button></form></div>
در اینجا بررسی میشود که آیا فرم معتبر است یا خیر. اگر خیر، دکمهی submit آن غیرفعال میشود و برعکس.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-template-driven-forms-lab-08.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس به ریشهی پروژه وارد شده و دو پنجرهی کنسول مجزا را باز کنید. در اولی دستورات
>npm install>ng build --watch
و در دومی دستورات ذیل را اجرا کنید:
>dotnet restore>dotnet watch run
اکنون میتوانید برنامه را در آدرس http://localhost:5000 مشاهده و اجرا کنید.