Skip to content
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

Add form-field state #192

Merged
merged 5 commits into from
Sep 15, 2024
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
6 changes: 3 additions & 3 deletions .github/workflows/forui_build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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/[email protected]
Expand All @@ -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
Expand All @@ -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/[email protected]
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/samples_build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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/[email protected]
Expand Down
2 changes: 2 additions & 0 deletions forui/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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(...)`.
Expand Down
53 changes: 33 additions & 20 deletions forui/example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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"
2 changes: 1 addition & 1 deletion forui/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
14 changes: 3 additions & 11 deletions forui/lib/src/foundation/form_field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -72,20 +72,12 @@ abstract class FFormField<T> extends StatelessWidget {
@override
Widget build(BuildContext context) => FormField<T>(
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].
Expand Down
130 changes: 96 additions & 34 deletions forui/lib/src/widgets/select_group/select_group.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// ignore_for_file: invalid_use_of_protected_member

import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';

Expand All @@ -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<T> extends StatelessWidget {
class FSelectGroup<T> extends FormField<Set<T>> {
static Widget _defaultErrorBuilder(BuildContext context, String error) => Text(error);

/// The controller.
///
/// See:
Expand All @@ -29,63 +33,121 @@ class FSelectGroup<T> 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<FSelectGroupItem<T>> 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,
kawaijoe marked this conversation as resolved.
Show resolved Hide resolved
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<Set<T>> createState() => _State();

@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty('style', style))
..add(DiagnosticsProperty('controller', controller))
..add(ObjectFlagProperty.has('errorBuilder', errorBuilder))
..add(IterableProperty('items', items));
}
}

class _State<T> extends FormFieldState<Set<T>> {
@override
void initState() {
super.initState();
widget.controller.addListener(_handleControllerChanged);
}

@override
void didUpdateWidget(covariant FSelectGroup<T> old) {
super.didUpdateWidget(old);
if (widget.controller == old.controller) {
return;
}

widget.controller.addListener(_handleControllerChanged);
old.controller.removeListener(_handleControllerChanged);
}

@override
void didChange(Set<T>? 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<T> get widget => super.widget as FSelectGroup<T>;
}

/// [FSelectGroup]'s style.
class FSelectGroupStyle with Diagnosticable {
/// The [FLabel]'s style.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ abstract class FSelectGroupController<T> with ChangeNotifier {
/// The currently selected values.
Set<T> get values => {..._values};

@protected
set values(Set<T> values) => {
Pante marked this conversation as resolved.
Show resolved Hide resolved
_values.clear(),
_values.addAll(values),
notifyListeners(),
};

@override
bool operator ==(Object other) =>
identical(this, other) ||
Expand Down
Loading
Loading