← الدرس السابق المستوى 4 - الدرس 10 من 12

📝 Angular Forms: بناء نماذج احترافية مع التحقق من الصحة

تقدمك في المستوى الرابع 83%

📝 لماذا النماذج (Forms) مهمة في Angular؟

النماذج هي البوابة الرئيسية للتفاعل بين المستخدم والتطبيق. سواء كان تسجيل دخول، إنشاء حساب، إضافة منتج، أو إرسال رسالة - كلها بتعتمد على نماذج. Angular بتقدم حلين قويين للتعامل مع النماذج: Template-driven Forms للنماذج البسيطة، و Reactive Forms للنماذج المعقدة.

الفرق الجوهري: في Template-driven، المنطق بيكون في الـ Template (HTML). في Reactive، المنطق بيكون في الـ Component (TypeScript). ده بيخلي Reactive Forms أكثر قوة، مرونة، وسهولة في الاختبار.

بنهاية هذا الدرس، هتكون قادر على:

💡 قاعدة ذهبية: استخدم Template-driven للنماذج البسيطة (Contact Form, Newsletter). استخدم Reactive للنماذج المعقدة (Registration, Checkout, Dynamic Fields).

🎯 Template-driven Forms - النماذج المبنية على القالب

Template-driven Forms هي الطريقة الأسهل والأسرع. بتعتمد على Directives في HTML (زي ngModel و ngForm) وبتستخدم Two-way Data Binding. المنطق كله في الـ Template، والـ Component بس بيستقبل البيانات النهائية.

🔍 الخطوة 1: إعداد FormsModule

// app.module.ts - تسجيل FormsModule import { FormsModule } from '@angular/forms'; @NgModule({ imports: [ // ... imports أخرى FormsModule // ✅ إضافة FormsModule للـ Template-driven ] }) export class AppModule { }

🔍 الخطوة 2: بناء النموذج في Template

<!-- contact.component.html - نموذج تواصل بسيط --> <form #contactForm="ngForm" (ngSubmit)="onSubmit(contactForm)"> <!-- حقل الاسم --> <div class="form-group"> <label for="name">👤 الاسم:</label> <input type="text" id="name" name="name" ngModel required minlength="3" #nameInput="ngModel" placeholder="أدخل اسمك الكامل" class="form-control" > <!-- رسائل الخطأ --> <div *ngIf="nameInput.invalid && (nameInput.dirty || nameInput.touched)" class="error"> <small *ngIf="nameInput.hasError('required')"> ❌ الاسم مطلوب </small> <small *ngIf="nameInput.hasError('minlength')"> ❌ الاسم لازم يكون 3 أحرف على الأقل </small> </div> </div> <!-- حقل البريد --> <div class="form-group"> <label for="email">📧 البريد الإلكتروني:</label> <input type="email" id="email" name="email" ngModel required email #emailInput="ngModel" placeholder="example@email.com" class="form-control" > <div *ngIf="emailInput.invalid && (emailInput.dirty || emailInput.touched)" class="error"> <small *ngIf="emailInput.hasError('required')">❌ البريد مطلوب</small> <small *ngIf="emailInput.hasError('email')">❌ بريد إلكتروني غير صحيح</small> </div> </div> <!-- حقل الرسالة --> <div class="form-group"> <label for="message">💬 الرسالة:</label> <textarea id="message" name="message" ngModel required minlength="10" maxlength="500" #messageInput="ngModel" placeholder="اكتب رسالتك هنا..." rows="5" class="form-control" ></textarea> <div *ngIf="messageInput.invalid && (messageInput.dirty || messageInput.touched)" class="error"> <small *ngIf="messageInput.hasError('required')">❌ الرسالة مطلوبة</small> <small *ngIf="messageInput.hasError('minlength')">❌ لازم 10 أحرف على الأقل</small> </div> <small class="char-count"> {{ messageInput.value?.length || 0 }} / 500 </small> </div> <!-- زر الإرسال --> <button type="submit" [disabled]="contactForm.invalid" class="btn-primary" > 📨 إرسال الرسالة </button> </form>

🔍 الخطوة 3: معالجة الإرسال في Component

// contact.component.ts import { Component } from '@angular/core'; @Component({ selector: 'app-contact', templateUrl: './contact.component.html' }) export class ContactComponent { onSubmit(form: any) { // ✅ التحقق من صحة النموذج if (form.valid) { console.log('✅ البيانات المرسلة:', form.value); // هنا تبعت البيانات لـ API // this.apiService.sendContact(form.value).subscribe(...) alert('✅ تم إرسال رسالتك بنجاح!'); form.reset(); // تفريغ النموذج } else { alert('❌ يرجى تصحيح الأخطاء قبل الإرسال'); } } }
📌 States المهم فهمها في Template-driven:
  • pristine = الحقل لسه متعدلش (قيمته الأصلية)
  • dirty = الحقل اتعدل (المستخدم كتب فيه)
  • touched = الحقل اكتسب Focus وخرج منه (blur)
  • untouched = الحقل لسه ما اكتسبش Focus
  • valid = الحقل يمر كل الـ Validators
  • invalid = الحقل فشل في Validator واحد على الأقل

⚡ Reactive Forms - النماذج التفاعلية (مستحسن)

Reactive Forms هي الطريقة الأقوى والأكثر مرونة. بتعتمد على FormBuilder و FormGroup و FormControl في الـ Component. المنطق كله في TypeScript، والـ Template بس بيربط بالـ Controls.

المميزات: اختبار أسهل (تقدر تختبر النموذج من غير DOM)، منطق أكثر تعقيد (Dynamic Fields, Nested Forms)، و أداء أفضل (Change Detection محسّن).

🔍 الخطوة 1: إعداد ReactiveFormsModule

// app.module.ts - تسجيل ReactiveFormsModule import { ReactiveFormsModule } from '@angular/forms'; @NgModule({ imports: [ // ... imports أخرى ReactiveFormsModule // ✅ إضافة ReactiveFormsModule ] }) export class AppModule { }

🔍 الخطوة 2: بناء النموذج برمجياً

// login.component.ts - نموذج تسجيل دخول Reactive import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; @Component({ selector: 'app-login', templateUrl: './login.component.html' }) export class LoginComponent implements OnInit { // ✅ FormGroup = مجموعة من FormControls loginForm!: FormGroup; submitted = false; loading = false; // ✅ FormBuilder يسهل إنشاء النماذج constructor( private fb: FormBuilder, private authService: AuthService ) {} ngOnInit() { // ✅ بناء النموذج برمجياً this.loginForm = this.fb.group({ // [القيمة الابتدائية, [Validators]] username: ['', [ Validators.required, Validators.minLength(3), Validators.pattern('^[a-zA-Z0-9_]+$') // أحرف وأرقام و underscore بس ]], password: ['', [ Validators.required, Validators.minLength(6), Validators.pattern('^(?=.*[A-Z])(?=.*[0-9]).+$') // حرف كبير + رقم ]], rememberMe: [false] // Checkbox بدون Validation }); } // ✅ معالجة الإرسال onSubmit() { this.submitted = true; // لو النموذج غير صالح، وقف if (this.loginForm.invalid) { return; } this.loading = true; const { username, password } = this.loginForm.value; this.authService.login(username, password) .subscribe({ next: (response) => { console.log('✅ تسجيل دخول ناجح:', response); // انتقل للـ Dashboard }, error: (err) => { console.error('❌ فشل تسجيل الدخول:', err); this.loading = false; } }); } // ✅ Getters للوصول السهل للحقول في Template get username() { return this.loginForm.get('username'); } get password() { return this.loginForm.get('password'); } // ✅ دالة مساعدة للتحقق من صحة حقل معين isFieldInvalid(fieldName: string): boolean { const field = this.loginForm.get(fieldName); return field ? (field.invalid && (field.dirty || field.touched)) : false; } }

🔍 الخطوة 3: ربط النموذج في Template

<!-- login.component.html --> <div class="login-container"> <h2>🔐 تسجيل الدخول</h2> <!-- ✅ [formGroup] يربط النموذج بالـ Component --> <form [formGroup]="loginForm" (ngSubmit)="onSubmit()"> <!-- حقل اسم المستخدم --> <div class="form-group"> <label for="username">👤 اسم المستخدم:</label> <input type="text" id="username" formControlName="username" placeholder="أدخل اسم المستخدم" class="form-control" [ngClass]="{ 'is-invalid': isFieldInvalid('username') }" > <div *ngIf="isFieldInvalid('username')" class="error-messages"> <small *ngIf="username?.hasError('required')"> ❌ اسم المستخدم مطلوب </small> <small *ngIf="username?.hasError('minlength')"> ❌ لازم 3 أحرف على الأقل </small> <small *ngIf="username?.hasError('pattern')"> ❌ مسموح بأحرف وأرقام و _ فقط </small> </div> </div> <!-- حقل كلمة المرور --> <div class="form-group"> <label for="password">🔒 كلمة المرور:</label> <input type="password" id="password" formControlName="password" placeholder="أدخل كلمة المرور" class="form-control" [ngClass]="{ 'is-invalid': isFieldInvalid('password') }" > <div *ngIf="isFieldInvalid('password')" class="error-messages"> <small *ngIf="password?.hasError('required')"> ❌ كلمة المرور مطلوبة </small> <small *ngIf="password?.hasError('minlength')"> ❌ لازم 6 أحرف على الأقل </small> <small *ngIf="password?.hasError('pattern')"> ❌ لازم تحتوي على حرف كبير ورقم </small> </div> </div> <!-- Checkbox --> <div class="form-group checkbox"> <label> <input type="checkbox" formControlName="rememberMe"> 🔄 تذكرني </label> </div> <!-- زر الدخول --> <button type="submit" [disabled]="loginForm.invalid || loading" class="btn-primary" > <span *ngIf="loading">⏳ جاري الدخول...</span> <span *ngIf="!loading">🔐 دخول</span> </button> </form> </div>

🔍 مقارنة شاملة: Template-driven vs Reactive Forms

الجانب Template-driven Reactive
الصعوبة ⭐ أسهل - Directives في HTML ⭐⭐ أصعب - منطق في TypeScript
المرونة محدودة - Validators جاهزة بس عالية جداً - Validators مخصصة، Dynamic Fields
الاختبار صعب - محتاج DOM سهل - تختبر من غير DOM
النماذج المعقدة ❌ غير مناسب ✅ مناسب جداً (Nested, Arrays)
Two-way Binding ✅ تلقائي مع ngModel ❌ يدوي (valueChanges)
الأداء أقل - Change Detection على كل حقل أفضل - Change Detection محسّن
متى تستخدم؟ نماذج بسيطة (Contact, Search) نماذج معقدة (Register, Checkout)

✅ Validators المدمجة في Angular

Angular بتوفّر مجموعة من Validators الجاهزة للتحقق من صحة البيانات:

الـ Validator الاستخدام مثال
required الحقل مطلوب (مش فارغ) Validators.required
minLength(n) الحد الأدنى للأحرف Validators.minLength(3)
maxLength(n) الحد الأقصى للأحرف Validators.maxLength(50)
email بريد إلكتروني صحيح Validators.email
pattern(regex) يطابق Regular Expression Validators.pattern('^[0-9]+$')
min(n) / max(n) للأرقام Validators.min(18)

⚠️ أخطاء شائعة في Angular Forms وتجنبها

❌ الخطأ 1: نسيان استيراد FormsModule أو ReactiveFormsModule

المشكلة: ngModel أو formGroup مش بيشتغلوا وبتطلع أخطاء في Console.

الحل: تأكد إنك مستورد FormsModule (لـ Template-driven) أو ReactiveFormsModule (لـ Reactive) في app.module.ts.

❌ الخطأ 2: استخدام ngModel مع formControlName في نفس الحقل

المشكلة: Angular بتطلع تحذير وبيحصل conflict.

الحل: استخدم ngModel في Template-driven، و formControlName في Reactive. متخلطش بينهم.

❌ الخطأ 3: عدم التحقق من form.invalid قبل الإرسال

المشكلة: بيانات غير صالحة بتتبعت للـ API.

الحل: دايماً افحص if (form.valid) أو if (this.loginForm.invalid) return; قبل ما تبعت البيانات.

💡 معايير احترافية في Angular Forms

  • استخدم Reactive Forms للنماذج المعقدة: Registration, Checkout, Wizard Forms.
  • استخدم Template-driven للنماذج البسيطة: Contact, Newsletter, Search.
  • تحقق من الأخطاء باستخدام Validators: متعتمدش على HTML validation بس.
  • أظهر رسائل الخطأ بوضوح: قول للمستخدم بالظبط إيه المشكلة وإزاي يصلحها.
  • استخدم formGroup و formControlName في Reactive: متستخدمش ngModel معاهم.
  • استخدم Getters للوصول السهل للحقول: get username() { return this.form.get('username'); }
  • عطل زر الإرسال لما النموذج غير صالح: [disabled]="form.invalid".
  • أضف حالة تحميل (Loading) للزر: عشان المستخدم يعرف إن حاجة بتحصل.

📚 دروس ذات صلة

❓ أسئلة شائعة عن Angular Forms

هل يمكن استخدام Template-driven و Reactive في نفس المشروع؟

نعم، ومش بس كده - في نفس الـ Module كمان! تقدر تستورد FormsModule و ReactiveFormsModule مع بعض في app.module.ts. بس في نفس الـ Component، لازم تختار نوع واحد. متستخدمش ngModel و formControlName في نفس الحقل.

إزاي أضيف Validator مخصص في Reactive Forms؟

اعمل دالة ترجع ValidatorFn. مثلاً Validator يتحقق إن الباسورد يحتوي على حرف كبير: passwordValidator(control: AbstractControl): ValidationErrors | null { ... }. وبعدين ضيفه في مصفوفة Validators: [Validators.required, passwordValidator].

هل Angular Forms آمنة ضد XSS؟

Angular بتعمل Sanitization تلقائي للبيانات اللي بتعرضها في Template. لكن لو هتبعت البيانات لـ Backend، لازم تعمل Sanitization على السيرفر كمان. متعتمدش على Client-side validation بس - دايماً تحقق على السيرفر.

إزاي أعمل Dynamic Fields (حقول تضاف/تحذف ديناميكياً)؟

في Reactive Forms، استخدم FormArray. مثلاً لو عندك نموذج بيضيف أرقام تليفون: phones: this.fb.array([this.fb.control('')]). وبعدين تقدر تضيف بـ this.phones.push(this.fb.control('')) وتحذف بـ this.phones.removeAt(index).

← الدرس السابق