فرم‌های پیشرفته

AdvancedForm سیستم داخلی اعتبارسنجی و رندر فرم در فینچ است. این کلاس تعریف فیلدها، اعتبارسنج‌ها (validators)، محافظت CSRF، و رندر قالب را در یک کلاس واحد کنار هم می‌آورد.

تعریف یک فرم

از AdvancedForm ارث‌بری کنید و name، widget، و fields() را override کنید:

import 'package:finch/finch_ui.dart';

class PersonForm extends AdvancedForm {
  @override
  String get name => 'form_person';

  @override
  String get widget => 'forms/person.j2.html';

  @override
  List<Field> fields() {
    return [
      csrf(),           // CSRF protection token (always include)
      Field('name', validators: [
        FieldValidator.requiredField(),
        FieldValidator.fieldLength(min: 3, max: 100),
      ]),
      Field('email', validators: [
        FieldValidator.requiredField(),
        FieldValidator.isEmailField(),
      ]),
      Field('age', validators: [
        FieldValidator.isNumberField(isRequired: false),
      ]),
    ];
  }
}

Field(name, {validators, initValue, type, initOptions}) — initValue مقدار اولیه فیلد را از پیش پر می‌کند (مثلاً یک تاریخ پیش‌فرض)، و initOptions یک callback ناهمگام (async) است که برای فیلدهای شبیه select استفاده می‌شود تا فهرست گزینه‌های آن‌ها را پر کند (به مثال دسته‌بندی در پایین مراجعه کنید).

اعتبارسنج‌های داخلی

FieldValidator هم بررسی‌های ساده روی مقدار و هم بررسی‌های رابطه (relation) و یکتایی (uniqueness) برای MongoDB/SQL را پوشش می‌دهد:

اعتبارسنج توضیح
FieldValidator.requiredField() فیلد نباید خالی باشد
FieldValidator.requiredFieldMultiLanguage() الزامی، انتظار یک شیء JSON از مقادیر به‌ازای هر زبان را دارد (حداقل یکی غیرخالی)
FieldValidator.fieldLength({min, max}) محدوده طول رشته
FieldValidator.isNumberField({min, max, isRequired}) باید یک عدد صحیح باشد، به‌صورت اختیاری محدودشده (مقدار پیش‌فرض isRequired برابر false است)
FieldValidator.isNumberDoubleField({min, max, isRequired}) مشابه بالا، برای اعداد اعشاری
FieldValidator.isEmailField() باید یک ایمیل معتبر باشد
FieldValidator.isPasswordField() حداقل ۸ کاراکتر، شامل حرف بزرگ، حرف کوچک، عدد، و یک کاراکتر خاص
FieldValidator.isColorField() باید یک رنگ هگزادسیمال باشد (#fff یا #ffffff)
FieldValidator.isSelectField(options) مقدار باید یکی از List گزینه‌های داده‌شده باشد
FieldValidator.isDateField({isRequired, checkUtc}) باید یک تاریخ قابل‌تجزیه باشد؛ checkUtc علاوه بر آن الزام می‌کند که UTC باشد
FieldValidator.contains(values, {isRequired}) مقدار باید یکی از values باشد
FieldValidator.hasRelation({collectionModel, relationField, isRequired}) MongoDB: مقدار باید به _id یک سند موجود در collectionModel (یک DBCollectionFree) اشاره کند
FieldValidator.hasSqlRelation({db, table, field, isRequired, operator, where}) SQL: مقدار باید به‌عنوان field در table وجود داشته باشد — برای db به MySQL/SQLite مراجعه کنید
FieldValidator.isUniqueSQLField({db, table, field, operator, where}) SQL: مقدار نباید از قبل به‌عنوان field در table وجود داشته باشد
FieldValidator.checkByRegexp(pattern, {isRequired}) بررسی سفارشی با RegExp — نه regExp()

یک فیلد شبیه select با گزینه‌های بارگذاری‌شده به‌صورت پویا (برگرفته از فرم کتاب پروژه مثال، که دسته‌بندی‌ها را از MySQL یا SQLite بارگذاری می‌کند):

Field(
  'category_id',
  validators: [
    FieldValidator.hasSqlRelation(
      isRequired: false,
      db: app.mysqlDriver,
      table: 'categories',
      field: 'id',
    ),
  ],
  initOptions: (field) async {
    var categories = await CategoriesRepository(app.mysqlDriver).getAllCategories();
    return categories.rows.assoc; // returned list becomes this field's `options`
  },
),

استفاده از فرم در یک Controller

Future<String> personForm() async {
  var form = PersonForm();

  return form.check(
    onValid: (formData) async {
      // form.get<String>('name') returns the validated value after check() ran
      var name  = form.get<String>('name');
      var email = form.get<String>('email');

      // process data...
      return rq.redirect('/example/person');
    },
    onInvalid: (formData) async {
      // form state (values + errors) is available in the template via rq params
      return rq.renderView(path: 'example/person');
    },
  );
}

rq درون AdvancedForm از قبل همان Context.rq است — هیچ مرحله جداگانه‌ای برای متصل کردن درخواست جاری لازم نیست. هر دو onValid/onInvalid نگاشت (map) داده‌های اعتبارسنجی‌شده فرم را به‌عنوان تنها آرگومان خود دریافت می‌کنند (حتی اگر از آن استفاده نکنید) — یک callback که بدون پارامتر تعریف شده باشد کامپایل می‌شود (چون نوع فیلد به‌صورت سست‌تایپ Function تعریف شده)، اما هنگام اجرا وقتی check() آن را فراخوانی می‌کند دچار خطا می‌شود، چون در واقع با یک آرگومان فراخوانی می‌شود. در یک درخواست GET، form.check() همیشه onInvalid را فراخوانی می‌کند تا فرم خالی نمایش داده شود.

قرار دادن Widget فرم در یک قالب

ویجت فرم را به یک قالب والد اضافه کنید:

{% include form_person.widget | unscape %}

مسیر ویجت فرم همان مقدار خاصیت widget کلاس شماست.

خواندن وضعیت فیلد در قالب‌ها

مقدار و خطاهای هر فیلد از طریق $n('formName/fieldName/...') در دسترس است. فیلد CSRF از طریق csrf() در fields() گنجانده می‌شود، اما نام واقعی این فیلد token است (مقدار csrfTokenName کلاس، که پیش‌فرض آن 'token' است) — نه csrf:

<form method="POST" action="/example/person">
  <input
    type="text"
    name="name"
    value="{{ $n('form_person/name/value') }}"
    class="{{ 'border-red-500' if $n('form_person/name/errors/0') else '' }}"
  />
  <p class="text-red-600">{{ $n('form_person/name/errors/0') }}</p>

  <input
    type="email"
    name="email"
    value="{{ $n('form_person/email/value') }}"
  />
  <p class="text-red-600">{{ $n('form_person/email/errors/0') }}</p>

  <!-- CSRF token -->
  <input type="hidden" name="token" value="{{ $n('form_person/token/value') }}" />

  <button type="submit">Save</button>
</form>

رفتار API Endpoint

وقتی URL درخواست با /api/ شروع شود، AdvancedForm به‌جای رندر کردن ویجت، یک پاسخ JSON برمی‌گرداند:

{
  "form_person": {
    "name": { "value": "Alice", "errors": [] },
    "email": { "value": "", "errors": ["Email is required"] }
  }
}

محافظت CSRF

گنجاندن csrf() در fields() یک فیلد مخفی توکن CSRF (با نام token) اضافه می‌کند. این توکن به‌ازای هر نام فرم تولید می‌شود، در session ذخیره می‌شود، و به‌صورت خودکار روی POST/PUT اعتبارسنجی می‌شود — و پیش از چرخش (rotate) شدن، تا ۶ ساعت از همان توکن دوباره استفاده می‌شود. برای فعال‌سازی آن، کاری بیش از گنجاندن csrf() در فهرست fields() و رندر کردن input مخفی token همان‌طور که در بالا نشان داده شد، لازم نیست.