أساسيات JavaScript

التعامل مع النماذج والتحقق من صحتها

45 دقيقة الدرس 26 من 60

الوصول الى عناصر النموذج في JavaScript

قبل ان تتمكن من التحقق من نموذج او معالجته، تحتاج لمعرفة كيفية الوصول الى عناصره. يوفر JavaScript طرقا متعددة للاشارة الى النماذج ومدخلاتها. الطريقة الاكثر موثوقية هي استخدام مجموعة document.forms او معرفات العناصر او خاصية elements لكائن النموذج. لكل طريقة حالة استخدامها، وفهمها جميعا يجعلك مطورا اكثر تنوعا.

مثال: طرق مختلفة للوصول الى عناصر النموذج

<form id="signup-form" name="signup">
    <input type="text" id="username" name="username">
    <input type="email" id="email" name="email">
    <input type="password" id="password" name="password">
    <button type="submit">تسجيل</button>
</form>

<script>
// الطريقة 1: بالمعرف (الاكثر شيوعا وموثوقية)
const form = document.getElementById('signup-form');

// الطريقة 2: استخدام مجموعة document.forms
const formByName = document.forms['signup'];    // بسمة name
const formByIndex = document.forms[0];           // بالفهرس

// الوصول للمدخلات الفردية عبر مجموعة form.elements
const usernameInput = form.elements['username']; // بسمة name
const emailInput = form.elements['email'];
const passwordInput = form.elements['password'];

// الوصول بالمعرف مباشرة
const usernameById = document.getElementById('username');

// خاصية elements تدعم ايضا الوصول بالفهرس
const firstInput = form.elements[0];  // اول مدخل في النموذج

// الحصول على العدد الاجمالي لعناصر التحكم
console.log('النموذج يحتوي على', form.elements.length, 'عنصر');

// التكرار عبر جميع عناصر النموذج
for (let i = 0; i < form.elements.length; i++) {
    const el = form.elements[i];
    console.log(el.name, el.type, el.value);
}
</script>
نصيحة احترافية: استخدام form.elements['name'] مفضل على document.getElementById() عند العمل مع النماذج لانه يحصر البحث في ذلك النموذج المحدد. هذا يمنع التعارضات عندما تحتوي الصفحة على نماذج متعددة بحقول متشابهة الاسماء.

ارسال النموذج: حدث submit

ينطلق حدث submit عندما يرسل المستخدم نموذجا، سواء بالنقر على زر الارسال او الضغط على Enter داخل حقل نصي. ينطلق هذا الحدث على عنصر <form> وليس على زر الارسال. اهم شيء يجب فهمه حول ارسال النموذج هو ان المتصفح سيحاول الانتقال الى عنوان action الخاص بالنموذج بشكل افتراضي. للتعامل مع الارسال باستخدام JavaScript، يجب استدعاء event.preventDefault() لايقاف هذا السلوك الافتراضي.

مثال: التعامل مع ارسال النموذج

<form id="contact-form" action="/api/contact" method="POST">
    <label for="name">الاسم:</label>
    <input type="text" id="name" name="name" required>

    <label for="message">الرسالة:</label>
    <textarea id="message" name="message" required></textarea>

    <button type="submit">ارسال</button>
</form>

<script>
const form = document.getElementById('contact-form');

form.addEventListener('submit', function(e) {
    // منع المتصفح من الانتقال الى عنوان action
    e.preventDefault();

    // الان يمكنك معالجة بيانات النموذج باستخدام JavaScript
    const name = form.elements['name'].value;
    const message = form.elements['message'].value;

    console.log('الاسم:', name);
    console.log('الرسالة:', message);

    // ارسال البيانات عبر fetch API بدلا من ذلك
    fetch('/api/contact', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ name: name, message: message })
    })
    .then(function(response) { return response.json(); })
    .then(function(data) {
        console.log('نجاح:', data);
        form.reset(); // مسح النموذج بعد الارسال الناجح
    })
    .catch(function(error) {
        console.error('خطا:', error);
    });
});
</script>
تحذير مهم: استدعِ دائما e.preventDefault() في بداية معالج الارسال، قبل اي منطق للتحقق. اذا القى كود التحقق خطا قبل الوصول الى preventDefault()، سيرسل النموذج بشكل طبيعي وينتقل بعيدا عن الصفحة، مما يفقدك اي رسائل خطا كنت تنوي عرضها.

واجهة FormData

توفر واجهة FormData طريقة مريحة لالتقاط جميع قيم النموذج دفعة واحدة بدون قراءة كل مدخل يدويا. تتعامل تلقائيا مع انواع المدخلات المختلفة بما في ذلك تحميل الملفات ومربعات الاختيار وعناصر التحديد. يمكن ارسال كائنات FormData مباشرة مع Fetch API او XMLHttpRequest.

مثال: استخدام FormData لجمع قيم النموذج

<form id="profile-form">
    <input type="text" name="firstName" value="احمد">
    <input type="text" name="lastName" value="محمد">
    <input type="email" name="email" value="ahmed@example.com">
    <select name="role">
        <option value="developer" selected>مطور</option>
        <option value="designer">مصمم</option>
    </select>
    <input type="file" name="avatar">
    <button type="submit">حفظ الملف الشخصي</button>
</form>

<script>
document.getElementById('profile-form').addEventListener('submit', function(e) {
    e.preventDefault();

    // انشاء FormData من عنصر النموذج
    const formData = new FormData(this);

    // قراءة القيم الفردية
    console.log('الاسم الاول:', formData.get('firstName'));
    console.log('البريد:', formData.get('email'));

    // التحقق من وجود حقل
    console.log('يحتوي على الدور:', formData.has('role'));

    // التكرار عبر جميع المدخلات
    for (const [key, value] of formData.entries()) {
        console.log(key + ': ' + value);
    }

    // التحويل الى كائن عادي (باستثناء الملفات)
    const dataObject = Object.fromEntries(formData.entries());
    console.log('بيانات النموذج ككائن:', dataObject);

    // اضافة بيانات اضافية غير موجودة في النموذج
    formData.append('submittedAt', new Date().toISOString());

    // الارسال مع fetch -- FormData يضبط Content-Type تلقائيا
    fetch('/api/profile', {
        method: 'POST',
        body: formData  // لا تضبط ترويسة Content-Type يدويا
    });
});
</script>
ملاحظة: عند ارسال FormData مع fetch()، لا تضبط ترويسة Content-Type يدويا. سيضبطها المتصفح تلقائيا الى multipart/form-data مع سلسلة الحدود الصحيحة اللازمة لتحميل الملفات. ضبطها يدويا سيكسر الطلب.

انواع المدخلات وقيمها

تعيد انواع مدخلات HTML المختلفة قيمها بتنسيقات مختلفة. فهم هذه الاختلافات ضروري للتعامل الصحيح مع النماذج والتحقق منها.

مثال: قراءة القيم من انواع مدخلات مختلفة

<form id="demo-form">
    <input type="text" name="username" value="ahmed">
    <input type="number" name="age" value="25">
    <input type="range" name="volume" min="0" max="100" value="75">
    <input type="date" name="birthday" value="1998-05-15">
    <input type="time" name="alarm" value="07:30">
    <input type="color" name="favorite" value="#3498db">
    <input type="url" name="website" value="https://example.com">
    <input type="hidden" name="userId" value="12345">
    <textarea name="bio">مرحبا بالعالم</textarea>
</form>

<script>
const form = document.getElementById('demo-form');

// جميع خصائص .value تعيد نصوصا، حتى لـ number و range
const age = form.elements['age'].value;       // "25" (نص!)
const volume = form.elements['volume'].value;  // "75" (نص!)

// التحويل الى ارقام عند الحاجة
const ageNumber = parseInt(form.elements['age'].value, 10);     // 25
const volumeNumber = parseFloat(form.elements['volume'].value); // 75

// بديل: استخدام valueAsNumber لمدخلات number و range و date
const ageFromProp = form.elements['age'].valueAsNumber;     // 25
const dateFromProp = form.elements['birthday'].valueAsDate;  // كائن Date

// قيمة textarea تعمل مثل مدخل النص
const bio = form.elements['bio'].value;  // "مرحبا بالعالم"

// المدخلات المخفية يمكن الوصول اليها مثل المرئية
const userId = form.elements['userId'].value;  // "12345"

console.log(typeof age);        // "string"
console.log(typeof ageNumber);  // "number"
</script>

مربعات الاختيار وازرار الراديو

تتطلب مربعات الاختيار وازرار الراديو تعاملا خاصا لان قيمتها لا تتغير -- فقط حالة التحديد تتغير. يجب استخدام خاصية checked (قيمة منطقية) بدلا من خاصية value لتحديد اختيار المستخدم.

مثال: العمل مع مربعات الاختيار وازرار الراديو

<form id="preferences-form">
    <fieldset>
        <legend>الاشعارات</legend>
        <label>
            <input type="checkbox" name="notifications" value="email" checked> البريد
        </label>
        <label>
            <input type="checkbox" name="notifications" value="sms"> الرسائل النصية
        </label>
        <label>
            <input type="checkbox" name="notifications" value="push" checked> الاشعارات الفورية
        </label>
    </fieldset>

    <fieldset>
        <legend>المظهر</legend>
        <label>
            <input type="radio" name="theme" value="light" checked> فاتح
        </label>
        <label>
            <input type="radio" name="theme" value="dark"> داكن
        </label>
        <label>
            <input type="radio" name="theme" value="auto"> تلقائي
        </label>
    </fieldset>

    <label>
        <input type="checkbox" name="terms" value="accepted"> اوافق على الشروط
    </label>

    <button type="submit">حفظ</button>
</form>

<script>
document.getElementById('preferences-form').addEventListener('submit', function(e) {
    e.preventDefault();

    // مربع اختيار واحد: تحقق من خاصية .checked المنطقية
    const termsAccepted = form.elements['terms'].checked;  // true او false
    console.log('تم قبول الشروط:', termsAccepted);

    // مربعات اختيار متعددة بنفس الاسم: الحصول على جميع القيم المحددة
    const checkboxes = form.querySelectorAll('input[name="notifications"]:checked');
    const selectedNotifications = Array.from(checkboxes).map(function(cb) {
        return cb.value;
    });
    console.log('الاشعارات:', selectedNotifications); // ["email", "push"]

    // ازرار الراديو: العثور على المحدد
    const selectedTheme = form.elements['theme'].value; // قيمة الراديو المحدد
    console.log('المظهر:', selectedTheme); // "light"

    // بديل للراديو: استخدام querySelector
    const checkedRadio = form.querySelector('input[name="theme"]:checked');
    console.log('المظهر (بديل):', checkedRadio ? checkedRadio.value : 'لا شيء');
});

// الاستماع للتغييرات على مربعات اختيار فردية
const termsCheckbox = document.querySelector('input[name="terms"]');
termsCheckbox.addEventListener('change', function() {
    console.log('مربع الشروط تغير الى:', this.checked);
});
</script>

عناصر التحديد

يمكن ان تكون عناصر التحديد للاختيار الفردي او المتعدد. عناصر الاختيار الفردي تعيد قيمة نصية، بينما عناصر الاختيار المتعدد تتطلب التكرار عبر الخيارات المحددة.

مثال: العمل مع عناصر التحديد

<form id="order-form">
    <label for="country">الدولة:</label>
    <select id="country" name="country">
        <option value="">-- اختر --</option>
        <option value="sa">المملكة العربية السعودية</option>
        <option value="eg" selected>مصر</option>
        <option value="ae">الامارات</option>
    </select>

    <label for="languages">اللغات:</label>
    <select id="languages" name="languages" multiple size="4">
        <option value="js" selected>JavaScript</option>
        <option value="py">Python</option>
        <option value="rb" selected>Ruby</option>
        <option value="go">Go</option>
    </select>
</form>

<script>
const form = document.getElementById('order-form');
const countrySelect = form.elements['country'];
const languagesSelect = form.elements['languages'];

// تحديد فردي: .value تعيد قيمة الخيار المحدد
console.log('الدولة:', countrySelect.value); // "eg"

// الحصول على نص العرض للخيار المحدد
const selectedIndex = countrySelect.selectedIndex;
const selectedText = countrySelect.options[selectedIndex].text;
console.log('اسم الدولة:', selectedText); // "مصر"

// تحديد متعدد: التكرار عبر الخيارات للعثور على المحددة
const selectedLanguages = Array.from(languagesSelect.selectedOptions).map(function(opt) {
    return opt.value;
});
console.log('اللغات:', selectedLanguages); // ["js", "rb"]

// الاستماع للتغييرات
countrySelect.addEventListener('change', function() {
    console.log('تغيرت الدولة الى:', this.value);
    console.log('نص العرض:', this.options[this.selectedIndex].text);
});

// اضافة خيارات ديناميكيا
const newOption = document.createElement('option');
newOption.value = 'jo';
newOption.textContent = 'الاردن';
countrySelect.appendChild(newOption);

// ضبط القيمة برمجيا
countrySelect.value = 'ae'; // يحدد الامارات
</script>

التحقق في الوقت الفعلي مع احداث input و change

يتوقع المستخدمون ملاحظات فورية اثناء ملء النماذج. حدث input ينطلق في كل مرة تتغير فيها القيمة (عند كل ضغطة مفتاح للمدخلات النصية)، بينما حدث change ينطلق عندما ينتهي المستخدم من التحرير ويبتعد عن الحقل. استخدام هذين الحدثين معا يمنحك تحكما دقيقا في وقت التحقق وعرض الملاحظات.

مثال: ملاحظات التحقق في الوقت الفعلي

<form id="realtime-form">
    <div class="field-group">
        <label for="rt-username">اسم المستخدم:</label>
        <input type="text" id="rt-username" name="username"
               minlength="3" maxlength="20" required>
        <span class="error-message" id="username-error"></span>
        <span class="char-count" id="username-count">0/20</span>
    </div>

    <div class="field-group">
        <label for="rt-email">البريد الالكتروني:</label>
        <input type="email" id="rt-email" name="email" required>
        <span class="error-message" id="email-error"></span>
    </div>

    <div class="field-group">
        <label for="rt-password">كلمة المرور:</label>
        <input type="password" id="rt-password" name="password"
               minlength="8" required>
        <div class="password-strength" id="password-strength"></div>
        <span class="error-message" id="password-error"></span>
    </div>
</form>

<script>
const usernameInput = document.getElementById('rt-username');
const emailInput = document.getElementById('rt-email');
const passwordInput = document.getElementById('rt-password');

// حدث input: ينطلق عند كل ضغطة مفتاح (وقت فعلي)
usernameInput.addEventListener('input', function() {
    const count = this.value.length;
    document.getElementById('username-count').textContent = count + '/20';

    if (count > 0 && count < 3) {
        showError('username-error', 'اسم المستخدم يجب ان يكون 3 احرف على الاقل');
        this.classList.add('invalid');
        this.classList.remove('valid');
    } else if (count >= 3) {
        clearError('username-error');
        this.classList.add('valid');
        this.classList.remove('invalid');
    } else {
        clearError('username-error');
        this.classList.remove('valid', 'invalid');
    }
});

// حدث change: ينطلق عندما يغادر المستخدم الحقل
emailInput.addEventListener('change', function() {
    if (this.value && !isValidEmail(this.value)) {
        showError('email-error', 'الرجاء ادخال بريد الكتروني صحيح');
        this.classList.add('invalid');
    } else if (this.value) {
        clearError('email-error');
        this.classList.add('valid');
        this.classList.remove('invalid');
    }
});

// دمج حدث input لمقياس قوة كلمة المرور
passwordInput.addEventListener('input', function() {
    const strength = calculatePasswordStrength(this.value);
    const strengthEl = document.getElementById('password-strength');
    strengthEl.textContent = 'القوة: ' + strength.label;
    strengthEl.className = 'password-strength ' + strength.level;
});

function calculatePasswordStrength(password) {
    let score = 0;
    if (password.length >= 8) score++;
    if (password.length >= 12) score++;
    if (/[A-Z]/.test(password)) score++;
    if (/[0-9]/.test(password)) score++;
    if (/[^A-Za-z0-9]/.test(password)) score++;

    if (score <= 1) return { label: 'ضعيفة', level: 'weak' };
    if (score <= 3) return { label: 'متوسطة', level: 'medium' };
    return { label: 'قوية', level: 'strong' };
}

function isValidEmail(email) {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

function showError(elementId, message) {
    document.getElementById(elementId).textContent = message;
}

function clearError(elementId) {
    document.getElementById(elementId).textContent = '';
}
</script>
نصيحة احترافية: استخدم حدث input لعدادات الاحرف والبحث اثناء الكتابة ومؤشرات قوة كلمة المرور حيث تحسن الملاحظات الفورية التجربة. استخدم حدث change للتحقق الذي لا يجب ان يقاطع المستخدم اثناء الكتابة، مثل التحقق من تنسيق البريد الالكتروني او فحوصات التفرد عبر API.

واجهة التحقق من القيود

توفر المتصفحات الحديثة واجهة تحقق من القيود مدمجة تعمل مع سمات التحقق في HTML5 مثل required وminlength وpattern وtype. تمنحك هذه الواجهة وصولا برمجيا لحالة التحقق من كل مدخل والقدرة على تخصيص رسائل الخطا. الخصائص والتوابع الاساسية هي validity وcheckValidity() وsetCustomValidity() وreportValidity().

خاصية validity

كل مدخل نموذج لديه خاصية validity تعيد كائن ValidityState. يحتوي هذا الكائن على علامات منطقية لكل نوع من اخطاء التحقق. فحص هذه العلامات يخبرك بالضبط ما هو الخطا في المدخل.

مثال: فحص كائن ValidityState

<form id="validity-demo">
    <input type="email" id="demo-email" required minlength="5" maxlength="50">
    <input type="number" id="demo-age" min="18" max="120" step="1">
    <input type="text" id="demo-zip" pattern="[0-9]{5}" title="رمز بريدي من 5 ارقام">
    <button type="submit">تحقق</button>
</form>

<script>
const emailField = document.getElementById('demo-email');

// خاصية validity تحتوي على هذه العلامات المنطقية:
// valueMissing    -- فشل قيد 'required'
// typeMismatch    -- فشل قيد 'type' (مثلا ليس بريدا صحيحا)
// patternMismatch -- فشل قيد 'pattern'
// tooLong         -- تجاوز 'maxlength'
// tooShort        -- اقل من 'minlength'
// rangeOverflow   -- تجاوز 'max' لـ number/date
// rangeUnderflow  -- اقل من 'min' لـ number/date
// stepMismatch    -- لا يتطابق مع قيمة 'step'
// badInput        -- المتصفح لا يستطيع تحويل المدخل
// customError     -- تم استدعاء setCustomValidity()
// valid           -- true اذا نجحت جميع القيود

emailField.addEventListener('input', function() {
    const v = this.validity;

    console.log('صحيح:', v.valid);
    console.log('قيمة مفقودة:', v.valueMissing);
    console.log('نوع غير متطابق:', v.typeMismatch);
    console.log('قصير جدا:', v.tooShort);

    // استخدم validationMessage لرسالة الخطا الافتراضية للمتصفح
    if (!v.valid) {
        console.log('خطا:', this.validationMessage);
    }
});
</script>

checkValidity() و reportValidity()

تابع checkValidity() يعيد true اذا استوفى العنصر جميع القيود، او false خلاف ذلك. يطلق ايضا حدث invalid على العناصر التي تفشل في التحقق. تابع reportValidity() يفعل نفس الشيء لكنه يعرض ايضا تلميح الخطا المدمج في المتصفح للمستخدم.

مثال: استخدام checkValidity و reportValidity

<form id="check-form">
    <input type="text" id="check-name" required minlength="2">
    <input type="email" id="check-email" required>
    <button type="button" id="validate-btn">تحقق</button>
    <button type="submit">ارسال</button>
</form>

<script>
const checkForm = document.getElementById('check-form');

// checkValidity على حقل فردي -- يعيد قيمة منطقية بصمت
document.getElementById('validate-btn').addEventListener('click', function() {
    const nameField = document.getElementById('check-name');
    const emailField = document.getElementById('check-email');

    // التحقق من الحقول الفردية
    const nameValid = nameField.checkValidity();
    const emailValid = emailField.checkValidity();
    console.log('الاسم صحيح:', nameValid, 'البريد صحيح:', emailValid);

    // التحقق من النموذج بالكامل دفعة واحدة
    const formValid = checkForm.checkValidity();
    console.log('النموذج صحيح:', formValid);

    // reportValidity يعرض تلميح المتصفح على اول حقل غير صحيح
    if (!formValid) {
        checkForm.reportValidity();
    }
});

// الاستماع لحدث invalid (يطلقه checkValidity على العناصر الفاشلة)
document.getElementById('check-name').addEventListener('invalid', function(e) {
    console.log('حقل الاسم غير صحيح:', this.validationMessage);
});

checkForm.addEventListener('submit', function(e) {
    e.preventDefault();
    if (this.checkValidity()) {
        console.log('النموذج صحيح -- جاري الارسال');
    } else {
        console.log('النموذج يحتوي على اخطاء');
        this.reportValidity();
    }
});
</script>

setCustomValidity()

يتيح لك تابع setCustomValidity() تحديد رسائل خطا مخصصة تحل محل رسائل المتصفح الافتراضية. عند تعيين نص غير فارغ، يتم تمييز الحقل على انه غير صحيح. يجب اعادة تعيينه الى نص فارغ لمسح الخطا المخصص والسماح للحقل بان يكون صحيحا مرة اخرى.

مثال: رسائل تحقق مخصصة

<form id="custom-form">
    <label for="custom-pw">كلمة المرور:</label>
    <input type="password" id="custom-pw" name="password" required minlength="8">

    <label for="custom-confirm">تاكيد كلمة المرور:</label>
    <input type="password" id="custom-confirm" name="confirmPassword" required>

    <button type="submit">تسجيل</button>
</form>

<script>
const password = document.getElementById('custom-pw');
const confirm = document.getElementById('custom-confirm');

// رسالة مخصصة لحقل كلمة المرور
password.addEventListener('input', function() {
    if (this.value.length > 0 && this.value.length < 8) {
        this.setCustomValidity('كلمة المرور يجب ان تكون 8 احرف على الاقل.');
    } else if (this.value && !/[A-Z]/.test(this.value)) {
        this.setCustomValidity('كلمة المرور يجب ان تحتوي على حرف كبير واحد على الاقل.');
    } else if (this.value && !/[0-9]/.test(this.value)) {
        this.setCustomValidity('كلمة المرور يجب ان تحتوي على رقم واحد على الاقل.');
    } else {
        this.setCustomValidity(''); // مسح الخطا -- الحقل صحيح
    }
});

// تطابق تاكيد كلمة المرور
confirm.addEventListener('input', function() {
    if (this.value !== password.value) {
        this.setCustomValidity('كلمات المرور غير متطابقة.');
    } else {
        this.setCustomValidity('');
    }
});

// اعادة فحص التاكيد عند تغيير كلمة المرور
password.addEventListener('input', function() {
    if (confirm.value && confirm.value !== this.value) {
        confirm.setCustomValidity('كلمات المرور غير متطابقة.');
    } else {
        confirm.setCustomValidity('');
    }
});

document.getElementById('custom-form').addEventListener('submit', function(e) {
    e.preventDefault();
    if (this.checkValidity()) {
        console.log('التسجيل ناجح');
    } else {
        this.reportValidity();
    }
});
</script>
تحذير حاسم: يجب استدعاء setCustomValidity('') بنص فارغ لمسح الخطا المخصص. اذا نسيت هذه الخطوة، سيبقى الحقل غير صحيح بشكل دائم حتى لو صحح المستخدم مدخلاته. امسح دائما صحة التخصيص قبل اعادة الفحص في معالج الاحداث.

التحقق بالتعبيرات المنتظمة

توفر التعبيرات المنتظمة مطابقة انماط قوية للتحقق من النماذج. بينما يمتلك HTML5 سمة pattern التي تقبل تعبيرات منتظمة، يمنحك JavaScript مرونة اكبر لقواعد التحقق المعقدة ورسائل الخطا المخصصة.

مثال: انماط التعبيرات المنتظمة للتحققات الشائعة

<form id="regex-form">
    <input type="text" id="phone" name="phone" placeholder="(555) 123-4567">
    <input type="text" id="zipcode" name="zipcode" placeholder="12345 او 12345-6789">
    <input type="text" id="website" name="website" placeholder="https://example.com">
    <input type="text" id="slug" name="slug" placeholder="my-page-slug">
    <button type="submit">تحقق</button>
</form>

<script>
const validationRules = {
    phone: {
        pattern: /^(\(\d{3}\)\s?|\d{3}[-.]?)\d{3}[-.]?\d{4}$/,
        message: 'ادخل رقم هاتف امريكي صحيح'
    },
    zipcode: {
        pattern: /^\d{5}(-\d{4})?$/,
        message: 'ادخل رمزا بريديا صحيحا (12345 او 12345-6789)'
    },
    website: {
        pattern: /^https?:\/\/[a-zA-Z0-9][\w.-]*\.[a-zA-Z]{2,}(\/\S*)?$/,
        message: 'ادخل رابطا صحيحا يبدا بـ http:// او https://'
    },
    slug: {
        pattern: /^[a-z0-9]+(-[a-z0-9]+)*$/,
        message: 'مسموح فقط بالاحرف الصغيرة والارقام والشرطات'
    }
};

document.getElementById('regex-form').addEventListener('submit', function(e) {
    e.preventDefault();
    let isValid = true;

    Object.keys(validationRules).forEach(function(fieldName) {
        const input = document.getElementById(fieldName);
        const rule = validationRules[fieldName];

        if (input.value && !rule.pattern.test(input.value)) {
            input.setCustomValidity(rule.message);
            isValid = false;
        } else {
            input.setCustomValidity('');
        }
    });

    if (!isValid) {
        this.reportValidity();
    } else {
        console.log('جميع الحقول صحيحة');
    }
});

// مسح الصحة المخصصة عند الادخال ليتمكن المستخدم من اصلاح الاخطاء
document.querySelectorAll('#regex-form input').forEach(function(input) {
    input.addEventListener('input', function() {
        this.setCustomValidity('');
    });
});
</script>

بناء نموذج تسجيل كامل مع التحقق

الان لنجمع كل شيء معا في نموذج تسجيل كامل وجاهز للانتاج. هذا المثال يجمع بين التحقق في الوقت الفعلي وواجهة التحقق من القيود وانماط التعبيرات المنتظمة ورسائل الخطا المخصصة وعرض الاخطاء القابل للوصول. يوضح النمط الكامل الذي ستستخدمه في تطبيق حقيقي.

مثال: نموذج تسجيل كامل

<style>
.form-group { margin-bottom: 16px; }
.form-group label { display: block; margin-bottom: 4px; font-weight: bold; }
.form-group input, .form-group select { width: 100%; padding: 8px; border: 2px solid #ccc; }
.form-group input.valid { border-color: #27ae60; }
.form-group input.invalid { border-color: #e74c3c; }
.error-text { color: #e74c3c; font-size: 14px; margin-top: 4px; display: block; }
.error-text:empty { display: none; }
.form-success { background: #d4edda; color: #155724; padding: 16px; display: none; }
</style>

<div id="success-message" class="form-success" role="alert">
    تم التسجيل بنجاح! مرحبا بك.
</div>

<form id="registration-form" novalidate>
    <div class="form-group">
        <label for="reg-name">الاسم الكامل *</label>
        <input type="text" id="reg-name" name="fullName"
               required minlength="2" maxlength="100"
               autocomplete="name"
               aria-describedby="name-error">
        <span class="error-text" id="name-error" role="alert"></span>
    </div>

    <div class="form-group">
        <label for="reg-email">البريد الالكتروني *</label>
        <input type="email" id="reg-email" name="email"
               required autocomplete="email"
               aria-describedby="email-error">
        <span class="error-text" id="email-error" role="alert"></span>
    </div>

    <div class="form-group">
        <label for="reg-phone">رقم الهاتف</label>
        <input type="tel" id="reg-phone" name="phone"
               autocomplete="tel"
               aria-describedby="phone-error">
        <span class="error-text" id="phone-error" role="alert"></span>
    </div>

    <div class="form-group">
        <label for="reg-password">كلمة المرور *</label>
        <input type="password" id="reg-password" name="password"
               required minlength="8" autocomplete="new-password"
               aria-describedby="password-error password-hint">
        <small id="password-hint">8 احرف على الاقل مع حرف كبير ورقم ورمز.</small>
        <span class="error-text" id="password-error" role="alert"></span>
    </div>

    <div class="form-group">
        <label for="reg-confirm">تاكيد كلمة المرور *</label>
        <input type="password" id="reg-confirm" name="confirmPassword"
               required autocomplete="new-password"
               aria-describedby="confirm-error">
        <span class="error-text" id="confirm-error" role="alert"></span>
    </div>

    <div class="form-group">
        <label for="reg-country">الدولة *</label>
        <select id="reg-country" name="country" required
                aria-describedby="country-error">
            <option value="">-- اختر دولتك --</option>
            <option value="sa">المملكة العربية السعودية</option>
            <option value="eg">مصر</option>
            <option value="ae">الامارات</option>
            <option value="jo">الاردن</option>
        </select>
        <span class="error-text" id="country-error" role="alert"></span>
    </div>

    <div class="form-group">
        <label>
            <input type="checkbox" id="reg-terms" name="terms" required
                   aria-describedby="terms-error">
            اوافق على شروط الخدمة *
        </label>
        <span class="error-text" id="terms-error" role="alert"></span>
    </div>

    <button type="submit" id="submit-btn">انشاء الحساب</button>
</form>

<script>
const regForm = document.getElementById('registration-form');

// قواعد التحقق لكل حقل
const validators = {
    fullName: function(value) {
        if (!value.trim()) return 'الاسم الكامل مطلوب.';
        if (value.trim().length < 2) return 'الاسم يجب ان يكون حرفين على الاقل.';
        return '';
    },
    email: function(value) {
        if (!value.trim()) return 'البريد الالكتروني مطلوب.';
        if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return 'الرجاء ادخال بريد الكتروني صحيح.';
        return '';
    },
    phone: function(value) {
        if (!value) return '';
        if (!/^[\d\s()+-]{7,20}$/.test(value)) return 'الرجاء ادخال رقم هاتف صحيح.';
        return '';
    },
    password: function(value) {
        if (!value) return 'كلمة المرور مطلوبة.';
        if (value.length < 8) return 'كلمة المرور يجب ان تكون 8 احرف على الاقل.';
        if (!/[A-Z]/.test(value)) return 'يجب ان تحتوي على حرف كبير واحد على الاقل.';
        if (!/[0-9]/.test(value)) return 'يجب ان تحتوي على رقم واحد على الاقل.';
        if (!/[^A-Za-z0-9]/.test(value)) return 'يجب ان تحتوي على رمز خاص واحد على الاقل.';
        return '';
    },
    confirmPassword: function(value) {
        if (!value) return 'الرجاء تاكيد كلمة المرور.';
        var pw = document.getElementById('reg-password').value;
        if (value !== pw) return 'كلمات المرور غير متطابقة.';
        return '';
    },
    country: function(value) {
        if (!value) return 'الرجاء اختيار دولتك.';
        return '';
    },
    terms: function(value, element) {
        if (!element.checked) return 'يجب الموافقة على شروط الخدمة.';
        return '';
    }
};

// ربط اسماء الحقول بمعرفات عناصر الخطا
const errorMap = {
    fullName: 'name-error', email: 'email-error',
    phone: 'phone-error', password: 'password-error',
    confirmPassword: 'confirm-error', country: 'country-error',
    terms: 'terms-error'
};

// ربط اسماء الحقول بمعرفات المدخلات
const inputMap = {
    fullName: 'reg-name', email: 'reg-email',
    phone: 'reg-phone', password: 'reg-password',
    confirmPassword: 'reg-confirm', country: 'reg-country',
    terms: 'reg-terms'
};

function validateField(fieldName) {
    var input = document.getElementById(inputMap[fieldName]);
    var errorEl = document.getElementById(errorMap[fieldName]);
    var errorMsg = validators[fieldName](input.value, input);
    errorEl.textContent = errorMsg;
    if (errorMsg) {
        input.classList.add('invalid');
        input.classList.remove('valid');
        input.setAttribute('aria-invalid', 'true');
    } else if (input.value || input.checked) {
        input.classList.add('valid');
        input.classList.remove('invalid');
        input.setAttribute('aria-invalid', 'false');
    }
    return errorMsg === '';
}

// ربط التحقق في الوقت الفعلي بكل حقل
Object.keys(inputMap).forEach(function(fieldName) {
    var input = document.getElementById(inputMap[fieldName]);
    var eventType = (input.type === 'checkbox' || input.tagName === 'SELECT')
                    ? 'change' : 'input';
    input.addEventListener(eventType, function() {
        validateField(fieldName);
        if (fieldName === 'password') {
            var confirmVal = document.getElementById('reg-confirm').value;
            if (confirmVal) validateField('confirmPassword');
        }
    });
});

// التعامل مع ارسال النموذج
regForm.addEventListener('submit', function(e) {
    e.preventDefault();
    var formIsValid = true;
    var firstInvalidField = null;

    Object.keys(validators).forEach(function(fieldName) {
        var isValid = validateField(fieldName);
        if (!isValid && !firstInvalidField) {
            firstInvalidField = document.getElementById(inputMap[fieldName]);
        }
        if (!isValid) formIsValid = false;
    });

    if (!formIsValid) {
        if (firstInvalidField) firstInvalidField.focus();
        return;
    }

    var formData = new FormData(this);
    var data = Object.fromEntries(formData.entries());
    console.log('جاري الارسال:', data);

    regForm.style.display = 'none';
    document.getElementById('success-message').style.display = 'block';
});
</script>

رسائل الخطا القابلة للوصول

يجب ان يكون التحقق من النماذج متاحا لجميع المستخدمين، بما في ذلك مستخدمي قارئات الشاشة. هناك عدة تقنيات اساسية تجعل رسائل الخطا متاحة وتساعد المستخدمين على فهم مشاكل التحقق واصلاحها بكفاءة.

  • استخدم aria-describedby -- اربط كل مدخل بعنصر رسالة الخطا الخاص به باستخدام aria-describedby. عندما يحصل المدخل على التركيز، ستعلن قارئة الشاشة عن رسالة الخطا مع تسمية المدخل.
  • استخدم aria-invalid -- اضبط aria-invalid="true" على المدخلات التي تفشل في التحقق. هذا يخبر قارئات الشاشة ان الحقل يحتوي على خطا. ازله او اضبطه على "false" عند تصحيح الخطا.
  • استخدم role="alert" -- اضافة role="alert" الى عناصر رسائل الخطا يجعل قارئات الشاشة تعلن عن الرسالة فورا عند تغييرها، بدون حاجة المستخدم للانتقال اليها.
  • ادارة التركيز -- عند فشل ارسال النموذج، انقل التركيز الى اول حقل غير صحيح. هذا يوجه المستخدم ويتيح له البدء في تصحيح الاخطاء فورا.
  • رسائل خطا مرئية -- لا تعتمد فقط على اللون للاشارة الى الاخطاء. اضف دائما رسالة نصية تصف المشكلة وكيفية اصلاحها.

مثال: نمط رسالة خطا قابلة للوصول

<!-- النمط الكامل القابل للوصول لحقل نموذج -->
<div class="form-group">
    <label for="acc-email">
        البريد الالكتروني
        <span aria-hidden="true">*</span>
        <span class="sr-only">(مطلوب)</span>
    </label>

    <input type="email" id="acc-email" name="email"
           required
           aria-required="true"
           aria-invalid="false"
           aria-describedby="email-help email-err">

    <small id="email-help">لن نشارك بريدك الالكتروني ابدا.</small>

    <!-- role="alert" يجعل قارئات الشاشة تعلن عن التغييرات فورا -->
    <span id="email-err" class="error-text" role="alert"></span>
</div>

<script>
function setFieldError(inputId, errorId, message) {
    const input = document.getElementById(inputId);
    const errorEl = document.getElementById(errorId);

    if (message) {
        errorEl.textContent = message;
        input.setAttribute('aria-invalid', 'true');
        input.classList.add('invalid');
    } else {
        errorEl.textContent = '';
        input.setAttribute('aria-invalid', 'false');
        input.classList.remove('invalid');
    }
}

// عند تعيين الخطا، تعلن قارئة الشاشة عنه فورا
// لان عنصر الخطا يحتوي على role="alert"
setFieldError('acc-email', 'email-err', 'الرجاء ادخال بريد الكتروني صحيح.');

// عند مسح الخطا
setFieldError('acc-email', 'email-err', '');
</script>
ملاحظة: سمة novalidate على عنصر <form> تعطل تلميحات التحقق المدمجة في المتصفح، مما يمنحك تحكما كاملا في عرض الاخطاء. لكن واجهة التحقق من القيود (validity وcheckValidity()) لا تزال تعمل -- فقط عرض التلميح التلقائي يتم كبحه. استخدم novalidate عندما تريد رسائل خطا مخصصة التنسيق بدلا من الافتراضية للمتصفح.

تمرين عملي

ابنِ نموذج تسجيل متعدد الخطوات بثلاث خطوات. الخطوة 1 تجمع المعلومات الشخصية (الاسم الكامل والبريد الالكتروني ورقم الهاتف). الخطوة 2 تجمع معلومات الحساب (اسم المستخدم مع فحص التفرد في الوقت الفعلي مقابل مصفوفة محلية، وكلمة المرور مع مقياس القوة، وتاكيد كلمة المرور). الخطوة 3 تجمع التفضيلات (تحديد الدولة ومربعات اختيار الاشعارات للبريد والرسائل النصية والاشعارات الفورية ومربع اختيار الموافقة على الشروط). كل خطوة يجب ان تكون <fieldset> تظهر وتختفي مع تقدم المستخدم. اضف ازرار "التالي" و"السابق" للتنقل بين الخطوات. تحقق من كل خطوة قبل السماح للمستخدم بالانتقال للتالية. استخدم واجهة التحقق من القيود مع setCustomValidity() لمطابقة كلمة المرور والقواعد المخصصة. استخدم التعبيرات المنتظمة للتحقق من تنسيق رقم الهاتف وتنسيق اسم المستخدم (احرف صغيرة وارقام فقط). اعرض جميع الاخطاء مضمنة مع aria-describedby وaria-invalid وrole="alert" لامكانية الوصول بقارئة الشاشة. اضف عداد احرف لحقل اسم المستخدم يتحدث عند كل ضغطة مفتاح. في الخطوة الاخيرة، اعرض ملخصا لجميع البيانات المدخلة قبل ان يؤكد المستخدم الارسال. استخدم واجهة FormData لجمع جميع القيم وتسجيلها في وحدة التحكم عند الارسال.