From 8a39df64d07701fab2f251cf1da6b120a6676ade Mon Sep 17 00:00:00 2001 From: Matthias Ngeo Date: Sun, 15 Sep 2024 23:22:40 +0800 Subject: [PATCH] Add form-field state (#192) * Add form-field state * Commit from GitHub Actions (Forui Presubmit) * Fix failing tests and bump min flutter version * Commit from GitHub Actions (Forui Samples Presubmit) * Fix form_field --------- Co-authored-by: Pante --- .github/workflows/forui_build.yaml | 6 +- .github/workflows/samples_build.yaml | 2 +- forui/CHANGELOG.md | 2 + forui/example/pubspec.lock | 53 ++++--- forui/example/pubspec.yaml | 2 +- forui/lib/src/foundation/form_field.dart | 14 +- .../widgets/select_group/select_group.dart | 130 +++++++++++++----- .../select_group/select_group_controller.dart | 7 + forui/pubspec.yaml | 2 +- .../select_group_golden_test.dart | 2 +- samples/pubspec.lock | 2 +- samples/pubspec.yaml | 1 + 12 files changed, 150 insertions(+), 73 deletions(-) diff --git a/.github/workflows/forui_build.yaml b/.github/workflows/forui_build.yaml index 3ce604380..02fc506fc 100644 --- a/.github/workflows/forui_build.yaml +++ b/.github/workflows/forui_build.yaml @@ -19,7 +19,7 @@ jobs: working-directory: forui strategy: matrix: - flutter-version: [ 3.22.x, 3.x ] + flutter-version: [ 3.x ] steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2.16.0 @@ -36,7 +36,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - flutter-version: [ 3.22.x, 3.x ] + flutter-version: [ 3.x ] defaults: run: working-directory: forui/example @@ -63,7 +63,7 @@ jobs: working-directory: forui/example strategy: matrix: - flutter-version: [ 3.22.x, 3.x ] + flutter-version: [ 3.x ] steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2.16.0 diff --git a/.github/workflows/samples_build.yaml b/.github/workflows/samples_build.yaml index dee697864..4856ec6dd 100644 --- a/.github/workflows/samples_build.yaml +++ b/.github/workflows/samples_build.yaml @@ -21,7 +21,7 @@ jobs: working-directory: samples strategy: matrix: - flutter-version: [ 3.22.x, 3.x ] + flutter-version: [ 3.x ] steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2.16.0 diff --git a/forui/CHANGELOG.md b/forui/CHANGELOG.md index b22e898b0..34b96f10b 100644 --- a/forui/CHANGELOG.md +++ b/forui/CHANGELOG.md @@ -1,5 +1,7 @@ ## 0.5.0 (Next) +The minimum Flutter version has been increased from `3.19.0` to `3.24.0`. + ### Additions * Add `FButton.icon(...)`. diff --git a/forui/example/pubspec.lock b/forui/example/pubspec.lock index e449b4264..4bb6c2c06 100644 --- a/forui/example/pubspec.lock +++ b/forui/example/pubspec.lock @@ -5,18 +5,23 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 url: "https://pub.dev" source: hosted - version: "67.0.0" + version: "72.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.2" analyzer: dependency: transitive description: name: analyzer - sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "6.7.0" args: dependency: transitive description: @@ -77,18 +82,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7" + sha256: dd09dd4e2b078992f42aac7f1a622f01882a8492fef08486b27ddde929c19f04 url: "https://pub.dev" source: hosted - version: "2.4.11" + version: "2.4.12" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 url: "https://pub.dev" source: hosted - version: "7.3.1" + version: "7.3.2" built_collection: dependency: transitive description: @@ -157,18 +162,18 @@ packages: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" dart_style: dependency: transitive description: name: dart_style - sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "2.3.7" fake_async: dependency: transitive description: @@ -181,10 +186,10 @@ packages: dependency: transitive description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" file: dependency: transitive description: @@ -367,6 +372,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + macros: + dependency: transitive + description: + name: macros + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + url: "https://pub.dev" + source: hosted + version: "0.1.2-main.4" matcher: dependency: transitive description: @@ -395,10 +408,10 @@ packages: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.6" mockito: dependency: "direct dev" description: @@ -459,10 +472,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "490539678396d4c3c0b06efdaab75ae60675c3e0c66f72bc04c2e2c1e0e2abeb" + sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" url: "https://pub.dev" source: hosted - version: "2.2.9" + version: "2.2.10" path_provider_foundation: dependency: transitive description: @@ -749,5 +762,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=3.22.0" + dart: ">=3.5.0-259.0.dev <4.0.0" + flutter: ">=3.24.0" diff --git a/forui/example/pubspec.yaml b/forui/example/pubspec.yaml index 4b27ba82e..f4cfe9daa 100644 --- a/forui/example/pubspec.yaml +++ b/forui/example/pubspec.yaml @@ -20,7 +20,7 @@ version: 1.0.0+1 environment: sdk: '>=3.3.0 <4.0.0' - flutter: ">=3.19.0" + flutter: ">=3.24.0" dependencies: flutter: diff --git a/forui/lib/src/foundation/form_field.dart b/forui/lib/src/foundation/form_field.dart index 7e4c10974..20eaa490c 100644 --- a/forui/lib/src/foundation/form_field.dart +++ b/forui/lib/src/foundation/form_field.dart @@ -72,20 +72,12 @@ abstract class FFormField extends StatelessWidget { @override Widget build(BuildContext context) => FormField( onSaved: onSave, - // TODO: Directly use forceErrorText when available. https://api.flutter.dev/flutter/widgets/FormField/forceErrorText.html - validator: forceErrorText == null ? validator : (_) => forceErrorText, + validator: validator, initialValue: initialValue, enabled: enabled, - autovalidateMode: - forceErrorText == null ? autovalidateMode : AutovalidateMode.always, // Workaround for forceErrorText. + autovalidateMode: autovalidateMode, restorationId: restorationId, - builder: (state) { - if (forceErrorText != null && !state.hasError) { - state.validate(); // TODO: Remove workaround when forceErrorText is available. - } - - return builder(context, state); - }, + builder: (state) => builder(context, state), ); /// The builder for the [FormField]. diff --git a/forui/lib/src/widgets/select_group/select_group.dart b/forui/lib/src/widgets/select_group/select_group.dart index d69d9ad84..da0374af8 100644 --- a/forui/lib/src/widgets/select_group/select_group.dart +++ b/forui/lib/src/widgets/select_group/select_group.dart @@ -1,3 +1,5 @@ +// ignore_for_file: invalid_use_of_protected_member + import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; @@ -12,7 +14,9 @@ import 'package:forui/forui.dart'; /// See: /// * https://forui.dev/docs/select-group for working examples. /// * [FSelectGroupStyle] for customizing a select group's appearance. -class FSelectGroup extends StatelessWidget { +class FSelectGroup extends FormField> { + static Widget _defaultErrorBuilder(BuildContext context, String error) => Text(error); + /// The controller. /// /// See: @@ -29,52 +33,61 @@ class FSelectGroup extends StatelessWidget { /// The description displayed below the [label]. final Widget? description; - /// The error displayed below the [description]. - /// - /// If the value is present, the select group is in an error state. - final Widget? error; + /// The builder for errors displayed below the [description]. Defaults to displaying the error message. + final Widget Function(BuildContext, String) errorBuilder; /// The items. final List> items; /// Creates a [FSelectGroup]. - const FSelectGroup({ + FSelectGroup({ required this.controller, required this.items, this.style, this.label, this.description, - this.error, + this.errorBuilder = _defaultErrorBuilder, + super.onSaved, + super.validator, + super.initialValue, + super.forceErrorText, + super.enabled = true, + super.autovalidateMode, + super.restorationId, super.key, - }); + }) : super( + builder: (field) { + final state = field as _State; + final groupStyle = style ?? state.context.theme.selectGroupStyle; + final labelState = switch (state) { + _ when !enabled => FLabelState.disabled, + _ when state.errorText != null => FLabelState.error, + _ => FLabelState.enabled, + }; + + return FLabel( + axis: Axis.vertical, + state: labelState, + style: groupStyle.labelStyle, + label: label, + description: description, + error: labelState == FLabelState.error ? errorBuilder(state.context, state.errorText!) : null, + child: Column( + children: [ + for (final item in items) + item.builder( + state.context, + controller.select, + controller.contains(item.value), + ), + ], + ), + ); + }, + ); @override - Widget build(BuildContext context) { - final style = this.style ?? context.theme.selectGroupStyle; - final labelState = error != null ? FLabelState.error : FLabelState.enabled; - - return FLabel( - axis: Axis.vertical, - state: labelState, - style: style.labelStyle, - label: label, - description: description, - error: error, - child: ListenableBuilder( - listenable: controller, - builder: (context, _) => Column( - children: [ - for (final item in items) - item.builder( - context, - controller.select, - controller.contains(item.value), - ), - ], - ), - ), - ); - } + FormFieldState> createState() => _State(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { @@ -82,10 +95,59 @@ class FSelectGroup extends StatelessWidget { properties ..add(DiagnosticsProperty('style', style)) ..add(DiagnosticsProperty('controller', controller)) + ..add(ObjectFlagProperty.has('errorBuilder', errorBuilder)) ..add(IterableProperty('items', items)); } } +class _State extends FormFieldState> { + @override + void initState() { + super.initState(); + widget.controller.addListener(_handleControllerChanged); + } + + @override + void didUpdateWidget(covariant FSelectGroup old) { + super.didUpdateWidget(old); + if (widget.controller == old.controller) { + return; + } + + widget.controller.addListener(_handleControllerChanged); + old.controller.removeListener(_handleControllerChanged); + } + + @override + void didChange(Set? values) { + super.didChange(values); + if (!setEquals(widget.controller.values, values)) { + widget.controller.values = values ?? {}; + } + } + + @override + void reset() { + // Set the controller value before calling super.reset() to let _handleControllerChanged suppress the change. + widget.controller.values = widget.initialValue ?? {}; + super.reset(); + } + + void _handleControllerChanged() { + // Suppress changes that originated from within this class. + // + // In the case where a controller has been passed in to this widget, we register this change listener. In these + // cases, we'll also receive change notifications for changes originating from within this class -- for example, the + // reset() method. In such cases, the FormField value will already have been set. + if (widget.controller.values != value) { + didChange(widget.controller.values); + } + } + + @override + FSelectGroup get widget => super.widget as FSelectGroup; +} + /// [FSelectGroup]'s style. class FSelectGroupStyle with Diagnosticable { /// The [FLabel]'s style. diff --git a/forui/lib/src/widgets/select_group/select_group_controller.dart b/forui/lib/src/widgets/select_group/select_group_controller.dart index bdc19348f..34f76d27e 100644 --- a/forui/lib/src/widgets/select_group/select_group_controller.dart +++ b/forui/lib/src/widgets/select_group/select_group_controller.dart @@ -17,6 +17,13 @@ abstract class FSelectGroupController with ChangeNotifier { /// The currently selected values. Set get values => {..._values}; + @protected + set values(Set values) => { + _values.clear(), + _values.addAll(values), + notifyListeners(), + }; + @override bool operator ==(Object other) => identical(this, other) || diff --git a/forui/pubspec.yaml b/forui/pubspec.yaml index 9a38d607a..ff4c58cb6 100644 --- a/forui/pubspec.yaml +++ b/forui/pubspec.yaml @@ -11,7 +11,7 @@ topics: environment: sdk: ">=3.3.0 <4.0.0" - flutter: ">=3.22.0" + flutter: ">=3.24.0" dependencies: collection: ^1.18.0 diff --git a/forui/test/src/widgets/select_group/select_group_golden_test.dart b/forui/test/src/widgets/select_group/select_group_golden_test.dart index ed4edcf8a..77615be40 100644 --- a/forui/test/src/widgets/select_group/select_group_golden_test.dart +++ b/forui/test/src/widgets/select_group/select_group_golden_test.dart @@ -68,7 +68,7 @@ void main() { child: FSelectGroup( label: const Text('Select Group'), description: const Text('Select Group Description'), - error: const Text('Some error message.'), + forceErrorText: 'Some error message.', controller: FMultiSelectGroupController(values: {1}), items: [ FSelectGroupItem.checkbox( diff --git a/samples/pubspec.lock b/samples/pubspec.lock index f923d9cd8..6a9400689 100644 --- a/samples/pubspec.lock +++ b/samples/pubspec.lock @@ -770,4 +770,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.4.3 <4.0.0" - flutter: ">=3.22.0" + flutter: ">=3.24.0" diff --git a/samples/pubspec.yaml b/samples/pubspec.yaml index f4cf46eab..af204cd6f 100644 --- a/samples/pubspec.yaml +++ b/samples/pubspec.yaml @@ -19,6 +19,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: + flutter: ">=3.24.0" sdk: '>=3.4.3 <4.0.0' # Dependencies specify other packages that your package needs in order to work.