EasyForm
A set of widgets for processing form field data in one common handler. Convenient for saving data to a database, sending via API, etc.
EasyForm
and EasyTextFormField
widgets are similar to and closely compatible with Flutter's Form
and TextFormField
widgets.
Form field widgets are placed in an EasyForm
container, each field has a name (specified in the name
parameter), when saved, the form field data is passed to the onSave
callback as a Map<String, dynamic>
, where the key is the field name and the value is the value of the form field.
The form can be saved with the EasyFormSaveButton
or by calling EasyForm.of(context).save()
.
To insert a text field, the EasyTextFormField
widget is used, for other fields, you can use the EasyCustomFormField
widget directly in the widget tree or create its inheritor.
Table of Contents
- Example
- Data processing
- Error handling
- Custom fields
- Customization
- Form buttons
- Form saving indicator
Example
This example shows a EasyForm
with two EasyTextFormField
to enter an username, password and an EasyFormSaveButton
to submit the form. The values โโof the form fields are passed to the hypothetical API client, the result of the API request is passed to onSaved
. While waiting for the API client to finish, the EasyFormSaveButton
displays a CircularProgressIndicator
.
@override
Widget build(BuildContext context) {
return EasyForm(
onSave: (values, form) async {
return API.login(values['username'], values['password']);
},
onSaved: (response, values, form) {
if (response.hasError) {
// ... display error
} else {
// ... navigate to another screen
}
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
EasyTextFormField(
name: 'username',
decoration: const InputDecoration(
hintText: 'Enter your username',
),
validator: (value) {
if (value.isEmpty) {
return 'Please enter some text';
}
return null;
},
),
const SizedBox(height: 16.0),
EasyTextFormField(
name: 'password',
decoration: const InputDecoration(
hintText: 'Enter your password',
),
validator: (value) {
if (value.isEmpty) {
return 'Please enter some text';
}
return null;
},
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 24.0),
child: EasyFormSaveButton.text('Sign In'),
),
],
),
);
}
Data processing
EasyForm
During the save of the form, the values from all fields are collected into the Map<String, dynamic>
and passed to the onSave
callback.
The onSave
callback is asynchronous, you can save or send values in it, wait for the response and pass it on to onSaved
. If onSave
returns nothing, a map with the values of the form fields will be passed to onSaved
.
EasyDataForm
Unlike EasyForm
, in EasyDataForm<T>
the result returned by the onSave
callback must be of the type (T
) specified when the EasyDataForm<T>
was instantiated.
Also, EasyDataForm<T>
has a different way of handling the result of the onSave
callback:
- EasyForm passes the result from the
onSave
callback to theonSaved
callback. If theonSave
result isnull
(onSave
returnednull
oronSave
was not specified) โ instead of theonSave
result, a map with the values of the form fields will be passed. - EasyDataForm always passes only the result returned by the
onSave
callback to theonSaved
callback, ornull
ifonSave
was not specified.
Error handling
If you need to pass errors to fields received from outside, for example, through the API, you can use the errors
parameter of the EasyForm
constructor or the setErrors
method of EasyFormState
.
An example of using the errors
parameter of the EasyForm
constructor:
class SampleFormPage extends StatefulWidget {
@override
_SampleFormPageState createState() => _SampleFormPageState();
}
class _SampleFormPageState extends State<SampleFormPage> {
Map<String, String> errors;
@override
Widget build(BuildContext context) {
return EasyForm(
errors: errors, // This is where errors are set in the fields.
onSave: (values, form) async {
return API.login(values['username'], values['password']);
},
onSaved: (response, values, form) {
if (response.hasError) {
setState(() {
errors = response.fieldsErrors;
});
} else {
// ... navigate to another screen
}
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
EasyTextFormField(
name: 'username',
decoration: const InputDecoration(
hintText: 'Enter your username',
),
validator: (value) {
if (value.isEmpty) {
return 'Please enter some text';
}
return null;
},
),
const SizedBox(height: 16.0),
EasyTextFormField(
name: 'password',
decoration: const InputDecoration(
hintText: 'Enter your password',
),
validator: (value) {
if (value.isEmpty) {
return 'Please enter some text';
}
return null;
},
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 24.0),
child: EasyFormSaveButton.text('Sign In'),
),
],
),
);
}
}
An example of using the setErrors
method in EasyFormState
:
class SampleFormPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return EasyForm(
onSave: (values, form) async {
return API.login(values['username'], values['password']);
},
onSaved: (response, values, form) {
if (response.hasError) {
form.setErrors(response.fieldsErrors); // This is where errors are set in the fields.
} else {
// ... navigate to another screen
}
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
EasyTextFormField(
name: 'username',
decoration: const InputDecoration(
hintText: 'Enter your username',
),
validator: (value) {
if (value.isEmpty) {
return 'Please enter some text';
}
return null;
},
),
const SizedBox(height: 16.0),
EasyTextFormField(
name: 'password',
decoration: const InputDecoration(
hintText: 'Enter your password',
),
validator: (value) {
if (value.isEmpty) {
return 'Please enter some text';
}
return null;
},
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 24.0),
child: EasyFormSaveButton.text('Sign In'),
),
],
),
);
}
}
Custom error display
For non-standard display of errors, or for displaying an error elsewhere in the layout, you can use the EasyFormFieldError
widget.
For example, in the following example, the error message is displayed below a layout that contains a custom field:
class CustomErrorDisplayPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return EasyForm(
onSave: (values, form) async {
return {
'file': 'Invalid file.',
};
},
onSaved: (response, values, form) {
if (response != null) {
form.setErrors(response);
}
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: [
// Custom form field.
DocumentFormField(
name: 'file',
),
const SizedBox(width: 16),
Text('Document to upload.'),
],
),
// An error message will be displayed here.
EasyFormFieldError(
name: 'file',
textStyle: TextStyle(color: Colors.red),
),
const Divider(
height: 24,
),
EasyTextFormField(
name: 'username',
decoration: const InputDecoration(
hintText: 'Enter your username',
),
validator: (value) {
if (value.isEmpty) {
return 'Please enter some text';
}
return null;
},
),
const SizedBox(height: 16.0),
EasyTextFormField(
name: 'password',
decoration: const InputDecoration(
hintText: 'Enter your password',
),
validator: (value) {
if (value.isEmpty) {
return 'Please enter some text';
}
return null;
},
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 24.0),
child: EasyFormSaveButton.text('Sign In'),
),
],
),
);
}
}
Custom fields
For text fields, you can use the EasyTextFormField
widget, which encapsulates the TextField
widget with all its properties.
EasyForm
makes it easy to create a custom type of form field.
As a controller, you can use the generic EasyFormFieldController
or create an inheritor of it:
class ColorController extends EasyFormFieldController<Color> {
ColorController(super.value);
}
To create a field, you can create an inheritor of EasyFormGenericField
and implement its build
method:
class ColorField extends EasyFormGenericField<Color> {
const ColorField({
super.key,
required ColorController super.controller,
super.onChange,
});
void _change() {
value = _getRandomColor();
}
Color _getRandomColor() {
return Color((Random().nextDouble() * 0xFFFFFF).toInt()).withOpacity(1.0);
}
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.centerLeft,
child: GestureDetector(
onTap: _change,
child: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: value,
border: Border.all(
color: Colors.grey,
width: 2,
),
shape: BoxShape.circle,
),
),
),
);
}
}
This field and controller can be used independently of EasyForm
.
To use them in EasyForm
, you can do it directly through the generic-widget of the EasyCustomFormField
:
EasyCustomFormField<Color, ColorController>(
name: 'color',
initialValue: Colors.teal,
controllerBuilder: (value) => ColorController(value),
builder: (fieldState, onChangedHandler) => ColorField(
controller: fieldState.controller as ColorController,
onChange: onChangedHandler,
),
),
Or create an inheritor of the EasyCustomFormField
widget:
class ColorFormField extends EasyCustomFormField<Color, ColorController> {
ColorFormField({
super.key,
required super.name,
super.controller,
Color? initialValue,
}) : super(
initialValue: initialValue ?? const Color(0x00000000),
controllerBuilder: (value) => ColorController(value),
builder: (state, onChangedHandler) {
return ColorField(
controller: state.controller as ColorController,
onChange: onChangedHandler,
);
},
);
}
... and use it in the form:
ColorFormField(
name: 'color',
initialValue: Colors.teal,
),
Stateful custom field
The EasyFormGenericField
widget is a stateful widget, if you need to store an intermediate state (for example, a link to a photo picker), this is how you can transform the above field code into a stateful widget:
class ColorField extends EasyFormGenericField<Color> {
const ColorField({
super.key,
required ColorController super.controller,
super.onChange,
});
@override
ColorFieldState createState() => ColorFieldState();
}
class ColorFieldState extends EasyFormGenericFieldState<Color> {
void _change() {
value = _getRandomColor();
}
Color _getRandomColor() {
return Color((Random().nextDouble() * 0xFFFFFF).toInt()).withOpacity(1.0);
}
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.centerLeft,
child: GestureDetector(
onTap: _change,
child: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: value,
border: Border.all(
color: Colors.grey,
width: 2,
),
shape: BoxShape.circle,
),
),
),
);
}
}
Customization
The appearance and layout of the save, reset buttons, save indicators, and error messages can be customized.
You can set the default appearance for all buttons and other widgets using EasyFormDefaultSettings
. All settings specified in EasyFormDefaultSettings
will apply to all underlying widgets.
Typically these settings are set in MaterialApp.builder
:
return MaterialApp(
...
builder: (context, child) => EasyFormDefaultSettings(
saveButton: EasyFormSaveButtonSettings(
builder: (context, key, child, onPressed, adaptivity) =>
ElevatedButton(
style: ElevatedButton.styleFrom(
foregroundColor: Colors.white,
backgroundColor: Colors.amber.shade700,
),
onPressed: onPressed,
child: child ?? const SizedBox.shrink(),
),
),
),
...
);
This way you can set default settings for:
-
EasyFormSaveButton
- builder
- layoutBuilder
- indicatorBuilder
-
EasyFormResetButton
- builder
-
EasyFormActionButton
- builder
-
EasyFormSaveIndicator
- builder
- layoutBuilder
-
EasyFormFieldError
- builder
Form buttons
For convenience, EasyForm
has three types of buttons:
EasyFormSaveButton
- to save the form;EasyFormResetButton
- to return the form to its original state;EasyFormButton
- for any form action;
The EasyFormSaveButton
and EasyFormResetButton
buttons each have two constructors, one - the standard one accepts the widget as the button content, and the second named text
accepts the text as the button content:
EasyFormSaveButton(child: Text('Save')),
// or
EasyFormSaveButton.text('Save'),
Button customization
By default, the EasyFormSaveButton
is created as an ElevatedButton
or CupertinoButton.filled
, and the EasyFormResetButton
is created as an OutlinedButton
or CupertinoButton
.
The appearance of the button can be customized using the adaptivity
argument of the EasyForm
constructor:
EasyFormAdaptivity.auto
- the button type will be selected based on the platform;EasyFormAdaptivity.material
- Material-design buttons;EasyFormAdaptivity.cupertion
- Apple design buttons.
These buttons can be customized by overriding the button builder
that creates the button widget itself.
The builder can be passed to the constructor:
EasyFormSaveButton.text(
'Save',
builder: (context, key, child, onPressed) => ElevatedButton(
key: key,
child: child,
onPressed: onPressed,
),
),
... or redefine globally, for the entire application (except for those buttons where the builder is passed to the constructor) using EasyFormDefaultSettings.
Process of saving a form
While the form is being saved, the EasyFormSaveButton
and EasyFormResetButton
buttons are blocked from being pressed, and a progress indicator is displayed on the EasyFormSaveButton
button instead of its content (for example, text).
By default, the indicator is an 18x18 CircularProgressIndicator
or CupertinoActivityIndicator
and the color of the text from the ElevatedButton
.
It can be customized by overriding the builder in the button constructor:
EasyFormSaveButton.text(
'Save',
indicatorBuilder: (context, size) => SizedBox.fromSize(
size: size,
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
),
)
... or redefine globally, for the entire application (except for those buttons where the builder is passed to the constructor) using EasyFormDefaultSettings.
Form saving indicator
Saving or submitting form data can take a certain amount of time, during which time a progress indicator can be displayed. If you are not using an EasyFormSaveButton
button that shows such an indicator, you may want to add such an indicator on top of all or part of the form. The EasyFormSaveIndicator
widget is used for this.
By default, this is CircularProgressIndicator
or CupertinoActivityIndicator
over the child
of the widget, but you can override the indicator builder, as well as override the layout builder so that the indicator is not placed on top of the child.
The indicator builder and layout builder can be passed to the EasyFormSaveIndicator
constructor or redefine globally for the entire application (except for EasyFormSaveIndicator
, with the specified builder in the constructor) using EasyFormDefaultSettings.