这是indexloc提供的服务,不要输入任何密码
Skip to content

Add FormField.errorBuilder #162255

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 20 additions & 9 deletions packages/flutter/lib/src/material/dropdown.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1722,6 +1722,7 @@ class DropdownButtonFormField<T> extends FormField<T> {
InputDecoration? decoration,
super.onSaved,
super.validator,
super.errorBuilder,
AutovalidateMode? autovalidateMode,
double? menuMaxHeight,
bool? enableFeedback,
Expand All @@ -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 &&
Expand All @@ -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(
Expand Down Expand Up @@ -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,
),
Expand Down
14 changes: 12 additions & 2 deletions packages/flutter/lib/src/material/text_form_field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand All @@ -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,
Expand Down
27 changes: 23 additions & 4 deletions packages/flutter/lib/src/widgets/form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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].
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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.
///
Expand Down
24 changes: 24 additions & 0 deletions packages/flutter/test/material/dropdown_form_field_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
}
21 changes: 21 additions & 0 deletions packages/flutter/test/material/text_form_field_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
}