Skip to content

Commit

Permalink
Add max selected chips param to limit maximum selection.
Browse files Browse the repository at this point in the history
Ref -> #36
  • Loading branch information
TheAlphamerc committed Oct 24, 2023
1 parent 2eca29a commit d3abab2
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 12 deletions.
2 changes: 2 additions & 0 deletions README.md
Expand Up @@ -211,6 +211,8 @@ Empty screen | FilterListDialog | Selected chip | Result fro
| hideHeader|`bool`|Hide complete header section from filter dialog.|
| headerCloseIcon|`Widget`|Widget to close the dialog.|
| hideSelectedTextCount|`bool`|Hide selected text count.|
| enableOnlySingleSelection|`bool`| Enable only single selection |
| maximumSelectionLength|`int`| Set maximum selection length.|
| hideSearchField|`bool`|Hide search text field.|
| headlineText|`String`|Set header text of filter dialog.|
| backgroundColor|`Color`|Set background color of filter color|
Expand Down
56 changes: 49 additions & 7 deletions lib/src/filter_list_delegate.dart
Expand Up @@ -26,6 +26,28 @@ typedef AppbarBottom = PreferredSizeWidget Function(BuildContext context);
///
/// The [onApplyButtonClick] is a callback which return list of all selected items on apply button click. if no item is selected then it will return empty list.
///
/// The [applyButtonStyle] is used to configure the apply button's visuals.
///
/// The [applyButtonText] is used to configure the apply button's text.
///
/// The [buildAppbarBottom] is used to configure the bottom of the appbar.
///
/// The [enableOnlySingleSelection] is used to configure the selection mode. If true, only one item can be selected at a time.
///
/// The [hideClearSearchIcon] is used to configure the clear search icon. If true, the clear search icon will be hidden.
///
/// The [emptySearchChild] is used to configure the empty search child. If null, the empty search child will be a SizedBox.
///
/// The [searchFieldHint] is used to configure the search field hint text.
///
/// The [theme] is used to configure the theme of the filter list delegate.
///
/// The [maximumSelectionLength] is used to configure the maximum number of items that can be selected.
///
/// The [selectedListData] is used to configure the list of items that are selected by default.
///
/// The [searchFieldStyle] is used to configure the search field's text style.
///
/// ### This example shows how to use [FilterListDialog]
///
/// ``` dart
Expand Down Expand Up @@ -74,6 +96,7 @@ class FilterListDelegate<T> extends SearchDelegate<T?> {

final ButtonStyle? applyButtonStyle;
final List<T>? selectedListData;
final int? maximumSelectionLength;

final FilterListDelegateThemeData? theme;
FilterListDelegate({
Expand All @@ -91,6 +114,7 @@ class FilterListDelegate<T> extends SearchDelegate<T?> {
this.emptySearchChild,
this.theme,
this.applyButtonStyle,
this.maximumSelectionLength,
this.hideClearSearchIcon = false,
this.applyButtonText = 'Apply',
}) : assert(searchFieldStyle == null || searchFieldDecorationTheme == null,
Expand Down Expand Up @@ -151,6 +175,8 @@ One of the tileLabel or suggestionBuilder is required
TextStyle? searchFieldStyle,
AppbarBottom? buildAppbarBottom,
bool enableOnlySingleSelection = false,
int? maximumSelectionLength,
bool hideClearSearchIcon = false,
Widget? emptySearchChild,
FilterListDelegateThemeData? theme,
ButtonStyle? applyButtonStyle,
Expand All @@ -174,6 +200,8 @@ One of the tileLabel or suggestionBuilder is required
theme: theme,
applyButtonStyle: applyButtonStyle,
applyButtonText: applyButtonText!,
hideClearSearchIcon: hideClearSearchIcon,
maximumSelectionLength: maximumSelectionLength,
),
);

Expand Down Expand Up @@ -234,9 +262,9 @@ One of the tileLabel or suggestionBuilder is required
Widget _result(BuildContext ctx) {
return StateProvider<FilterState<T>>(
value: FilterState<T>(
allItems: listData,
selectedItems: selectedListData,
),
allItems: listData,
selectedItems: selectedListData,
maximumSelectionLength: maximumSelectionLength),
child: FilterListDelegateTheme(
theme: theme ?? FilterListDelegateThemeData(),
child: Builder(
Expand All @@ -247,6 +275,13 @@ One of the tileLabel or suggestionBuilder is required
itemBuilder: (context, index) {
final theme = FilterListDelegateTheme.of(innerContext);
final item = tempList[index];
final selected = isSelected(item);
// ignore: avoid_bool_literals_in_conditional_expressions
final maxSelectionReached = !selected &&
maximumSelectionLength != null &&
selectedItems != null
? selectedItems!.length >= maximumSelectionLength!
: false;
if (suggestionBuilder != null) {
return GestureDetector(
onTap: () => onItemSelect(context, item),
Expand All @@ -268,16 +303,18 @@ One of the tileLabel or suggestionBuilder is required
data: theme.listTileTheme,
child: ListTile(
onTap: () => onItemSelect(context, item),
selected: isSelected(item),
selected: selected,
title: _title(context, item, theme.tileTextStyle),
),
)
: ListTileTheme(
data: theme.listTileTheme,
child: CheckboxListTile(
value: isSelected(item),
selected: isSelected(item),
onChanged: (value) => onItemSelect(context, item),
value: selected,
selected: selected,
onChanged: maxSelectionReached
? null
: (value) => onItemSelect(context, item),
title: _title(context, item, theme.tileTextStyle),
),
),
Expand Down Expand Up @@ -308,6 +345,11 @@ One of the tileLabel or suggestionBuilder is required
if (selectedItems!.contains(item)) {
selectedItems!.remove(item);
} else {
// Add maximum selection length check
if (maximumSelectionLength != null &&
selectedItems!.length >= maximumSelectionLength!) {
return;
}
selectedItems!.add(item);
}
final qq = query;
Expand Down
16 changes: 14 additions & 2 deletions lib/src/filter_list_dialog.dart
Expand Up @@ -29,7 +29,6 @@ part 'filter_list_widget.dart';
///
///
/// The [onApplyButtonClick] is a callback which return list of all selected items on apply button click. if no item is selected then it will return empty list.
/// {@endtemplate}
/// The [useSafeArea] argument is used to indicate if the dialog should only display in 'safe' areas of the screen not used by the operating system (see [SafeArea] for more details). It is true by default, which means the dialog will not overlap operating system areas. If it is set to false the dialog will only be constrained by the screen size. It can not be null.
///
/// The [useRootNavigator] argument is used to determine whether to push the dialog to the [Navigator] furthest from or nearest to the given context. By default, useRootNavigator is true and the dialog route created by this method is pushed to the root navigator. It can not be null.
Expand All @@ -41,6 +40,13 @@ part 'filter_list_widget.dart';
///
/// Defaults to EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0).
///
/// The [controlButtons] is a list of [ControlButtonType] which is used to display control buttons on dialog.
///
/// The [enableOnlySingleSelection] is a boolean which is used to enable/disable single selection mode.
///
/// The [maximumSelectionLength] is a integer which is used to limit the maximum selection length.
/// {@endtemplate}
///
/// ### This example shows how to use [FilterListDialog]
///
/// ``` dart
Expand Down Expand Up @@ -76,7 +82,6 @@ part 'filter_list_widget.dart';
class FilterListDialog {
static Future display<T extends Object>(
BuildContext context, {

/// Filter theme
FilterListThemeData? themeData,

Expand Down Expand Up @@ -153,6 +158,12 @@ class FilterListDialog {
/// Default value is [false]
bool enableOnlySingleSelection = false,

/// if `maximumSelectionLength` is not null then it will limit the maximum selection length.
/// `maximumSelectionLength` should be greater than 0. If `maximumSelectionLength` is less than 0 then it will throw an exception.
/// Only works when `enableOnlySingleSelection` is false.
/// Default value is [null]
int? maximumSelectionLength,

/// Background color of dialog box.
Color backgroundColor = Colors.white,

Expand Down Expand Up @@ -225,6 +236,7 @@ class FilterListDialog {
resetButtonText: resetButtonText,
allButtonText: allButtonText,
validateRemoveItem: validateRemoveItem,
maximumSelectionLength: maximumSelectionLength,
controlButtons: controlButtons ??
[ControlButtonType.All, ControlButtonType.Reset],
),
Expand Down
9 changes: 9 additions & 0 deletions lib/src/filter_list_widget.dart
Expand Up @@ -72,6 +72,7 @@ class FilterListWidget<T extends Object> extends StatelessWidget {
this.hideHeader = false,
this.backgroundColor = Colors.white,
this.enableOnlySingleSelection = false,
this.maximumSelectionLength,
this.allButtonText = 'All',
this.applyButtonText = 'Apply',
this.resetButtonText = 'Reset',
Expand Down Expand Up @@ -120,6 +121,12 @@ class FilterListWidget<T extends Object> extends StatelessWidget {
/// Default value is `false`
final bool enableOnlySingleSelection;

/// if `maximumSelectionLength` is not null then it will limit the maximum selection length.
/// `maximumSelectionLength` should be greater than 0. If `maximumSelectionLength` is less than 0 then it will throw an exception.
/// Only works when `enableOnlySingleSelection` is false.
/// Default value is [null]
final int? maximumSelectionLength;

/// The `onApplyButtonClick` is a callback which return list of all selected items on apply button click. if no item is selected then it will return empty list.
final OnApplyButtonClick<T>? onApplyButtonClick;

Expand Down Expand Up @@ -206,6 +213,7 @@ class FilterListWidget<T extends Object> extends StatelessWidget {
enableOnlySingleSelection: enableOnlySingleSelection,
validateSelectedItem: validateSelectedItem,
validateRemoveItem: validateRemoveItem,
maximumSelectionLength: maximumSelectionLength,
),
),
],
Expand All @@ -214,6 +222,7 @@ class FilterListWidget<T extends Object> extends StatelessWidget {
// /// Bottom section for control buttons
ControlButtonBar<T>(
controlButtons: controlButtons,
maximumSelectionLength: maximumSelectionLength,
allButtonText: allButtonText,
applyButtonText: applyButtonText,
resetButtonText: resetButtonText,
Expand Down
6 changes: 5 additions & 1 deletion lib/src/state/filter_state.dart
Expand Up @@ -2,13 +2,17 @@ import 'package:filter_list/src/state/provider.dart';
import 'package:flutter/material.dart';

class FilterState<K> extends ListenableState {
FilterState({List<K>? allItems, List<K>? selectedItems}) {
FilterState(
{List<K>? allItems,
List<K>? selectedItems,
this.maximumSelectionLength}) {
this.selectedItems = selectedItems;
items = allItems;
}

static FilterState<T> of<T>(BuildContext context) =>
StateProvider.of<FilterState<T>>(context);
final int? maximumSelectionLength;

/// List of all items
List<K>? _items;
Expand Down
4 changes: 3 additions & 1 deletion lib/src/widget/choice_chip_widget.dart
Expand Up @@ -9,12 +9,14 @@ class ChoiceChipWidget<T> extends StatelessWidget {
this.selected,
this.onSelected,
this.choiceChipBuilder,
this.disabled = false,
}) : super(key: key);

final String? text;
final bool? selected;
final void Function(bool)? onSelected;
final T? item;
final bool disabled;

/// Builder for custom choice chip
final ChoiceChipBuilder? choiceChipBuilder;
Expand Down Expand Up @@ -66,7 +68,7 @@ class ChoiceChipWidget<T> extends StatelessWidget {
labelStyle: getSelectedTextStyle(context),
visualDensity: theme.visualDensity,
selected: selected!,
onSelected: onSelected,
onSelected: disabled ? null : onSelected,
elevation: theme.elevation,
side: getSide(context),
shape: getShape(context),
Expand Down
12 changes: 12 additions & 0 deletions lib/src/widget/choice_list.dart
Expand Up @@ -12,12 +12,14 @@ class ChoiceList<T> extends StatelessWidget {
this.choiceChipLabel,
this.enableOnlySingleSelection = false,
this.validateRemoveItem,
this.maximumSelectionLength,
}) : super(key: key);
final ValidateSelectedItem<T> validateSelectedItem;
final ChoiceChipBuilder? choiceChipBuilder;
final LabelDelegate<T>? choiceChipLabel;
final bool enableOnlySingleSelection;
final ValidateRemoveItem<T>? validateRemoveItem;
final int? maximumSelectionLength;

List<Widget> _buildChoiceList(BuildContext context) {
final theme = FilterListTheme.of(context).controlBarButtonTheme;
Expand All @@ -30,9 +32,15 @@ class ChoiceList<T> extends StatelessWidget {
final List<Widget> choices = [];
for (final item in items) {
final selected = validateSelectedItem(selectedListData, item);

// Check if maximum selection length reached
final maxSelectionReached = maximumSelectionLength != null &&
state.selectedItems != null &&
state.selectedItems!.length >= maximumSelectionLength!;
choices.add(
ChoiceChipWidget(
choiceChipBuilder: choiceChipBuilder,
disabled: maxSelectionReached,
item: item,
onSelected: (value) {
if (enableOnlySingleSelection) {
Expand All @@ -48,6 +56,10 @@ class ChoiceList<T> extends StatelessWidget {
state.removeSelectedItem(item);
}
} else {
// Add maximum selection length check
if (maxSelectionReached && !selected) {
return;
}
state.addSelectedItem(item);
}
}
Expand Down
9 changes: 8 additions & 1 deletion lib/src/widget/control_button_bar.dart
Expand Up @@ -5,6 +5,10 @@ import 'package:filter_list/src/state/provider.dart';
import 'package:filter_list/src/widget/control_button.dart';
import 'package:flutter/material.dart';

/// {@template control_buttons}
/// control buttons to show on bottom of dialog along with 'Apply' button.
/// Generally used to show 'All', 'Reset' and 'Apply' button.
/// {@endtemplate}
class ControlButtonBar<T> extends StatelessWidget {
const ControlButtonBar({
Key? key,
Expand All @@ -13,13 +17,15 @@ class ControlButtonBar<T> extends StatelessWidget {
this.resetButtonText,
this.applyButtonText,
this.onApplyButtonClick,
this.maximumSelectionLength,
required this.controlButtons,
}) : super(key: key);
final bool enableOnlySingleSelection;
final String? allButtonText;
final String? resetButtonText;
final String? applyButtonText;
final VoidCallback? onApplyButtonClick;
final int? maximumSelectionLength;

/// {@macro control_buttons}
final List<ControlButtonType> controlButtons;
Expand Down Expand Up @@ -48,7 +54,8 @@ class ControlButtonBar<T> extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: <Widget>[
/* All Button */
if (!enableOnlySingleSelection &&
if (maximumSelectionLength == null &&
!enableOnlySingleSelection &&
controlButtons.contains(ControlButtonType.All)) ...[
ControlButton(
choiceChipLabel: '$allButtonText',
Expand Down

0 comments on commit d3abab2

Please sign in to comment.