高级表单
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 输入框之外,不需要调用任何其他方法来启用它。