diff --git a/.github/workflows/base-beta.yaml b/.github/workflows/base-beta.yaml new file mode 100644 index 000000000..ec06eb327 --- /dev/null +++ b/.github/workflows/base-beta.yaml @@ -0,0 +1,39 @@ +name: Base (beta) + +on: + schedule: + - cron: '0 0 */15 * *' # Runs every 15 days for verifying changes on Flutter beta channel + push: + branches: [beta] + tags: + - '[0-9]+.[0-9]+.[0-9]+-*' + + pull_request: + branches: [beta] + + workflow_dispatch: + +# This ensures that previous jobs for the PR are canceled when PR is updated +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + uses: flutter-form-builder-ecosystem/.github/.github/workflows/minimal-quality.yaml@main + with: + codecov-name: flutter_form_builder + enable-fix-tests: true + fvm-flavor: beta + example: + uses: flutter-form-builder-ecosystem/.github/.github/workflows/build-examples.yaml@main + with: + fvm-flavor: beta + + + deployment: + permissions: + id-token: write + uses: flutter-form-builder-ecosystem/.github/.github/workflows/deployment.yaml@main + if: ${{ github.ref_type == 'tag' }} + needs: [build, example] diff --git a/.github/workflows/base.yaml b/.github/workflows/base.yaml index e5374008e..40d54471d 100644 --- a/.github/workflows/base.yaml +++ b/.github/workflows/base.yaml @@ -4,7 +4,7 @@ on: push: branches: [main] tags: - - '[0-9]+.[0-9]+.[0-9]+*' + - '[0-9]+.[0-9]+.[0-9]+' pull_request: branches: [main] diff --git a/example/lib/sources/complete_form.dart b/example/lib/sources/complete_form.dart index 844947af6..5364363dc 100644 --- a/example/lib/sources/complete_form.dart +++ b/example/lib/sources/complete_form.dart @@ -89,6 +89,20 @@ class _CompleteFormState extends State { validator: FormBuilderValidators.compose([ FormBuilderValidators.min(6), ]), + errorBuilder: (context, errorText) => TextButton( + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(errorText), + duration: const Duration(seconds: 2), + ), + ); + }, + child: Text( + errorText, + style: const TextStyle(color: Colors.red), + ), + ), onChanged: _onChanged, min: 0.0, max: 10.0, diff --git a/example/pubspec.lock b/example/pubspec.lock index 9ae53bb79..89ff28531 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" boolean_selector: dependency: transitive description: diff --git a/lib/src/fields/form_builder_checkbox.dart b/lib/src/fields/form_builder_checkbox.dart index 8d7f5ba01..f8aebb68c 100644 --- a/lib/src/fields/form_builder_checkbox.dart +++ b/lib/src/fields/form_builder_checkbox.dart @@ -106,6 +106,7 @@ class FormBuilderCheckbox extends FormBuilderFieldDecoration { super.onReset, super.focusNode, super.restorationId, + super.errorBuilder, required this.title, this.activeColor, this.autofocus = false, diff --git a/lib/src/fields/form_builder_checkbox_group.dart b/lib/src/fields/form_builder_checkbox_group.dart index d40f5350f..456e9e21c 100644 --- a/lib/src/fields/form_builder_checkbox_group.dart +++ b/lib/src/fields/form_builder_checkbox_group.dart @@ -45,6 +45,7 @@ class FormBuilderCheckboxGroup extends FormBuilderFieldDecoration> { super.onReset, super.focusNode, super.restorationId, + super.errorBuilder, required this.options, this.activeColor, this.checkColor, diff --git a/lib/src/fields/form_builder_choice_chips.dart b/lib/src/fields/form_builder_choice_chips.dart index 5322fcf83..b494e3b87 100644 --- a/lib/src/fields/form_builder_choice_chips.dart +++ b/lib/src/fields/form_builder_choice_chips.dart @@ -359,6 +359,7 @@ class FormBuilderChoiceChips extends FormBuilderFieldDecoration { super.onChanged, super.valueTransformer, super.onReset, + super.errorBuilder, this.alignment = WrapAlignment.start, this.avatarBorder = const CircleBorder(), this.backgroundColor, diff --git a/lib/src/fields/form_builder_date_range_picker.dart b/lib/src/fields/form_builder_date_range_picker.dart index ff4eacc77..6293ba10c 100644 --- a/lib/src/fields/form_builder_date_range_picker.dart +++ b/lib/src/fields/form_builder_date_range_picker.dart @@ -79,6 +79,7 @@ class FormBuilderDateRangePicker super.onReset, super.focusNode, super.restorationId, + super.errorBuilder, required this.firstDate, required this.lastDate, this.format, diff --git a/lib/src/fields/form_builder_date_time_picker.dart b/lib/src/fields/form_builder_date_time_picker.dart index cc378b2a2..2ad8ea6a2 100644 --- a/lib/src/fields/form_builder_date_time_picker.dart +++ b/lib/src/fields/form_builder_date_time_picker.dart @@ -138,6 +138,7 @@ class FormBuilderDateTimePicker extends FormBuilderFieldDecoration { super.onReset, super.focusNode, super.restorationId, + super.errorBuilder, this.inputType = InputType.both, this.scrollPadding = const EdgeInsets.all(20.0), this.cursorWidth = 2.0, diff --git a/lib/src/fields/form_builder_dropdown.dart b/lib/src/fields/form_builder_dropdown.dart index 87f9dfacb..685584b1f 100644 --- a/lib/src/fields/form_builder_dropdown.dart +++ b/lib/src/fields/form_builder_dropdown.dart @@ -264,6 +264,7 @@ class FormBuilderDropdown extends FormBuilderFieldDecoration { super.onReset, super.focusNode, super.restorationId, + super.errorBuilder, required this.items, this.isExpanded = true, this.isDense = true, diff --git a/lib/src/fields/form_builder_filter_chips.dart b/lib/src/fields/form_builder_filter_chips.dart index b8c549a45..fd53660a4 100644 --- a/lib/src/fields/form_builder_filter_chips.dart +++ b/lib/src/fields/form_builder_filter_chips.dart @@ -46,6 +46,7 @@ class FormBuilderFilterChips extends FormBuilderFieldDecoration> { super.initialValue, required super.name, super.restorationId, + super.errorBuilder, required this.options, this.alignment = WrapAlignment.start, this.avatarBorder = const CircleBorder(), diff --git a/lib/src/fields/form_builder_radio_group.dart b/lib/src/fields/form_builder_radio_group.dart index c470e097e..f4a1e4b92 100644 --- a/lib/src/fields/form_builder_radio_group.dart +++ b/lib/src/fields/form_builder_radio_group.dart @@ -38,6 +38,11 @@ class FormBuilderRadioGroup extends FormBuilderFieldDecoration { required super.name, required this.options, super.initialValue, + super.onChanged, + super.valueTransformer, + super.onReset, + super.restorationId, + super.errorBuilder, this.activeColor, this.controlAffinity = ControlAffinity.leading, this.disabled, @@ -54,10 +59,6 @@ class FormBuilderRadioGroup extends FormBuilderFieldDecoration { this.wrapSpacing = 0.0, this.wrapTextDirection, this.wrapVerticalDirection = VerticalDirection.down, - super.onChanged, - super.valueTransformer, - super.onReset, - super.restorationId, this.itemDecoration, }) : super( builder: (FormFieldState field) { diff --git a/lib/src/fields/form_builder_range_slider.dart b/lib/src/fields/form_builder_range_slider.dart index 0bf374dfc..d43b61bac 100644 --- a/lib/src/fields/form_builder_range_slider.dart +++ b/lib/src/fields/form_builder_range_slider.dart @@ -174,6 +174,7 @@ class FormBuilderRangeSlider extends FormBuilderFieldDecoration { super.autovalidateMode = AutovalidateMode.disabled, super.onReset, super.focusNode, + super.errorBuilder, required this.min, required this.max, this.divisions, diff --git a/lib/src/fields/form_builder_slider.dart b/lib/src/fields/form_builder_slider.dart index e394d1105..ad65bdda4 100644 --- a/lib/src/fields/form_builder_slider.dart +++ b/lib/src/fields/form_builder_slider.dart @@ -195,6 +195,7 @@ class FormBuilderSlider extends FormBuilderFieldDecoration { super.onReset, super.focusNode, super.restorationId, + super.errorBuilder, required this.min, required this.max, this.divisions, diff --git a/lib/src/fields/form_builder_switch.dart b/lib/src/fields/form_builder_switch.dart index fa7b0520f..9304324d3 100644 --- a/lib/src/fields/form_builder_switch.dart +++ b/lib/src/fields/form_builder_switch.dart @@ -103,6 +103,7 @@ class FormBuilderSwitch extends FormBuilderFieldDecoration { super.onReset, super.focusNode, super.restorationId, + super.errorBuilder, required this.title, this.activeColor, this.activeTrackColor, diff --git a/lib/src/fields/form_builder_text_field.dart b/lib/src/fields/form_builder_text_field.dart index 4568ce200..172720ea0 100644 --- a/lib/src/fields/form_builder_text_field.dart +++ b/lib/src/fields/form_builder_text_field.dart @@ -379,6 +379,7 @@ class FormBuilderTextField extends FormBuilderFieldDecoration { super.focusNode, super.restorationId, String? initialValue, + super.errorBuilder, this.readOnly = false, this.maxLines = 1, this.obscureText = false, diff --git a/lib/src/form_builder_field.dart b/lib/src/form_builder_field.dart index 588f65a7f..f38d15c18 100644 --- a/lib/src/form_builder_field.dart +++ b/lib/src/form_builder_field.dart @@ -53,6 +53,7 @@ class FormBuilderField extends FormField { super.validator, super.restorationId, required super.builder, + super.errorBuilder, required this.name, this.valueTransformer, this.onChanged, diff --git a/lib/src/form_builder_field_decoration.dart b/lib/src/form_builder_field_decoration.dart index bff8e0fd9..e48104480 100644 --- a/lib/src/form_builder_field_decoration.dart +++ b/lib/src/form_builder_field_decoration.dart @@ -19,6 +19,7 @@ class FormBuilderFieldDecoration extends FormBuilderField { super.onChanged, super.onReset, super.focusNode, + super.errorBuilder, required super.builder, this.decoration = const InputDecoration(), }) : assert( @@ -45,15 +46,22 @@ class FormBuilderFieldDecorationState< F get widget => super.widget as F; /// Get the decoration with the current state - InputDecoration get decoration => widget.decoration.copyWith( - // Read only allow show error to support property skipDisabled - errorText: + InputDecoration get decoration { + final String? efectiveErrorText = widget.enabled || readOnly ? widget.decoration.errorText ?? errorText - : null, - enabled: - widget.decoration.enabled ? widget.enabled : widget.decoration.enabled, - ); + : null; + + return widget.decoration.copyWith( + // Read only allow show error to support property skipDisabled + errorText: widget.errorBuilder != null ? null : efectiveErrorText, + error: + widget.errorBuilder != null && efectiveErrorText != null + ? widget.errorBuilder!(context, efectiveErrorText) + : null, + enabled: widget.decoration.enabled ? widget.enabled : false, + ); + } @override bool get hasError => super.hasError || widget.decoration.errorText != null; diff --git a/pubspec.lock b/pubspec.lock index 0e6bb7614..09725c745 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" boolean_selector: dependency: transitive description: diff --git a/test/src/form_builder_field_decoration_test.dart b/test/src/form_builder_field_decoration_test.dart index 96a848459..9b540f46e 100644 --- a/test/src/form_builder_field_decoration_test.dart +++ b/test/src/form_builder_field_decoration_test.dart @@ -43,6 +43,53 @@ void main() { // The field should be valid again expect(decorationFieldKey.currentState?.isValid, isTrue); }); + + testWidgets('when set errorBuilder then show a custom widget on error', ( + widgetTester, + ) async { + final decorationFieldKey = GlobalKey(); + const name = 'testField'; + const errorTextField = 'error text field'; + final errorTextKey = Key('errorTextKey'); + final widget = FormBuilderFieldDecoration( + key: decorationFieldKey, + name: name, + errorBuilder: (context, errorText) { + return Text( + errorText, + key: errorTextKey, + style: const TextStyle(color: Colors.red), + ); + }, + builder: (FormFieldState field) { + return InputDecorator( + decoration: (field as FormBuilderFieldDecorationState).decoration, + child: TextField( + onChanged: (value) { + field.didChange(value); + }, + ), + ); + }, + ); + + await widgetTester.pumpWidget(buildTestableFieldWidget(widget)); + + // Initially, the field should be valid + expect(decorationFieldKey.currentState?.isValid, isTrue); + + decorationFieldKey.currentState?.invalidate(errorTextField); + + // The field should be invalid + expect(decorationFieldKey.currentState?.isValid, isFalse); + + await widgetTester.pumpAndSettle(); + + // Check if the custom error widget is displayed + expect(find.text(errorTextField), findsOneWidget); + expect(find.byKey(errorTextKey), findsOneWidget); + }); + group('decoration enabled -', () { testWidgets( 'when change the error text then the field should be invalid',