بناء مدقق نماذج عام (Generic Form Validator) في Angular: استراتيجية فعالة للتحقق من البيانات
بناء مدقق نماذج عام (Generic Form Validator) في Angular: استراتيجية فعالة للتحقق من البيانات
يُعد تطوير تطبيقات Angular التي تتضمن العديد من النماذج مهمة قد تكون مرهقة، خاصةً عندما يتعين عليك التعامل مع رسائل التحقق لكل مكون على حدة. إحدى الطرق الفعالة لتقليل هذا العبء هي إنشاء فئة تحقق عامة (generic validation class) تتولى إدارة جميع رسائل التحقق الخاصة بك. هذا النهج يقلل بشكل كبير من كمية الكود في قالب HTML الخاص بك، ويوفر لك مصدرًا موحدًا لرسائل الأخطاء مع مرونة في تجاوز الرسائل الافتراضية لكل مكون. صحيح أن هذا يتطلب كتابة المزيد من الكود في المكونات وملفات إضافية، ولكن الفوائد تفوق التحديات عند التعامل مع نماذج متعددة في مكونات مختلفة.
المتطلبات الأساسية
- معرفة أساسية بـ
Angular. - معرفة أساسية بـ
Reactive Forms.
ما سنقوم ببنائه
يحتوي Angular على نوعين من النماذج: template driven forms و reactive forms. في هذا المقال، سنركز على reactive forms. سنتعلم كيفية التحقق من نماذج تسجيل الدخول والتسجيل البسيطة باستخدام آلية تحقق عامة ضمن reactive form. لقد استخدمت إطار عمل Bulma CSS للتصميم. سيتم عرض قيم إدخال النموذج في سجل المتصفح (console.log) عند النقر على زر الإرسال. لقد فعلت ذلك لكي نتمكن من التركيز بشكل أساسي على التحقق من النموذج، ولكن يمكنك التعامل مع قيم الإدخال بالطريقة التي تراها مناسبة. يمكنك الاطلاع على العرض التوضيحي على Stackblitz.
الخطوة 1: الإعداد
لقد قمت بإنشاء ملف بدء لهذا المشروع يتضمن جميع ملفات HTML و CSS وتنسيقات Bulma جاهزة. هذا يسمح لنا بالتركيز أكثر على التحقق العام للنماذج. يمكنك استنساخ هذا المستودع من GitHub. ثم، في الطرفية (terminal) الخاصة بك، قم بتشغيل هذا الأمر:
git clone git@github.com:onwuvic/generic-reactive-form-validation.git
أو يمكنك استخدام:
git clone https://github.com/onwuvic/generic-reactive-form-validation.git
بعد الاستنساخ، انتقل إلى مجلد المشروع وقم بالتبديل إلى فرع البداية وتثبيت التبعيات وتشغيل التطبيق:
cd generic-reactive-form-validation
git checkout starter
npm install
ng serve
بعد ذلك، قم بزيارة http://localhost:4200/ في متصفحك. افتح مجلد generic-reactive-form-validation في أي محرر نصوص تفضله. يجب أن يبدو هيكل الملفات كما يلي:

الخطوة 2: استيراد ReactiveFormsModule
الآن، لنقم باستيراد ReactiveFormsModule إلى وحدة التطبيق (app module) الخاصة بنا وإضافتها إلى مصفوفة الاستيرادات (imports array).
import { BrowserModule } from '@angular/platform-browser' ;
import { NgModule } from '@angular/core' ;
import { ReactiveFormsModule } from '@angular/forms' ;
import { AppRoutingModule } from './app-routing.module' ;
import { AppComponent } from './app.component' ;
import { LoginComponent } from './modules/login/login.component' ;
import { SignUpComponent } from './modules/sign-up/sign-up.component' ;
@NgModule ({
declarations: [
AppComponent,
LoginComponent,
SignUpComponent
],
imports: [
BrowserModule,
AppRoutingModule,
ReactiveFormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
الخطوة 3: إنشاء فئة تحقق عامة ومدقق تأكيد كلمة المرور
فئة التحقق العامة (Generic Validation Class)
لنقم بإنشاء مجلد shared داخل مجلد app الجذري. ثم داخل مجلد shared، أنشئ ملفًا باسم generic-validator.ts. اكتب الكود التالي:
import { FormGroup } from '@angular/forms' ;
// توفير جميع رسائل التحقق هنا
const VALIDATION_MESSAGES = {
email: {
required: 'مطلوب',
email: 'هذا البريد الإلكتروني غير صالح'
},
password: {
required: 'مطلوب',
minlength: 'يجب أن يكون طول كلمة المرور أكبر من أو يساوي 8'
},
confirmPassword: {
required: 'مطلوب',
match: 'كلمة المرور غير متطابقة'
},
firstName: {
required: 'مطلوب'
},
lastName: {
required: 'مطلوب'
}
};
export class GenericValidator {
// بشكل افتراضي يتم تمرير مجموعة رسائل التحقق المحددة، ولكن يمكن تمرير رسالة مخصصة عند استدعاء الفئة
constructor ( private validationMessages: { [key: string ]: { [key: string ]: string } } = VALIDATION_MESSAGES ) {}
// ستقوم هذه الدالة بمعالجة كل عنصر تحكم (formControl) في مجموعة النموذج (form group)
// ثم تعيد رسالة الخطأ لعرضها
// ستكون القيمة المعادة بهذا التنسيق `formControlName: 'error message'`
processMessages(container: FormGroup): { [key: string ]: string } {
const messages = {};
// التكرار عبر جميع عناصر التحكم في النموذج
for ( const controlKey in container.controls) {
if (container.controls.hasOwnProperty(controlKey)) {
// الحصول على خصائص كل عنصر تحكم
const controlProperty = container.controls[controlKey];
// إذا كان FormGroup، قم بمعالجة عناصر التحكم الفرعية الخاصة به.
if (controlProperty instanceof FormGroup) {
const childMessages = this .processMessages(controlProperty);
Object .assign(messages, childMessages);
} else {
// التحقق فقط إذا كانت هناك رسائل تحقق لعنصر التحكم
if ( this .validationMessages[controlKey]) {
messages[controlKey] = '' ;
if ((controlProperty.dirty || controlProperty.touched) && controlProperty.errors) {
// التكرار عبر كائن الأخطاء
Object .keys(controlProperty.errors).map(
messageKey => {
if ( this .validationMessages[controlKey][messageKey]) {
messages[controlKey] += this .validationMessages[controlKey][messageKey] + ' ' ;
}
}
);
}
}
}
}
}
return messages;
}
}
أولاً، نقوم باستيراد FormGroup. يمكننا كتابة جميع رسائل التحقق الخاصة بنا في هذا الملف أو تمرير كل رسالة تحقق من النموذج من المكون الخاص بها. كل خاصية في كائن VALIDATION_MESSAGES تتوافق مع اسم حقل الإدخال أو formControlName. بالإضافة إلى ذلك، تتوافق كل خاصية لحقل الإدخال مع اسم التحقق الخاص بها. قيمتها هي ما تريد عرضه كرسالة خطأ. على سبيل المثال، حقل الإدخال الذي يحمل formControlName “email” لديه تحققات “required” و “email“.
في دالة البناء (constructor)، يمكننا تجاوز رسائل الخطأ الافتراضية من المكون حيث يتم استخدام التحقق العام الخاص بنا عن طريق تمرير رسالة التحقق عند إنشاء مثيل لفئة التحقق العامة. تقوم دالة processMessages بمعالجة كل حقل إدخال في النموذج وتعيد رسالة الخطأ لعرضها.
مدقق تأكيد كلمة المرور
الآن، لنقم بإنشاء مدقق لتأكيد كلمة المرور للتحقق مما إذا كانت كلمة المرور وتأكيد كلمة المرور متطابقين. داخل مجلد shared، أنشئ ملفًا باسم password-matcher.ts. اكتب الكود التالي:
import { AbstractControl } from '@angular/forms' ;
export class PasswordMatcher {
static match(control: AbstractControl): void | null {
const passwordControl = control.get( 'password' );
const confirmPasswordControl = control.get( 'confirmPassword' );
if (passwordControl.pristine || confirmPasswordControl.pristine) {
return null ;
}
if (passwordControl.value === confirmPasswordControl.value) {
return null ;
}
confirmPasswordControl.setErrors({ match: true });
}
}
هنا، نستخدم دالة match الثابتة التي تستقبل AbstractControl. تتحقق هذه الدالة مما إذا كانت قيم حقلي password و confirmPassword متطابقة. إذا لم تكن كذلك، فإنها تقوم بتعيين خطأ match على حقل confirmPassword.
الخطوة 4: إضافة FormGroup و FormBuilder إلى كل مكون وقالب
مكون نموذج التسجيل (Sign Up Form Component)
داخل مجلد app/modules/sign-up، أضف الكود التالي إلى مكون التسجيل sign-up.component.ts:
import { Component, OnInit } from '@angular/core' ;
import { FormGroup, FormBuilder, Validators } from '@angular/forms' ;
import { PasswordMatcher } from '../../shared/password-matcher' ;
@Component ({
selector: 'app-sign-up' ,
templateUrl: './sign-up.component.html' ,
styleUrls: [
'./sign-up.component.scss'
]
})
export class SignUpComponent implements OnInit {
signupForm: FormGroup;
// يستخدم مع فئة رسائل التحقق العامة
displayMessage: { [key: string ]: string } = {};
constructor ( private fb: FormBuilder ) {}
ngOnInit() {
this .signupForm = this .fb.group({
firstName: [ '' , [Validators.required]],
lastName: [ '' , [Validators.required]],
email: [ '' , [Validators.required, Validators.email]],
password: [ '' , [Validators.required, Validators.minLength( 8 )]],
confirmPassword: [ '' , Validators.required]
},
{ validator: PasswordMatcher.match });
}
signup() {
console .log( '---form' , this .signupForm.value);
}
}
لقد قمنا هنا بتضمين التحققات المدمجة في Angular لكل حقل إدخال، بالإضافة إلى التحقق المخصص PasswordMatcher لضمان تطابق كلمة المرور وتأكيد كلمة المرور. يتم تهيئة النموذج في دالة ngOnInit باستخدام FormBuilder.
قالب نموذج التسجيل (Sign Up Form Template)
الآن دعنا نلقي نظرة على قالب نموذج التسجيل:
< h1 class = "title is-4" > Sign Up </ h1 >
< p class = "description" > Let's get started! </ p >
< form ( ngSubmit )= "signup()" [ formGroup ]= "signupForm" novalidate autocomplete = "false" >
< div class = "field" >
< div class = "control" >
< input [ ngClass ]= "{'is-danger': displayMessage.firstName}" formControlName = "firstName" class = "input is-medium" type = "text" placeholder = "First Name" >
< p * ngIf = "displayMessage.firstName" class = "help is-danger" > {{ displayMessage.firstName }} </ p >
</ div >
</ div >
< div class = "field" >
< div class = "control" >
< input [ ngClass ]= "{'is-danger': displayMessage.lastName}" formControlName = "lastName" class = "input is-medium" type = "text" placeholder = "Last Name" >
< p * ngIf = "displayMessage.lastName" class = "help is-danger" > {{ displayMessage.lastName }} </ p >
</ div >
</ div >
< div class = "field" >
< div class = "control" >
< input [ ngClass ]= "{'is-danger': displayMessage.email}" formControlName = "email" class = "input is-medium" type = "email" placeholder = "Email" >
< p * ngIf = "displayMessage.email" class = "help is-danger" > {{ displayMessage.email }} </ p >
</ div >
</ div >
< div class = "field" >
< div class = "control" >
< input [ ngClass ]= "{'is-danger': displayMessage.password || displayMessage.confirmPassword }" formControlName = "password" class = "input is-medium" type = "password" placeholder = "Password" >
< p * ngIf = "displayMessage.password" class = "help is-danger" > {{ displayMessage.password }} </ p >
</ div >
</ div >
< div class = "field" >
< div class = "control" >
< input [ ngClass ]= "{'is-danger': displayMessage.confirmPassword}" formControlName = "confirmPassword" class = "input is-medium" type = "password" placeholder = "Confirm Password" >
< p * ngIf = "displayMessage.confirmPassword" class = "help is-danger" > {{ displayMessage.confirmPassword }} </ p >
</ div >
</ div >
< br >
< button type = "submit" class = "button is-block is-primary is-fullwidth is-medium" [ disabled ]= "signupForm.invalid" > Submit </ button >
< br >
< small class = "has-text-centered" >
< em > Already have an account < a [ routerLink ]= "['']" class = "primary-color" > Login </ a >
</ em >
</ small >
</ form >
لقد أضفنا ngSubmit و formGroup إلى وسم النموذج (form tag). كما أضفنا formControlName لكل حقل إدخال. إذا كانت رسالة العرض (displayMessage) تحتوي على رسالة خطأ للحقل firstName، فسيتم تطبيق الفئة is-danger من ngClass على حقل الإدخال. يقوم الوسم <p *ngIf="displayMessage.firstName"> بعرض رسالة الخطأ الخاصة بنا. نقوم بتعطيل زر الإرسال (Submit) إذا كان النموذج غير صالح باستخدام [disabled]="signupForm.invalid".
مكون نموذج تسجيل الدخول (Login Form Component)
داخل مجلد app/modules/login، أضف الكود التالي إلى مكون تسجيل الدخول login.component.ts:
import { Component, OnInit, AfterViewInit } from '@angular/core' ;
import { FormGroup, FormBuilder, Validators } from '@angular/forms' ;
@Component ({
selector: 'app-login' ,
templateUrl: './login.component.html' ,
styleUrls: [
'./login.component.scss'
]
})
export class LoginComponent implements OnInit, AfterViewInit {
loginForm: FormGroup;
// يستخدم مع فئة رسائل التحقق العامة
displayMessage: { [key: string ]: string } = {};
private validationMessages: { [key: string ]: { [key: string ]: string } };
constructor ( private fb: FormBuilder ) {
// يحدد جميع رسائل التحقق للنموذج.
this .validationMessages = {
email: {
required: 'مطلوب',
email: 'هذا البريد الإلكتروني غير صالح'
},
password: {
required: 'مطلوب',
minlength: 'يجب أن يكون طول كلمة المرور أكبر من أو يساوي 8'
}
};
}
ngOnInit() {
this .loginForm = this .fb.group({
email: [ '' , [Validators.required, Validators.email]],
password: [ '' , [Validators.required, Validators.minLength( 8 )]],
});
}
login() {
console .log( '---form' , this .loginForm.value);
}
}
الفرق الوحيد هنا مقارنة بمكون التسجيل هو أننا سنقوم بتجاوز رسائل الخطأ الافتراضية في فئة التحقق العامة الخاصة بنا برسائل التحقق المحددة في هذا المكون.
قالب نموذج تسجيل الدخول (Login Form Template)
اكتب الكود التالي في قالب تسجيل الدخول:
< h1 class = "title is-4" > Login </ h1 >
< p class = "description" > Welcome back! </ p >
< form ( ngSubmit )= "login()" [ formGroup ]= "loginForm" novalidate autocomplete = "false" >
< div class = "field" >
< div class = "control" >
< input [ ngClass ]= "{'is-danger': displayMessage.email}" class = "input is-medium" type = "email" placeholder = "Email" formControlName = "email" >
< p * ngIf = "displayMessage.email" class = "help is-danger" > {{ displayMessage.email }} </ p >
</ div >
</ div >
< div class = "field" >
< div class = "control" >
< input [ ngClass ]= "{'is-danger': displayMessage.password}" class = "input is-medium" type = "password" placeholder = "Password" formControlName = "password" >
< p * ngIf = "displayMessage.password" class = "help is-danger" > {{ displayMessage.password }} </ p >
</ div >
</ div >
< button type = "submit" class = "button is-block is-primary is-fullwidth is-medium" [ disabled ]= "loginForm.invalid" > Login </ button >
< br >
< small class = "has-text-centered" >
< em > Don't have an account < a [ routerLink ]= "['signup']" class = "primary-color" > Sign Up </ a >
</ em >
</ small >
</ form >
الخطوة 5: استخدام التحقق العام في كل مكون
التحقق العام في نموذج التسجيل (Sign Up)
أضف الكود التالي إلى ملف sign-up.component.ts:
import { Component, OnInit, ViewChildren, ElementRef, AfterViewInit } from '@angular/core' ;
import { FormGroup, FormBuilder, Validators, FormControlName, AbstractControl } from '@angular/forms' ;
import { Observable, fromEvent, merge } from 'rxjs' ;
import { debounceTime } from 'rxjs/operators' ;
import { GenericValidator } from '../../shared/generic-validator' ;
import { PasswordMatcher } from '../../shared/password-matcher' ;
@Component ({
selector: 'app-sign-up' ,
templateUrl: './sign-up.component.html' ,
styleUrls: [
'./sign-up.component.scss'
]
})
export class SignUpComponent implements OnInit, AfterViewInit {
// الوصول إلى جميع حقول إدخال النموذج في ملف html الخاص بالتسجيل لدينا
@ViewChildren (FormControlName, { read: ElementRef }) formInputElements: ElementRef[];
signupForm: FormGroup;
// يستخدم مع فئة رسائل التحقق العامة
displayMessage: { [key: string ]: string } = {};
private genericValidator: GenericValidator;
constructor ( private fb: FormBuilder ) {
// تعريف مثيل للمدقق لاستخدامه مع هذا النموذج
this .genericValidator = new GenericValidator();
}
ngOnInit() {
this .signupForm = this .fb.group({
firstName: [ '' , [Validators.required]],
lastName: [ '' , [Validators.required]],
email: [ '' , [Validators.required, Validators.email]],
password: [ '' , [Validators.required, Validators.minLength( 8 )]],
confirmPassword: [ '' , Validators.required]
},
{ validator: PasswordMatcher.match });
}
ngAfterViewInit(): void {
// مراقبة حدث 'blur' من أي عنصر إدخال في النموذج.
const controlBlurs: Observable< any >[] = this .formInputElements
.map( ( formControl: ElementRef ) => fromEvent(formControl.nativeElement, 'blur' ));
// دمج حدث 'blur' مع حدث 'valueChanges'
merge( this .signupForm.valueChanges, ...controlBlurs).pipe(
debounceTime( 800 )
).subscribe( value => {
this .displayMessage = this .genericValidator.processMessages(
this .signupForm);
});
}
signup() {
console .log( '---form' , this .signupForm.value);
}
}
هنا قمنا باستيراد فئة التحقق العامة (generic validation class). أضفنا @ViewChildren للوصول إلى كل حقل إدخال في ملف HTML الخاص بالتسجيل، مما يساعدنا على الاستماع للأحداث عليها. نقوم بإنشاء مثيل لـ GenericValidator داخل دالة البناء (constructor). ثم، نقوم بتطبيق واجهة ngAfterViewInit.
في دالة ngAfterViewInit()، نراقب حدث blur من أي عنصر إدخال في النموذج. ثم ندمج ملاحظات valueChanges للنموذج (التي يتم تشغيلها عند تغيير أي من قيم الإدخال) وأحداث blur لأي حقل إدخال في ملاحظة واحدة باستخدام merge. لذلك، عندما يغير المستخدم قيمة إدخال أو ينقر على أي حقل إدخال، يتم تشغيل دالة merge هذه. ثم نضيف تأخيرًا قدره 800 مللي ثانية باستخدام debounceTime(800). هذا يمنح المستخدم وقتًا لإجراء التغييرات قبل تشغيل التحقق. أخيرًا، نحصل على رسائل الخطأ لعرضها عن طريق استدعاء دالة processMessages من المدقق العام.
التحقق العام في نموذج تسجيل الدخول (Login)
اكتب الكود التالي في ملف login.component.ts:
import { Component, OnInit, ViewChildren, ElementRef, AfterViewInit } from '@angular/core' ;
import { FormGroup, FormBuilder, Validators, FormControlName } from '@angular/forms' ;
import { Observable, fromEvent, merge } from 'rxjs' ;
import { debounceTime } from 'rxjs/operators' ;
import { GenericValidator } from '../../shared/generic-validator' ;
@Component ({
selector: 'app-login' ,
templateUrl: './login.component.html' ,
styleUrls: [
'./login.component.scss'
]
})
export class LoginComponent implements OnInit, AfterViewInit {
// الوصول إلى جميع حقول إدخال النموذج في ملف html الخاص بتسجيل الدخول لدينا
@ViewChildren (FormControlName, { read: ElementRef }) formInputElements: ElementRef[];
loginForm: FormGroup;
// يستخدم مع فئة رسائل التحقق العامة
displayMessage: { [key: string ]: string } = {};
private validationMessages: { [key: string ]: { [key: string ]: string } };
private genericValidator: GenericValidator;
constructor ( private fb: FormBuilder ) {
// يحدد جميع رسائل التحقق للنموذج.
this .validationMessages = {
email: {
required: 'مطلوب',
email: 'هذا البريد الإلكتروني غير صالح'
},
password: {
required: 'مطلوب',
minlength: 'يجب أن يكون طول كلمة المرور أكبر من أو يساوي 8'
}
};
// تعريف مثيل للمدقق لاستخدامه مع هذا النموذج،
// مع تمرير مجموعة رسائل التحقق الخاصة بهذا النموذج.
this .genericValidator = new GenericValidator( this .validationMessages);
}
ngOnInit() {
this .loginForm = this .fb.group({
email: [ '' , [Validators.required, Validators.email]],
password: [ '' , [Validators.required, Validators.minLength( 8 )]],
});
}
ngAfterViewInit(): void {
// مراقبة حدث 'blur' من أي عنصر إدخال في النموذج.
const controlBlurs: Observable< any >[] = this .formInputElements
.map( ( formControl: ElementRef ) => fromEvent(formControl.nativeElement, 'blur' ));
// دمج حدث 'blur' مع حدث 'valueChanges'
merge( this .loginForm.valueChanges, ...controlBlurs).pipe(
debounceTime( 800 )
).subscribe( value => {
this .displayMessage = this .genericValidator.processMessages(
this .loginForm);
});
}
login() {
console .log( '---form' , this .loginForm.value);
}
}
الفرق الوحيد هنا عن كود التسجيل هو أننا نقوم بتجاوز رسائل التحقق الافتراضية الخاصة بنا برسائل التحقق الجديدة المحددة في هذا المكون. ثم نقوم بتمريرها إلى فئة التحقق العامة (generic validation class) عند إنشاء مثيل لها. يمكننا أن نتوقع أن يعمل هذا بنفس طريقة التحقق العام للتسجيل. وهذا كل ما تحتاجه لبناء مدقق عام في Angular.
الخلاصة التقنية
يُعد بناء مدقق نماذج عام في Angular استراتيجية برمجية ذكية لتحسين قابلية الصيانة وتقليل التكرار في تطبيقات الويب المعقدة. من خلال توحيد منطق التحقق ورسائل الأخطاء في فئة واحدة، يمكن للمطورين تحقيق كفاءة عالية في إدارة النماذج المتعددة. إن استخدام Reactive Forms جنبًا إلى جنب مع مراقبة أحداث valueChanges و blur، بالإضافة إلى تطبيق debounceTime، يوفر تجربة مستخدم سلسة واستجابة فورية للأخطاء، مما يعزز جودة التطبيق بشكل عام. هذا النهج لا يقلل فقط من حجم الكود في القوالب، بل يضمن أيضًا مرونة في تخصيص رسائل التحقق على مستوى المكونات عند الحاجة، مما يجعله حلًا مثاليًا للتطبيقات التي تتطلب تحكمًا دقيقًا في إدخالات المستخدم.