高级表单

AdvancedForm 是 Finch 内置的表单验证与渲染系统。它将字段定义、验证器、CSRF 保护和模板渲染整合到一个类中。

定义表单

继承 AdvancedForm,并重写 name、widget 和 fields():

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 是一个异步回调,用于为下拉选择类型的字段填充选项列表(参见下方的分类示例)。

内置验证器

FieldValidator 涵盖了普通值检查,以及 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() 至少 8 个字符,且包含大写字母、小写字母、数字和特殊字符
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:值必须引用 collectionModel(一个 DBCollectionFree)中某个已存在文档的 _id
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()

一个动态加载选项的下拉选择字段示例(改编自示例项目中的图书表单,它从 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`
  },
),

在控制器中使用表单

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');
    },
  );
}

AdvancedForm 内部的 rq 本身就是 Context.rq——不需要额外的步骤来关联当前请求。onValid/onInvalid 都会接收已验证表单的数据 map 作为唯一参数(即使你不使用它)——声明为不带参数的回调可以编译通过(因为该字段的类型被宽松地声明为 Function),但在 check() 调用它时会在运行时抛出异常,因为它实际上是带一个参数被调用的。在 GET 请求中,form.check() 总是会调用 onInvalid,从而显示空白表单。

在模板中包含表单 Widget

将表单 widget 添加到父模板中:

{% include form_person.widget | unscape %}

表单 widget 的路径就是你类中的 widget 属性。

在模板中读取字段状态

每个字段的值和错误都可以通过 $n('formName/fieldName/...') 获取。CSRF 字段是通过 fields() 中的 csrf() 添加的,但它实际的字段名是 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 端点行为

当请求 URL 以 /api/ 开头时,AdvancedForm 会返回 JSON 响应,而不是渲染 widget:

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

CSRF 保护

在 fields() 中包含 csrf() 会添加一个隐藏的 CSRF token 字段(名为 token)。该 token 按表单名称生成,存储在 session 中,并在 POST/PUT 请求时自动进行验证——同一个 token 最多复用 6 小时后才会轮换。除了在 fields() 列表中包含 csrf(),并像上面那样渲染隐藏的 token 输入框之外,不需要调用任何其他方法来启用它。