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,
|
InputDecoration? decoration,
|
||||||
super.onSaved,
|
super.onSaved,
|
||||||
super.validator,
|
super.validator,
|
||||||
|
super.errorBuilder,
|
||||||
AutovalidateMode? autovalidateMode,
|
AutovalidateMode? autovalidateMode,
|
||||||
double? menuMaxHeight,
|
double? menuMaxHeight,
|
||||||
bool? enableFeedback,
|
bool? enableFeedback,
|
||||||
@ -1747,10 +1748,8 @@ class DropdownButtonFormField<T> extends FormField<T> {
|
|||||||
autovalidateMode: autovalidateMode ?? AutovalidateMode.disabled,
|
autovalidateMode: autovalidateMode ?? AutovalidateMode.disabled,
|
||||||
builder: (FormFieldState<T> field) {
|
builder: (FormFieldState<T> field) {
|
||||||
final _DropdownButtonFormFieldState<T> state = field as _DropdownButtonFormFieldState<T>;
|
final _DropdownButtonFormFieldState<T> state = field as _DropdownButtonFormFieldState<T>;
|
||||||
final InputDecoration decorationArg = decoration ?? const InputDecoration();
|
InputDecoration effectiveDecoration = (decoration ?? const InputDecoration())
|
||||||
final InputDecoration effectiveDecoration = decorationArg.applyDefaults(
|
.applyDefaults(Theme.of(field.context).inputDecorationTheme);
|
||||||
Theme.of(field.context).inputDecorationTheme,
|
|
||||||
);
|
|
||||||
|
|
||||||
final bool showSelectedItem =
|
final bool showSelectedItem =
|
||||||
items != null &&
|
items != null &&
|
||||||
@ -1767,6 +1766,22 @@ class DropdownButtonFormField<T> extends FormField<T> {
|
|||||||
: effectiveHint != null || effectiveDisabledHint != null;
|
: effectiveHint != null || effectiveDisabledHint != null;
|
||||||
final bool isEmpty = !showSelectedItem && !isHintOrDisabledHintAvailable;
|
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
|
// An unfocusable Focus widget so that this widget can detect if its
|
||||||
// descendants have focus or not.
|
// descendants have focus or not.
|
||||||
return Focus(
|
return Focus(
|
||||||
@ -1800,11 +1815,7 @@ class DropdownButtonFormField<T> extends FormField<T> {
|
|||||||
enableFeedback: enableFeedback,
|
enableFeedback: enableFeedback,
|
||||||
alignment: alignment,
|
alignment: alignment,
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
// Clear the decoration hintText because DropdownButton has its own hint logic.
|
inputDecoration: effectiveDecoration,
|
||||||
inputDecoration: effectiveDecoration.copyWith(
|
|
||||||
errorText: field.errorText,
|
|
||||||
hintText: effectiveDecoration.hintText != null ? '' : null,
|
|
||||||
),
|
|
||||||
isEmpty: isEmpty,
|
isEmpty: isEmpty,
|
||||||
padding: padding,
|
padding: padding,
|
||||||
),
|
),
|
||||||
|
@ -150,6 +150,7 @@ class TextFormField extends FormField<String> {
|
|||||||
ValueChanged<String>? onFieldSubmitted,
|
ValueChanged<String>? onFieldSubmitted,
|
||||||
super.onSaved,
|
super.onSaved,
|
||||||
super.validator,
|
super.validator,
|
||||||
|
super.errorBuilder,
|
||||||
List<TextInputFormatter>? inputFormatters,
|
List<TextInputFormatter>? inputFormatters,
|
||||||
bool? enabled,
|
bool? enabled,
|
||||||
bool? ignorePointers,
|
bool? ignorePointers,
|
||||||
@ -209,8 +210,17 @@ class TextFormField extends FormField<String> {
|
|||||||
autovalidateMode: autovalidateMode ?? AutovalidateMode.disabled,
|
autovalidateMode: autovalidateMode ?? AutovalidateMode.disabled,
|
||||||
builder: (FormFieldState<String> field) {
|
builder: (FormFieldState<String> field) {
|
||||||
final _TextFormFieldState state = field as _TextFormFieldState;
|
final _TextFormFieldState state = field as _TextFormFieldState;
|
||||||
final InputDecoration effectiveDecoration = (decoration ?? const InputDecoration())
|
InputDecoration effectiveDecoration = (decoration ?? const InputDecoration())
|
||||||
.applyDefaults(Theme.of(field.context).inputDecorationTheme);
|
.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) {
|
void onChangedHandler(String value) {
|
||||||
field.didChange(value);
|
field.didChange(value);
|
||||||
onChanged?.call(value);
|
onChanged?.call(value);
|
||||||
@ -223,7 +233,7 @@ class TextFormField extends FormField<String> {
|
|||||||
restorationId: restorationId,
|
restorationId: restorationId,
|
||||||
controller: state._effectiveController,
|
controller: state._effectiveController,
|
||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
decoration: effectiveDecoration.copyWith(errorText: field.errorText),
|
decoration: effectiveDecoration,
|
||||||
keyboardType: keyboardType,
|
keyboardType: keyboardType,
|
||||||
textInputAction: textInputAction,
|
textInputAction: textInputAction,
|
||||||
style: style,
|
style: style,
|
||||||
|
@ -421,6 +421,14 @@ class _FormScope extends InheritedWidget {
|
|||||||
/// Used by [FormField.validator].
|
/// Used by [FormField.validator].
|
||||||
typedef FormFieldValidator<T> = String? Function(T? value);
|
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.
|
/// Signature for being notified when a form field changes value.
|
||||||
///
|
///
|
||||||
/// Used by [FormField.onSaved].
|
/// Used by [FormField.onSaved].
|
||||||
@ -460,12 +468,19 @@ class FormField<T> extends StatefulWidget {
|
|||||||
this.onSaved,
|
this.onSaved,
|
||||||
this.forceErrorText,
|
this.forceErrorText,
|
||||||
this.validator,
|
this.validator,
|
||||||
|
this.errorBuilder,
|
||||||
this.initialValue,
|
this.initialValue,
|
||||||
this.enabled = true,
|
this.enabled = true,
|
||||||
AutovalidateMode? autovalidateMode,
|
AutovalidateMode? autovalidateMode,
|
||||||
this.restorationId,
|
this.restorationId,
|
||||||
}) : autovalidateMode = autovalidateMode ?? AutovalidateMode.disabled;
|
}) : 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
|
/// An optional method to call with the final value when the form is saved via
|
||||||
/// [FormState.save].
|
/// [FormState.save].
|
||||||
final FormFieldSetter<T>? onSaved;
|
final FormFieldSetter<T>? onSaved;
|
||||||
@ -503,10 +518,14 @@ class FormField<T> extends StatefulWidget {
|
|||||||
/// parameter to a space.
|
/// parameter to a space.
|
||||||
final FormFieldValidator<T>? validator;
|
final FormFieldValidator<T>? validator;
|
||||||
|
|
||||||
/// Function that returns the widget representing this form field. It is
|
/// Function that returns the widget representing the error to display.
|
||||||
/// passed the form field state as input, containing the current value and
|
///
|
||||||
/// validation state of this field.
|
/// It is passed the form field validator error string as input.
|
||||||
final FormFieldBuilder<T> builder;
|
/// 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.
|
/// An optional value to initialize the form field to, or null otherwise.
|
||||||
///
|
///
|
||||||
|
@ -1229,4 +1229,28 @@ void main() {
|
|||||||
expect(inputDecorator.isFocused, true);
|
expect(inputDecorator.isFocused, true);
|
||||||
expect(inputDecorator.decoration.errorText, 'Required');
|
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(forceErrorText), findsOne);
|
||||||
expect(find.text(decorationErrorText), findsNothing);
|
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