النماذج والتحقق ومدخلات المستخدم

منسقات الإدخال: تقييد النص وتشكيله

15 دقيقة الدرس 4 من 12

منسقات الإدخال: تقييد النص وتشكيله

تقبل كل من TextField وTextFormField في Flutter معاملًا اسمه inputFormatters — وهو قائمة من كائنات TextInputFormatter تعترض كل ضغطة مفتاح وتستطيع تصفية القيمة أو استبدالها أو إعادة تشكيلها قبل أن تصل إلى مخزن العرض الخاص بالحقل. يمنحك هذا تحكمًا دقيقًا وفوريًا في ما يمكن للمستخدم كتابته، بما يفوق بكثير ما يمكن أن يوفره keyboardType وحده.

ملاحظة: تعمل منسقات الإدخال بشكل متزامن عند كل تغيير في الحرف. تتلقى قيمة TextEditingValue القديمة والقيمة الجديدة المقترحة، وتُعيد القيمة التي ينبغي استخدامها فعلًا. إعادة القيمة القديمة يرفض التغيير بفاعلية.

FilteringTextInputFormatter

يُعدّ FilteringTextInputFormatter أكثر المنسقات المدمجة استخدامًا. يعتمد على RegExp إما للسماح بالأحرف (القائمة البيضاء) أو لرفضها (القائمة السوداء).

  • FilteringTextInputFormatter.allow(pattern) — يحتفظ فقط بالأحرف المطابقة للنمط.
  • FilteringTextInputFormatter.deny(pattern) — يحذف أي حرف يطابق النمط.
  • يقبل كلا المُنشِئَين معاملًا اختياريًا replacementString لاستبدال الأحرف المحذوفة (القيمة الافتراضية هي السلسلة الفارغة).
  • يوفر Flutter مُنشِئَين مُسمَّيَين جاهزَين: FilteringTextInputFormatter.digitsOnly وFilteringTextInputFormatter.singleLineFormatter.

أمثلة شائعة على FilteringTextInputFormatter

import 'package:flutter/services.dart';

// السماح بالأرقام فقط (مثل FilteringTextInputFormatter.digitsOnly)
TextField(
  inputFormatters: [
    FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
  ],
  keyboardType: TextInputType.number,
  decoration: const InputDecoration(labelText: 'العمر'),
),

// السماح بالحروف والمسافات فقط (بلا أرقام أو رموز)
TextField(
  inputFormatters: [
    FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z؀-ۿ ]')),
  ],
  decoration: const InputDecoration(labelText: 'الاسم الكامل'),
),

// رفض الأحرف الخاصة (نهج القائمة السوداء)
TextField(
  inputFormatters: [
    FilteringTextInputFormatter.deny(RegExp(r'[!@#\$%^&*()]')),
  ],
  decoration: const InputDecoration(labelText: 'اسم المستخدم'),
),

LengthLimitingTextInputFormatter

يحدّ LengthLimitingTextInputFormatter من عدد الأحرف التي يقبلها الحقل. وبينما يمكنك ضبط maxLength على TextField مباشرةً، فإن استخدام المنسّق يمنحك الحدّ دون عرض عداد الأحرف أسفل الحقل — وهو خيار أنيق للتصاميم الحساسة بصريًا.

دمج منسقات متعددة

import 'package:flutter/services.dart';

// حقل OTP/PIN: أرقام فقط، بحد أقصى 6 أحرف
TextField(
  inputFormatters: [
    FilteringTextInputFormatter.digitsOnly,
    LengthLimitingTextInputFormatter(6),
  ],
  keyboardType: TextInputType.number,
  obscureText: true,
  decoration: const InputDecoration(labelText: 'أدخل رمز OTP'),
),

// اسم مستخدم أبجدي رقمي: حروف وأرقام، بحد أقصى 20 حرفًا
TextField(
  inputFormatters: [
    FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9_]')),
    LengthLimitingTextInputFormatter(20),
  ],
  decoration: const InputDecoration(labelText: 'اسم المستخدم (20 حرفًا كحد أقصى)'),
),
نصيحة: أضف دائمًا LengthLimitingTextInputFormatter بعد منسّق التصفية في القائمة. تُنفَّذ المنسقات بالترتيب؛ وإن عمل منسّق الطول أولًا فقد يحجب أحرفًا صالحة قبل أن يتمكن الفلتر من تقييمها — وإن كان كلا الترتيبين يعمل عمليًا في معظم الحالات، فإن جعل الترتيب مقصودًا يُبقي الكود مقروءًا.

كتابة TextInputFormatter مخصص

للتشكيل الأكثر تعقيدًا — كإدراج شرطات في رقم هاتف، أو إضافة خطوط مائلة لتاريخ، أو تحويل الحرف الأول إلى حرف كبير — يمكنك توسيع TextInputFormatter وتجاوز الدالة الوحيدة المطلوبة formatEditUpdate.

توقيع الدالة هو:

TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue)

تستلم القيمتين السابقة والمقترحة (بما فيها موضع المؤشر) ويجب أن تُعيد TextEditingValue النهائية — متضمنةً موضع مؤشر محدّثًا لمنعه من القفز بشكل غير متوقع.

منسّق رقم هاتف مخصص (###-###-####)

import 'package:flutter/services.dart';

/// ينسّق رقم هاتف أمريكي مكوّن من 10 أرقام بصيغة ###-###-####
/// أثناء كتابة المستخدم.
class PhoneNumberFormatter extends TextInputFormatter {
  @override
  TextEditingValue formatEditUpdate(
    TextEditingValue oldValue,
    TextEditingValue newValue,
  ) {
    // احذف كل حرف ليس رقمًا أولًا
    final digitsOnly = newValue.text.replaceAll(RegExp(r'[^0-9]'), '');

    // حدّ بـ 10 أرقام
    final capped = digitsOnly.length > 10
        ? digitsOnly.substring(0, 10)
        : digitsOnly;

    // ابنِ السلسلة المُقنَّعة
    final buffer = StringBuffer();
    for (int i = 0; i < capped.length; i++) {
      if (i == 3 || i == 6) buffer.write('-');
      buffer.write(capped[i]);
    }

    final formatted = buffer.toString();

    return TextEditingValue(
      text: formatted,
      // ضع المؤشر في نهاية السلسلة المنسّقة
      selection: TextSelection.collapsed(offset: formatted.length),
    );
  }
}

// الاستخدام داخل ودجت:
TextField(
  inputFormatters: [PhoneNumberFormatter()],
  keyboardType: TextInputType.phone,
  decoration: const InputDecoration(
    labelText: 'رقم الهاتف',
    hintText: '555-867-5309',
  ),
)
تحذير: احرص دائمًا على تحديث selection (موضع المؤشر) في القيمة المُعادة من منسّقك المخصص. إذا أعدت TextEditingValue(text: formatted) فقط دون selection، سيقفز المؤشر إلى الإزاحة 0 عند كل ضغطة مفتاح، مما يجعل الحقل شبه غير قابل للاستخدام.

مثال: منسّق رقم بطاقة الائتمان

نمط شائع آخر هو تجميع الأرقام مع مسافات — يُستخدم لأرقام بطاقات الائتمان (#### #### #### ####). المنطق مطابق لمنسّق الهاتف لكن بقواعد تجميع مختلفة.

منسّق بطاقة الائتمان (#### #### #### ####)

class CreditCardFormatter extends TextInputFormatter {
  @override
  TextEditingValue formatEditUpdate(
    TextEditingValue oldValue,
    TextEditingValue newValue,
  ) {
    final digits = newValue.text.replaceAll(RegExp(r'[^0-9]'), '');
    final capped = digits.length > 16 ? digits.substring(0, 16) : digits;

    final buffer = StringBuffer();
    for (int i = 0; i < capped.length; i++) {
      if (i != 0 && i % 4 == 0) buffer.write(' ');
      buffer.write(capped[i]);
    }

    final formatted = buffer.toString();
    return TextEditingValue(
      text: formatted,
      selection: TextSelection.collapsed(offset: formatted.length),
    );
  }
}

ملخص

منسقات الإدخال أداة قوية وخفيفة لفرض سلامة البيانات على مستوى واجهة المستخدم:

  • استخدم FilteringTextInputFormatter.allow لإدراج الأحرف المقبولة ضمن قائمة بيضاء باستخدام RegExp.
  • استخدم FilteringTextInputFormatter.deny لرفض الأحرف غير المرغوب فيها.
  • استخدم LengthLimitingTextInputFormatter لتحديد طول الحقل دون عرض عداد.
  • وسّع TextInputFormatter وتجاوز formatEditUpdate للإخفاء المتقدم (هواتف، تواريخ، بطاقات ائتمان).
  • أعد دائمًا TextEditingValue كاملة — تتضمن selection صالحًا — من المنسقات المخصصة.
النقطة الرئيسية: تعترض منسقات الإدخال النص قبل تثبيته في الحقل، مما يمنحك تحكمًا فوريًا حرفًا بحرف. إن الجمع بين المنسقات المدمجة ومنسّق مخصص من خلال توسيع TextInputFormatter يغطي كل متطلبات تشكيل النص التي ستواجهها في تطبيقات Flutter الإنتاجية.