sunarya-thito / shadcn_flutter

Shadcn/UI ported to Flutter (Unofficial)
https://sunarya-thito.github.io/shadcn_flutter/
BSD 3-Clause "New" or "Revised" License
87 stars 14 forks source link

Select Widget Does Not Rebuild After Clearing Search Query #95

Open AmoonPod opened 1 week ago

AmoonPod commented 1 week ago

Description

There seems to be a state issue with the Select widget in the ShadCN package when handling search queries. Specifically, when a search query returns no results (i.e., no children are displayed), clearing the search query does not cause the Select widget to rebuild and display the children again. The children remain invisible, even though the query has been cleared from the input.

Steps to Reproduce

  1. Initialize the Select widget with a list of children.
  2. Enter a search query that does not match any of the available children.
  3. Notice that the children disappear, as expected.
  4. Clear the search query by deleting the input.

Issue

The children remain hidden, even though the search query is empty, and the children should be visible again.

Expected Behavior

After clearing the search query, the Select widget should rebuild and display the list of children again.

Actual Behavior

The children remain hidden even after clearing the search query. The widget does not rebuild to reflect the current state where no search query is applied.

Example Code

class DynamicFilter extends StatefulWidget {
  final Filter filter;
  final FilterReference filterReference;
  final int? index;

  const DynamicFilter({
    super.key,
    required this.filter,
    required this.filterReference,
    required this.index,
  });

  @override
  State<DynamicFilter> createState() => _DynamicFilterState();
}

class _DynamicFilterState extends State<DynamicFilter> {
  List<FilterValue> filteredValue = [];
  String? query;

  @override
  void initState() {
    super.initState();
    handleQueryUpdate('');
  }

  /// Handles search queries and fetches the corresponding filtered values.
  void handleQueryUpdate(String? searchQuery) {
    if (searchQuery != query) {
      query = searchQuery;
      _fetchFilteredValue(); // Fetch values based on the updated query
    }
  }

  @override
  Widget build(BuildContext context) {
    final filterBloc = context.read<FilterBloc>();
    final AppliedFilter? appliedFilter;
    if (widget.index != null) {
      appliedFilter = filterBloc.state.getFilter(widget.index!);
    } else {
      appliedFilter = null;
    }
    List<FilterValue> selectedValues =
        appliedFilter?.value as List<FilterValue>? ?? [];
    for (var value in selectedValues) {
      if (!filteredValue.contains(value)) {
        filteredValue.add(value);
      }
    }
    return Select<FilterValue>(
        borderRadius: getBorderRadius(BadgePosition.center),
        value: null,
        padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
        itemBuilder: (context, item) {
          return Text(item.label, style: const TextStyle(fontSize: 12));
        },
        searchFilter: (item, searchQuery) {
          debugPrint('searchFilter: $searchQuery');
          handleQueryUpdate(searchQuery);
          return 1;
        },
        popupConstraints: const BoxConstraints(
          maxHeight: 300,
          maxWidth: 240,
        ),
        autoClosePopover: false,
        // Keep popover open while searching
        placeholder: buildPlaceholder(selectedValues, widget.filter),
        onChanged: (selectedValue) {
          if (selectedValue == null) {
            return;
          }
          _toggleSelection(selectedValues, selectedValue, filterBloc);
        },
        emptyBuilder: (context) {
          return const SizedBox();
        },
        children: [
          SelectGroup(
              children: filteredValue.map((value) {
            CheckboxState state = selectedValues.contains(value)
                ? CheckboxState.checked
                : CheckboxState.unchecked;
            Widget? leading;
            if (value.graphic is FilterValueGraphic<FilterReferenceUtente>) {
              leading = Padding(
                padding: const EdgeInsets.symmetric(horizontal: 4),
                child: Avatar(
                  borderRadius: 20,
                  provider: NetworkImage(value.graphic!.value),
                  initials: value.label.split(' ').map((e) => e[0]).join(),
                  size: 20,
                ),
              );
            }
            return SelectItemButton(
              value: value,
              child: Row(
                children: [
                  Checkbox(
                      state: state,
                      onChanged: (boolValue) {
                        _toggleSelection(selectedValues, value, filterBloc);
                      }),
                  leading ?? const SizedBox(width: 2),
                  Expanded(
                    child: Text(value.label,
                        overflow: TextOverflow.ellipsis,
                        maxLines: 1,
                        style: const TextStyle(fontSize: 12)),
                  ),
                ],
              ),
            );
          }).toList()),
        ]);
  }

  /// Fetches filtered values based on the search query.
  Future<void> _fetchFilteredValue() async {
    var result = await DynamicFilterService.fetch(
        filterReference: widget.filterReference, search: query);
    setState(() {
      filteredValue = result;
    });
  }

  /// Toggles the selection of filter values and updates the FilterBloc.
  void _toggleSelection(List<FilterValue> selectedValues,
      FilterValue selectedValue, FilterBloc filterBloc) {
    List<FilterValue> newSelectedValue = List.from(selectedValues);
    if (selectedValues.contains(selectedValue)) {
      newSelectedValue.remove(selectedValue);
    } else {
      newSelectedValue.add(selectedValue);
    }

    final appliedFilter = AppliedFilter(
      filter: widget.filter,
      operatorType: widget.filter.supportedOperators.first,
      value: newSelectedValue,
    );
    filterBloc.add(AddOrUpdateFilterEvent(
      appliedFilter: appliedFilter,
      index: widget.index,
    ));
  }
}
ì

Additional Information

This issue affects the user experience when searching through the list of options. It prevents users from seeing the available options again after clearing the search query, forcing them to close and reopen the Select widget to refresh the list.

Workaround

Currently, the only workaround is to close and reopen the Select widget to refresh the list of children after the search query is cleared.

There seems to be a similar issue reported earlier, where the Select widget does not reflect real-time updates when children are dynamically added, but it's already been solved.

sunarya-thito commented 6 days ago

searchFilter is invoked when you type at least 1 character, otherwise, all values will be displayed (the searchFilter wont be called).

I see that you are handling your own search handling. searchFilter should not be used to detect whether the query has changed, it is used to compute items search/indexing score (the highest score will be displayed first), calling the fetch method inside searchFilter will cause the fetch method to be invoked many times depending on how many select value items you have.

Select does not have onSearch method atm, but I can add it.

AmoonPod commented 6 days ago

Yeah, it would be great to have a onSearch method. The thing is that in my code the searchFilter is not invoked anymore whenever I search for something that makes the children be empty.