Add FormField.errorBuilder (#162255)
## Description This PR adds the `TextFormField.errorBuilder`property which makes it possible to customize the widget used to display the error message. Implementation based on https://github.com/flutter/flutter/pull/156275#issuecomment-2521651828 ## Related Issue Fixes [Unable to use validator along with error widget in TextFormField](https://github.com/flutter/flutter/issues/133629) Fixes https://github.com/flutter/flutter/issues/135292 ## Tests Adds 1 tests.
This commit is contained in:
parent
9d7d36cfba
commit
3b7c4aa2a7
@ -1722,6 +1722,7 @@ class DropdownButtonFormField<T> extends FormField<T> {
|
||||
InputDecoration? decoration,
|
||||
super.onSaved,
|
||||
super.validator,
|
||||
super.errorBuilder,
|
||||
AutovalidateMode? autovalidateMode,
|
||||
double? menuMaxHeight,
|
||||
bool? enableFeedback,
|
||||
@ -1747,10 +1748,8 @@ class DropdownButtonFormField<T> extends FormField<T> {
|
||||
autovalidateMode: autovalidateMode ?? AutovalidateMode.disabled,
|
||||
builder: (FormFieldState<T> field) {
|
||||
final _DropdownButtonFormFieldState<T> state = field as _DropdownButtonFormFieldState<T>;
|
||||
final InputDecoration decorationArg = decoration ?? const InputDecoration();
|
||||
final InputDecoration effectiveDecoration = decorationArg.applyDefaults(
|
||||
Theme.of(field.context).inputDecorationTheme,
|
||||
);
|
||||
InputDecoration effectiveDecoration = (decoration ?? const InputDecoration())
|
||||
.applyDefaults(Theme.of(field.context).inputDecorationTheme);
|
||||
|
||||
final bool showSelectedItem =
|
||||
items != null &&
|
||||
@ -1767,6 +1766,22 @@ class DropdownButtonFormField<T> extends FormField<T> {
|
||||
: effectiveHint != null || effectiveDisabledHint != null;
|
||||
final bool isEmpty = !showSelectedItem && !isHintOrDisabledHintAvailable;
|
||||
|
||||
if (field.errorText != null || effectiveDecoration.hintText != null) {
|
||||
final Widget? error =
|
||||
field.errorText != null && errorBuilder != null
|
||||
? errorBuilder(state.context, field.errorText!)
|
||||
: null;
|
||||
final String? errorText = error == null ? field.errorText : null;
|
||||
// Clear the decoration hintText because DropdownButton has its own hint logic.
|
||||
final String? hintText = effectiveDecoration.hintText != null ? '' : null;
|
||||
|
||||
effectiveDecoration = effectiveDecoration.copyWith(
|
||||
error: error,
|
||||
errorText: errorText,
|
||||
hintText: hintText,
|
||||
);
|
||||
}
|
||||
|
||||
// An unfocusable Focus widget so that this widget can detect if its
|
||||
// descendants have focus or not.
|
||||
return Focus(
|
||||
@ -1800,11 +1815,7 @@ class DropdownButtonFormField<T> extends FormField<T> {
|
||||
enableFeedback: enableFeedback,
|
||||
alignment: alignment,
|
||||
borderRadius: borderRadius,
|
||||
// Clear the decoration hintText because DropdownButton has its own hint logic.
|
||||
inputDecoration: effectiveDecoration.copyWith(
|
||||
errorText: field.errorText,
|
||||
hintText: effectiveDecoration.hintText != null ? '' : null,
|
||||
),
|
||||
inputDecoration: effectiveDecoration,
|
||||
isEmpty: isEmpty,
|
||||
padding: padding,
|
||||
),
|
||||
|
@ -150,6 +150,7 @@ class TextFormField extends FormField<String> {
|
||||
ValueChanged<String>? onFieldSubmitted,
|
||||
super.onSaved,
|
||||
super.validator,
|
||||
super.errorBuilder,
|
||||
List<TextInputFormatter>? inputFormatters,
|
||||
bool? enabled,
|
||||
bool? ignorePointers,
|
||||
@ -209,8 +210,17 @@ class TextFormField extends FormField<String> {
|
||||
autovalidateMode: autovalidateMode ?? AutovalidateMode.disabled,
|
||||
builder: (FormFieldState<String> field) {
|
||||
final _TextFormFieldState state = field as _TextFormFieldState;
|
||||
final InputDecoration effectiveDecoration = (decoration ?? const InputDecoration())
|
||||
InputDecoration effectiveDecoration = (decoration ?? const InputDecoration())
|
||||
.applyDefaults(Theme.of(field.context).inputDecorationTheme);
|
||||
|
||||
final String? errorText = field.errorText;
|
||||
if (errorText != null) {
|
||||
effectiveDecoration =
|
||||
errorBuilder != null
|
||||
? effectiveDecoration.copyWith(error: errorBuilder(state.context, errorText))
|
||||
: effectiveDecoration.copyWith(errorText: errorText);
|
||||
}
|
||||
|
||||
void onChangedHandler(String value) {
|
||||
field.didChange(value);
|
||||
onChanged?.call(value);
|
||||
@ -223,7 +233,7 @@ class TextFormField extends FormField<String> {
|
||||
restorationId: restorationId,
|
||||
controller: state._effectiveController,
|
||||
focusNode: focusNode,
|
||||
decoration: effectiveDecoration.copyWith(errorText: field.errorText),
|
||||
decoration: effectiveDecoration,
|
||||
keyboardType: keyboardType,
|
||||
textInputAction: textInputAction,
|
||||
style: style,
|
||||
|
@ -421,6 +421,14 @@ class _FormScope extends InheritedWidget {
|
||||
/// Used by [FormField.validator].
|
||||
typedef FormFieldValidator<T> = String? Function(T? value);
|
||||
|
||||
/// Signature for a callback that builds an error widget.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [FormField.errorBuilder], which is of this type, and passes the result error
|
||||
/// given by [TextFormField.validator].
|
||||
typedef FormFieldErrorBuilder = Widget Function(BuildContext context, String errorText);
|
||||
|
||||
/// Signature for being notified when a form field changes value.
|
||||
///
|
||||
/// Used by [FormField.onSaved].
|
||||
@ -460,12 +468,19 @@ class FormField<T> extends StatefulWidget {
|
||||
this.onSaved,
|
||||
this.forceErrorText,
|
||||
this.validator,
|
||||
this.errorBuilder,
|
||||
this.initialValue,
|
||||
this.enabled = true,
|
||||
AutovalidateMode? autovalidateMode,
|
||||
this.restorationId,
|
||||
}) : autovalidateMode = autovalidateMode ?? AutovalidateMode.disabled;
|
||||
|
||||
/// Function that returns the widget representing this form field.
|
||||
///
|
||||
/// It is passed the form field state as input, containing the current value
|
||||
/// and validation state of this field.
|
||||
final FormFieldBuilder<T> builder;
|
||||
|
||||
/// An optional method to call with the final value when the form is saved via
|
||||
/// [FormState.save].
|
||||
final FormFieldSetter<T>? onSaved;
|
||||
@ -503,10 +518,14 @@ class FormField<T> extends StatefulWidget {
|
||||
/// parameter to a space.
|
||||
final FormFieldValidator<T>? validator;
|
||||
|
||||
/// Function that returns the widget representing this form field. It is
|
||||
/// passed the form field state as input, containing the current value and
|
||||
/// validation state of this field.
|
||||
final FormFieldBuilder<T> builder;
|
||||
/// Function that returns the widget representing the error to display.
|
||||
///
|
||||
/// It is passed the form field validator error string as input.
|
||||
/// The resulting widget is passed to [InputDecoration.error].
|
||||
///
|
||||
/// If null, the validator error string is passed to
|
||||
/// [InputDecoration.errorText].
|
||||
final FormFieldErrorBuilder? errorBuilder;
|
||||
|
||||
/// An optional value to initialize the form field to, or null otherwise.
|
||||
///
|
||||
|
@ -1229,4 +1229,28 @@ void main() {
|
||||
expect(inputDecorator.isFocused, true);
|
||||
expect(inputDecorator.decoration.errorText, 'Required');
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/flutter/flutter/issues/135292.
|
||||
testWidgets('Widget returned by errorBuilder is shown', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Material(
|
||||
child: DropdownButtonFormField<String>(
|
||||
items:
|
||||
menuItems.map((String value) {
|
||||
return DropdownMenuItem<String>(value: value, child: Text(value));
|
||||
}).toList(),
|
||||
onChanged: onChanged,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
validator: (String? v) => 'Required',
|
||||
errorBuilder: (BuildContext context, String errorText) => Text('**$errorText**'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('**Required**'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
@ -1663,4 +1663,25 @@ void main() {
|
||||
expect(find.text(forceErrorText), findsOne);
|
||||
expect(find.text(decorationErrorText), findsNothing);
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/flutter/flutter/issues/135292.
|
||||
testWidgets('Widget returned by errorBuilder is shown', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: TextFormField(
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
validator: (String? value) => 'validation error',
|
||||
errorBuilder: (BuildContext context, String errorText) => Text('**$errorText**'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('**validation error**'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user