Basics

表单

简介

Nylo Website v7 提供了围绕 NyFormWidget 构建的表单系统。您的表单类继承 NyFormWidget 并且本身就是组件 -- 不需要单独的包装器。表单支持内置验证、多种字段类型、样式和数据管理。

import 'package:nylo_framework/nylo_framework.dart';

// 1. Define a form
class LoginForm extends NyFormWidget {
  LoginForm({super.key, super.submitButton, super.onSubmit, super.onFailure});

  @override
  fields() => [
    Field.email("Email", validator: FormValidator.email()),
    Field.password("Password", validator: FormValidator.password()),
  ];

  static NyFormActions get actions => const NyFormActions('LoginForm');
}

// 2. Display and submit it
LoginForm(
  submitButton: Button.primary(text: "Login"),
  onSubmit: (data) {
    print(data); // {email: "...", password: "..."}
  },
)

创建表单

使用 Metro CLI 创建新表单:

metro make:form LoginForm

这会创建 lib/app/forms/login_form.dart

import 'package:nylo_framework/nylo_framework.dart';

class LoginForm extends NyFormWidget {
  LoginForm({super.key, super.submitButton, super.onSubmit, super.onFailure});

  @override
  fields() => [
    Field.email("Email", validator: FormValidator.email()),
    Field.password("Password", validator: FormValidator.password()),
  ];

  static NyFormActions get actions => const NyFormActions('LoginForm');
}

表单继承 NyFormWidget 并覆盖 fields() 方法来定义表单字段。每个字段使用命名构造函数,如 Field.text()Field.email()Field.password()static NyFormActions get actions getter 提供了从应用的任何位置与表单交互的便捷方式。

显示表单

由于您的表单类继承了 NyFormWidget,它本身就是组件。直接在您的组件树中使用它:

@override
Widget view(BuildContext context) {
  return Scaffold(
    body: SafeArea(
      child: LoginForm(
        submitButton: Button.primary(text: "Submit"),
        onSubmit: (data) {
          print(data);
        },
      ),
    ),
  );
}

提交表单

有三种方式提交表单:

使用 onSubmit 和 submitButton

构建表单时传递 onSubmitsubmitButton。Nylo Website 提供可作为提交按钮使用的预构建按钮:

LoginForm(
  submitButton: Button.primary(text: "Submit"),
  onSubmit: (data) {
    print(data); // {email: "...", password: "..."}
  },
  onFailure: (errors) {
    print(errors.first.rule.getMessage());
  },
)

可用的按钮样式:Button.primaryButton.secondaryButton.outlinedButton.textOnlyButton.iconButton.gradientButton.roundedButton.transparency

使用 NyFormActions

使用 actions getter 从任何位置提交:

LoginForm.actions.submit(
  onSuccess: (data) {
    print(data);
  },
  onFailure: (errors) {
    print(errors.first.rule.getMessage());
  },
  showToastError: true,
);

使用 NyFormWidget.submit() 静态方法

通过名称从任何位置提交表单:

NyFormWidget.submit("LoginForm",
  onSuccess: (data) {
    print(data);
  },
  onFailure: (errors) {
    print(errors.first.rule.getMessage());
  },
  showToastError: true,
);

提交时,表单会验证所有字段。如果有效,onSuccess 将被调用,并传入字段数据的 Map<String, dynamic>(键为字段名的 snake_case 版本)。如果无效,默认会显示 toast 错误,并调用 onFailure(如已提供)。

字段类型

Nylo Website v7 通过 Field 类上的命名构造函数提供 22 种字段类型。所有字段构造函数共享这些通用参数:

参数 类型 默认值 描述
key String 必需 字段标识符(位置参数)
label String? null 自定义显示标签(默认为标题格式的 key)
value dynamic null 初始值
validator FormValidator? null 验证规则
autofocus bool false 加载时自动聚焦
dummyData String? null 测试/开发数据
header Widget? null 字段上方显示的组件
footer Widget? null 字段下方显示的组件
titleStyle TextStyle? null 自定义标签文字样式
hidden bool false 隐藏字段
readOnly bool? null 使字段只读
style FieldStyle? 因类型而异 字段特定样式配置
onChanged Function(dynamic)? null 值变更回调

文本字段

Field.text("Name")

Field.text("Name",
  value: "John",
  validator: FormValidator.notEmpty(),
  autofocus: true,
)

样式类型:FieldStyleTextField

数字字段

Field.number("Age")

// Decimal numbers
Field.number("Score", decimal: true)

decimal 参数控制是否允许小数输入。样式类型:FieldStyleTextField

密码字段

Field.password("Password")

// With visibility toggle
Field.password("Password", viewable: true)

viewable 参数添加显示/隐藏切换。样式类型:FieldStyleTextField

邮箱字段

Field.email("Email", validator: FormValidator.email())

自动设置邮箱键盘类型并过滤空格。样式类型:FieldStyleTextField

URL 字段

Field.url("Website", validator: FormValidator.url())

设置 URL 键盘类型。样式类型:FieldStyleTextField

文本区域字段

Field.textArea("Description")

多行文本输入。样式类型:FieldStyleTextField

电话号码字段

Field.phoneNumber("Mobile Phone")

自动格式化电话号码输入。样式类型:FieldStyleTextField

单词首字母大写

Field.capitalizeWords("Full Name")

将每个单词的首字母大写。样式类型:FieldStyleTextField

句首字母大写

Field.capitalizeSentences("Bio")

将每个句子的首字母大写。样式类型:FieldStyleTextField

日期字段

Field.date("Birthday")

Field.date("Birthday",
  dummyData: "1990-01-01",
  style: FieldStyleDateTimePicker(
    firstDate: DateTime(1900),
    lastDate: DateTime.now(),
  ),
)

打开日期选择器。样式类型:FieldStyleDateTimePicker

日期时间字段

Field.datetime("Check in Date")

Field.datetime("Appointment", dummyData: "2025-01-01 10:00")

打开日期和时间选择器。样式类型:FieldStyleDateTimePicker

掩码输入字段

Field.mask("Phone", mask: "(###) ###-####")

Field.mask("Credit Card", mask: "#### #### #### ####")

Field.mask("Custom Code",
  mask: "AA-####",
  match: r'[\w\d]',
  maskReturnValue: true, // Returns the formatted value
)

掩码中的 # 字符由用户输入替换。使用 match 控制允许的字符。当 maskReturnValuetrue 时,返回的值包含掩码格式。

货币字段

Field.currency("Price", currency: "usd")

currency 参数为必需,决定货币格式。样式类型:FieldStyleTextField

复选框字段

Field.checkbox("Accept Terms")

Field.checkbox("Agree to terms",
  header: Text("Terms and conditions"),
  footer: Text("You must agree to continue."),
  validator: FormValidator.booleanTrue(message: "You must accept the terms"),
)

样式类型:FieldStyleCheckbox

开关字段

Field.switchBox("Enable Notifications")

样式类型:FieldStyleSwitchBox

选择器字段

Field.picker("Category",
  options: FormCollection.from(["Electronics", "Clothing", "Books"]),
)

// With key-value pairs
Field.picker("Country",
  options: FormCollection.fromMap({
    "us": "United States",
    "ca": "Canada",
    "uk": "United Kingdom",
  }),
)

options 参数需要一个 FormCollection(不是原始列表)。查看 FormCollection 了解详情。样式类型:FieldStylePicker

单选按钮字段

Field.radio("Newsletter",
  options: FormCollection.fromMap({
    "Yes": "Yes, I want to receive newsletters",
    "No": "No, I do not want to receive newsletters",
  }),
)

options 参数需要一个 FormCollection。样式类型:FieldStyleRadio

标签字段

Field.chips("Tags",
  options: FormCollection.from(["Featured", "Sale", "New"]),
)

// With key-value pairs
Field.chips("Engine Size",
  options: FormCollection.fromMap({
    "125": "125cc",
    "250": "250cc",
    "500": "500cc",
  }),
)

通过标签组件允许多选。options 参数需要一个 FormCollection。样式类型:FieldStyleChip

滑块字段

Field.slider("Rating",
  label: "Rate us",
  validator: FormValidator.minValue(4, message: "Rating must be at least 4"),
  style: FieldStyleSlider(
    min: 0,
    max: 10,
    divisions: 10,
    activeColor: Colors.blue,
    inactiveColor: Colors.grey,
  ),
)

样式类型:FieldStyleSlider -- 配置 minmaxdivisions、颜色、值显示等。

范围滑块字段

Field.rangeSlider("Price Range",
  style: FieldStyleRangeSlider(
    min: 0,
    max: 1000,
    divisions: 20,
    activeColor: Colors.blue,
    inactiveColor: Colors.grey,
  ),
)

返回一个 RangeValues 对象。样式类型:FieldStyleRangeSlider

自定义字段

使用 Field.custom() 提供您自己的有状态组件:

Field.custom("My Field",
  child: MyCustomFieldWidget(),
)

child 参数需要一个继承 NyFieldStatefulWidget 的组件。这让您完全控制字段的渲染和行为。

组件字段

使用 Field.widget() 在表单中嵌入任何组件,而不将其作为表单字段:

Field.widget(child: Divider())

Field.widget(child: Text("Section Header", style: TextStyle(fontSize: 18)))

组件字段不参与验证或数据收集。它们纯粹用于布局。

FormCollection

选择器、单选和标签字段的选项需要 FormCollectionFormCollection 为处理不同的选项格式提供了统一的接口。

创建 FormCollection

// From a list of strings (value and label are the same)
FormCollection.from(["Red", "Green", "Blue"])

// Same as above, explicit
FormCollection.fromArray(["Red", "Green", "Blue"])

// From a map (key = value, value = label)
FormCollection.fromMap({
  "us": "United States",
  "ca": "Canada",
})

// From structured data (useful for API responses)
FormCollection.fromKeyValue([
  {"value": "en", "label": "English"},
  {"value": "es", "label": "Spanish"},
])

FormCollection.from() 自动检测数据格式并委托给适当的构造函数。

FormOption

FormCollection 中的每个选项都是一个具有 valuelabel 属性的 FormOption

FormOption option = FormOption(value: "us", label: "United States");
print(option.value); // "us"
print(option.label); // "United States"

查询选项

FormCollection options = FormCollection.fromMap({"us": "United States", "ca": "Canada"});

options.getByValue("us");          // FormOption(value: us, label: United States)
options.getLabelByValue("us");     // "United States"
options.containsValue("ca");      // true
options.searchByLabel("can");      // [FormOption(value: ca, label: Canada)]
options.values;                    // ["us", "ca"]
options.labels;                    // ["United States", "Canada"]

表单验证

使用 validator 参数和 FormValidator 为任何字段添加验证:

// Named constructor
Field.email("Email", validator: FormValidator.email())

// Chained rules
Field.text("Username",
  validator: FormValidator()
    .notEmpty()
    .minLength(3)
    .maxLength(20)
)

// Password with strength level
Field.password("Password",
  validator: FormValidator.password(strength: 2)
)

// Boolean validation
Field.checkbox("Terms",
  validator: FormValidator.booleanTrue(message: "You must accept the terms")
)

// Custom inline validation
Field.number("Age",
  validator: FormValidator.custom(
    message: "Age must be between 18 and 100",
    validate: (data) {
      int? age = int.tryParse(data.toString());
      return age != null && age >= 18 && age <= 100;
    },
  )
)

提交表单时,所有验证器都会被检查。如果任何验证失败,将显示第一个错误消息的 toast,并调用 onFailure 回调。

另请参阅: 有关可用验证器的完整列表,请查看验证页面。

管理表单数据

初始数据

有两种方式设置表单的初始数据。

选项 1:在表单类中覆盖 init getter

class EditAccountForm extends NyFormWidget {
  EditAccountForm({super.key, super.submitButton, super.onSubmit, super.onFailure});

  @override
  Function()? get init => () async {
    final user = await api<ApiService>((request) => request.getUserData());

    return {
      "First Name": user?.firstName,
      "Last Name": user?.lastName,
    };
  };

  @override
  fields() => [
    Field.text("First Name"),
    Field.text("Last Name"),
  ];

  static NyFormActions get actions => const NyFormActions('EditAccountForm');
}

init getter 可以返回同步的 Map 或异步的 Future<Map>。键使用 snake_case 标准化与字段名匹配,因此 "First Name" 映射到键为 "First Name" 的字段。

选项 2:将 initialData 传递给表单组件

EditAccountForm(
  initialData: {
    "first_name": "John",
    "last_name": "Doe",
  },
)

设置字段值

使用 NyFormActions 从任何位置设置字段值:

// Set a single field value
EditAccountForm.actions.updateField("First Name", "Jane");

设置字段选项

动态更新选择器、标签或单选字段的选项:

EditAccountForm.actions.setOptions("Category", FormCollection.from(["New Option 1", "New Option 2"]));

读取表单数据

表单数据通过提交时的 onSubmit 回调访问,或通过 onChanged 回调进行实时更新:

EditAccountForm(
  onSubmit: (data) {
    // data is a Map<String, dynamic>
    // {first_name: "Jane", last_name: "Doe", email: "jane@example.com"}
    print(data);
  },
  onChanged: (Field field, dynamic value) {
    print("${field.key} changed to: $value");
  },
)

清除数据

// Clear all fields
EditAccountForm.actions.clear();

// Clear a specific field
EditAccountForm.actions.clearField("First Name");

更新字段

// Update a field value
EditAccountForm.actions.updateField("First Name", "Jane");

// Refresh the form UI
EditAccountForm.actions.refresh();

// Refresh form fields (re-calls fields())
EditAccountForm.actions.refreshForm();

提交按钮

构建表单时传递 submitButtononSubmit 回调:

UserInfoForm(
  submitButton: Button.primary(text: "Submit"),
  onSubmit: (data) {
    print(data);
  },
  onFailure: (errors) {
    print(errors.first.rule.getMessage());
  },
)

submitButton 会自动显示在表单字段下方。您可以使用任何内置按钮样式或自定义组件。

您也可以使用任何组件作为提交按钮,将其作为 footer 传递:

UserInfoForm(
  onSubmit: (data) {
    print(data);
  },
  footer: ElevatedButton(
    onPressed: () {
      UserInfoForm.actions.submit(
        onSuccess: (data) {
          print(data);
        },
      );
    },
    child: Text("Submit"),
  ),
)

表单布局

通过将字段包裹在 List 中来并排放置字段:

@override
fields() => [
  // Single field (full width)
  Field.text("Title"),

  // Two fields in a row
  [
    Field.text("First Name"),
    Field.text("Last Name"),
  ],

  // Another single field
  Field.textArea("Bio"),

  // Slider and range slider in a row
  [
    Field.slider("Rating", style: FieldStyleSlider(min: 0, max: 10)),
    Field.rangeSlider("Budget", style: FieldStyleRangeSlider(min: 0, max: 1000)),
  ],

  // Embed a non-field widget
  Field.widget(child: Divider()),

  Field.email("Email"),
];

List 中的字段以等宽 ExpandedRow 渲染。字段之间的间距由 NyFormWidget 上的 crossAxisSpacing 参数控制。

字段可见性

使用 Field 上的 hide()show() 方法以编程方式显示或隐藏字段。您可以在表单类内部或通过 onChanged 回调访问字段:

// Inside your NyFormWidget subclass or onChanged callback
Field nameField = ...;

// Hide the field
nameField.hide();

// Show the field
nameField.show();

隐藏的字段不会在 UI 中渲染,但仍然存在于表单的字段列表中。

字段样式

每种字段类型都有对应的 FieldStyle 子类用于样式设置:

字段类型 样式类
Text、Email、Password、Number、URL、TextArea、PhoneNumber、Currency、Mask、CapitalizeWords、CapitalizeSentences FieldStyleTextField
Date、DateTime FieldStyleDateTimePicker
Picker FieldStylePicker
Checkbox FieldStyleCheckbox
Switch Box FieldStyleSwitchBox
Radio FieldStyleRadio
Chip FieldStyleChip
Slider FieldStyleSlider
Range Slider FieldStyleRangeSlider

将样式对象传递给任何字段的 style 参数:

Field.text("Name",
  style: FieldStyleTextField(
    filled: true,
    fillColor: Colors.grey.shade100,
    border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
    contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
    prefixIcon: Icon(Icons.person),
  ),
)

Field.slider("Rating",
  style: FieldStyleSlider(
    min: 0,
    max: 10,
    divisions: 10,
    activeColor: Colors.blue,
    showValue: true,
  ),
)

Field.chips("Tags",
  options: FormCollection.from(["Sale", "New", "Featured"]),
  style: FieldStyleChip(
    selectedColor: Colors.blue,
    checkmarkColor: Colors.white,
    spacing: 8.0,
    runSpacing: 8.0,
  ),
)

NyFormWidget 静态方法

NyFormWidget 提供了静态方法,可通过名称从应用的任何位置与表单交互:

方法 描述
NyFormWidget.submit(name, onSuccess:, onFailure:, showToastError:) 通过名称提交表单
NyFormWidget.stateRefresh(name) 刷新表单的 UI 状态
NyFormWidget.stateSetValue(name, key, value) 通过表单名称设置字段值
NyFormWidget.stateSetOptions(name, key, options) 通过表单名称设置字段选项
NyFormWidget.stateClearData(name) 通过表单名称清除所有字段
NyFormWidget.stateRefreshForm(name) 刷新表单字段(重新调用 fields()
// Submit a form named "LoginForm" from anywhere
NyFormWidget.submit("LoginForm", onSuccess: (data) {
  print(data);
});

// Update a field value remotely
NyFormWidget.stateSetValue("LoginForm", "Email", "new@email.com");

// Clear all form data
NyFormWidget.stateClearData("LoginForm");

提示: 建议使用 NyFormActions(见下方)而不是直接调用这些静态方法 -- 更简洁且不易出错。

NyFormWidget 构造函数参考

继承 NyFormWidget 时,您可以传递以下构造函数参数:

LoginForm(
  Key? key,
  double crossAxisSpacing = 10,  // Horizontal spacing between row fields
  double mainAxisSpacing = 10,   // Vertical spacing between fields
  Map<String, dynamic>? initialData, // Initial field values
  Function(Field field, dynamic value)? onChanged, // Field change callback
  Widget? header,                // Widget above the form
  Widget? submitButton,          // Submit button widget
  Widget? footer,                // Widget below the form
  double headerSpacing = 10,     // Spacing after header
  double submitButtonSpacing = 10, // Spacing after submit button
  double footerSpacing = 10,     // Spacing before footer
  LoadingStyle? loadingStyle,    // Loading indicator style
  bool locked = false,           // Makes form read-only
  Function(dynamic data)? onSubmit,   // Called with form data on successful validation
  Function(dynamic error)? onFailure, // Called with errors on failed validation
)

onChanged 回调接收更改的 Field 及其新值:

LoginForm(
  onChanged: (Field field, dynamic value) {
    print("${field.key} changed to: $value");
  },
)

NyFormActions

NyFormActions 提供了从应用的任何位置与表单交互的便捷方式。在表单类中将其定义为静态 getter:

class LoginForm extends NyFormWidget {
  LoginForm({super.key, super.submitButton, super.onSubmit, super.onFailure});

  @override
  fields() => [
    Field.email("Email", validator: FormValidator.email()),
    Field.password("Password", validator: FormValidator.password()),
  ];

  static NyFormActions get actions => const NyFormActions('LoginForm');
}

可用操作

方法 描述
actions.updateField(key, value) 设置字段的值
actions.clearField(key) 清除特定字段
actions.clear() 清除所有字段
actions.refresh() 刷新表单的 UI 状态
actions.refreshForm() 重新调用 fields() 并重建
actions.setOptions(key, options) 设置选择器/标签/单选字段的选项
actions.submit(onSuccess:, onFailure:, showToastError:) 带验证提交
// Update a field value
LoginForm.actions.updateField("Email", "new@email.com");

// Clear all form data
LoginForm.actions.clear();

// Submit the form
LoginForm.actions.submit(
  onSuccess: (data) {
    print(data);
  },
);

NyFormWidget 覆盖

您可以在 NyFormWidget 子类中覆盖的方法:

覆盖 描述
fields() 定义表单字段(必需)
init 提供初始数据(同步或异步)
onChange(field, data) 内部处理字段变更

所有字段类型参考

构造函数 关键参数 描述
Field.text() -- 标准文本输入
Field.email() -- 带键盘类型的邮箱输入
Field.password() viewable 带可选可见性切换的密码
Field.number() decimal 数字输入,可选小数
Field.currency() currency(必需) 货币格式化输入
Field.capitalizeWords() -- 标题格式文本输入
Field.capitalizeSentences() -- 句首格式文本输入
Field.textArea() -- 多行文本输入
Field.phoneNumber() -- 自动格式化的电话号码
Field.url() -- 带键盘类型的 URL 输入
Field.mask() mask(必需)、matchmaskReturnValue 掩码文本输入
Field.date() -- 日期选择器
Field.datetime() -- 日期和时间选择器
Field.checkbox() -- 布尔复选框
Field.switchBox() -- 布尔开关
Field.picker() options(必需 FormCollection 从列表中单选
Field.radio() options(必需 FormCollection 单选按钮组
Field.chips() options(必需 FormCollection 多选标签
Field.slider() -- 单值滑块
Field.rangeSlider() -- 范围值滑块
Field.custom() child(必需 NyFieldStatefulWidget 自定义有状态组件
Field.widget() child(必需 Widget 嵌入任何组件(非字段)