flutterx_forms 2.0.1
flutterx_forms: ^2.0.1 copied to clipboard
Declarative Flutter forms with typed fields, validators, number, date, file and list inputs, a form controller and drop support.
flutterx_forms #
Typed Flutter form fields with validators and one form controller: text, integer and double with arrows, checkbox and switch, dropdowns, date and time, file and files with drop on the web, lists of fields, tabs, chips and dynamic inner forms.
Installation #
flutter pub add flutterx_forms
import 'package:flutterx_forms/flutterx_forms.dart';
Setup #
Mix FormController into the State of the form. Every field has a handle, XFormField<T>, created with field()
before the form is built: it gives the value, the focus node and the controller. The mixin builds the Form, disposes
the handles with the state and calls onFormSubmit when the form is valid.
class _LoginState extends State<Login> with FormController<Login> {
late final XFormField<String> _email = field();
late final XFormField<String> _password = field();
@override
Widget build(BuildContext context) => buildForm(
autovalidateMode: AutovalidateMode.onUserInteraction,
children: [
XTextFormField(
field: _email,
decoration: const InputDecoration(labelText: 'Email'),
textInputAction: TextInputAction.next,
validator: validateFirst([nonBlank(() => 'Required'), matchesEmail(() => 'Not an email')]),
),
XTextFormField(field: _password, obscureText: true, validator: minLen(8, () => 'At least 8 characters')),
FilledButton(onPressed: submitForm, child: const Text('Sign in')),
],
);
@override
Future<void> onFormSubmit() => signIn(_email.value, _password.value);
}
submitForm() validates, unfocuses and runs onFormSubmit; validateForm(), resetForm() and saveForm() act on
every field. formFields lists the attached handles; with autoscroll overridden to true a failed submit focuses the
first invalid field and scrolls to it. A handle read before its field is built fails: use valueOrNull or attached.
controller() returns the text controller of the handle, created on the first call: TextEditingController for
String, IntEditingController for int, DoubleEditingController for double (also nullable),
TextListEditingController for List<String>. Any other T throws UnsupportedError: pass a controllerFactory to
field().
Fields #
Every field takes its handle as field: and an optional initialValue, that applies only when the handle has no
controller yet. Validators take the non-null value.
late final XFormField<int?> _age = field();
late final XFormField<bool> _terms = field();
late final XFormField<MaterialColor> _color = field();
late final XFormField<DateTime?> _birthday = field();
late final XFormField<TimeOfDay?> _alarm = field();
late final XFormField<XFile?> _photo = field();
XIntFormField(field: _age, min: 18, max: 120, arrows: const NumberFieldArrows(step: 5)),
XBoolFormField.switchbox(field: _terms, titleText: 'I accept', validator: (value) => value ? null : 'Required'),
XDropdownButtonFormField<MaterialColor>(field: _color, items: Colors.primaries),
XDateTimeFormField(field: _birthday, lastDate: DateTime.now()),
XTimeFormField(field: _alarm),
XFileFormField(field: _photo, mimeTypes: const {'image/*'}),
- Text:
XTextFormField(XFormField<String>). - Numbers:
XIntFormFieldandXDoubleFormField(XFormField<int?>,XFormField<double?>) takemin,maxandarrows;NumberFieldArrowssetsstep,onlyArrowsandroundToStep.IntFieldandDoubleFieldare the same inputs outside a form. - Bool:
XBoolFormField.checkboxand.switchbox. - Dropdown:
XDropdownButtonFormFieldandXDropdownMenuFormField(withmenuEntryByLabel). - Date and time:
XDateTimeFormField(firstDate,lastDate,includeTime,dateFormat) andXTimeFormField. - Files:
XFileFormField(XFormField<XFile?>) andXFilesFormField(XFormField<List<XFile>>) takemimeTypesandonMimeTypeNotMatch, and accept files dropped on them on the web.XDropViewis the standalone drop target.
Lists #
XListFormField<E> shows a row per item with add and remove buttons; the itemBuilder gets the handle of each item.
XSetFormField<E> lets each row choose among items not chosen by the other rows. XListTabsFormField<E> puts each
item in a tab. XTextListFormField takes words typed in one text field and shows them as chips (List<String>).
XListFormField<String>(
field: _tags, // XFormField<List<String>>
decoration: const InputDecoration(labelText: 'Tags'),
maxLength: 3,
addButtonText: 'Add tag',
itemInitialValue: (values, index) => '',
validator: listUnique<String>(() => 'Duplicated tag'),
itemBuilder: (state, field, values, index) => XTextFormField(field: field, initialValue: values[index]),
),
Dynamic inner forms #
ProxyFormField<T> computes its value from inner fields that live in an XFormFieldStore: create, remove, reorder and
read them by id.
ProxyFormField<List<String>>(
field: _phones, // XFormField<List<String>>
initialValue: const [],
initializeFields: (value, fields) => fields.addFields<String>({for (var i = 0; i < value.length; i++) '$i'}),
computeFieldData: (values) => values.values.cast<String>().toList(),
builder: (state, fields) => Column(
children: [
for (final id in fields.fields.keys) XTextFormField(field: fields.field<String>(id)),
TextButton(onPressed: fields.addUniqueField<String>, child: const Text('Add')),
],
),
),
The inner fields are validated, saved and reset with the outer form; computeErrorText builds the error from the
invalid ones. validateUnique and validateUniqueBy reject a value repeated among the fields of a store.
Validators #
nonEmpty, nonBlank, nonNull, minLen, maxLen, matches, matchesEmail, contains, gt, gte, lt, lte,
oneOf, notIn, fieldEquals, fieldNotEquals, listNonEmpty, listUnique, listUniqueBy. Each takes a message
getter, so messages can be localized. Comparison and pattern validators accept a null value: pair them with nonNull.
validateFirst returns the first error of several validators; validateAll joins all errors (joiner prefixes each).
fieldEquals(_password, () => 'Different') compares with another handle, for a confirmation field.
Decorations #
SingleSideInputBorder(side:, margin:) draws one line on a side of the field, as a border of the InputDecoration.
InputDecoratorLoader shows a progress indicator in the decorator while something loads. CustomContentInputDecorator
puts any widget inside a themed decorator, as the bool and the arrows do.
Behavior #
resetrestores the value of the first construction, also after the form was rebuilt with anotherinitialValue.- The handle disposes the focus node and the controller it created, also those of a
controllerFactory; the controllers, scroll controllers and nodes you pass to a field stay yours to dispose. - Fields removed from an
XFormFieldStore(also on reset and with the state) are disposed by the store. - Dates are formatted with the locale of the context: for a locale other than
en_UScallinitializeDateFormattingfirst. The pickers start fromclock.now(), which tests control withwithClock. XDropView, and so the drop on the file fields, receives files only on the web; elsewhere the dialog is the way in.matchesandcontainsclash with the matchers offlutter_test: in tests import it withhide contains, matches.
Testing #
Replace the file dialog with a FileSelectorPlatform fake (file_selector_platform_interface,
plugin_platform_interface): set FileSelectorPlatform.instance to a class that returns the chosen file from
openFile and openFiles. Fields are driven with tester.enterText, submitForm() and resetForm().
Claude Code plugin #
The repository ships a plugin with skills that teach Claude Code to integrate the library:
/plugin marketplace add https://gitlab.com/dade.sliep/flutterx_forms
/plugin install flutterx-forms@flutterx-forms