diff --git a/example/lib/main.dart b/example/lib/main.dart index 34bff41..620c1ca 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:searchable_listview/resources/arrays.dart'; import 'package:searchable_listview/searchable_listview.dart'; void main() { @@ -63,6 +64,7 @@ class _ExampleAppState extends State { }; final TextEditingController searchTextController = TextEditingController(); + final formKey = GlobalKey(); @override void initState() { @@ -80,14 +82,32 @@ class _ExampleAppState extends State { Expanded( child: Padding( padding: const EdgeInsets.all(15), - child: renderAsynchSearchableListview(), + child: renderSimpleSearchableList(), ), ), Align( alignment: Alignment.center, - child: ElevatedButton( - onPressed: addActor, - child: const Text('Add actor'), + child: Row( + children: [ + ElevatedButton( + onPressed: addActor, + child: const Text('Add actor'), + ), + ElevatedButton( + onPressed: () { + if (formKey.currentState?.validate() ?? false) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Field is valid')), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Field is not valid')), + ); + } + }, + child: const Text('Validate field'), + ), + ], ), ) ], @@ -140,11 +160,7 @@ class _ExampleAppState extends State { ], ), filter: (query) { - filteredActors = actors - .where((element) => - element.name.toLowerCase().contains(query.toLowerCase()) || - element.lastName.toLowerCase().contains(query.toLowerCase())) - .toList(); + filteredActors = actors.where((element) => element.name.toLowerCase().contains(query.toLowerCase()) || element.lastName.toLowerCase().contains(query.toLowerCase())).toList(); return filteredActors; }, initialList: actors, @@ -160,6 +176,7 @@ class _ExampleAppState extends State { itemBuilder: (item) { return ActorItem(actor: item); }, + searchMode: SearchMode.onSubmit, errorWidget: const Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -222,11 +239,7 @@ class _ExampleAppState extends State { return actors; }, asyncListFilter: (query, list) { - var result = actors - .where((element) => - element.name.contains(query) || - element.lastName.contains(query)) - .toList(); + var result = actors.where((element) => element.name.contains(query) || element.lastName.contains(query)).toList(); return result; }, separatorBuilder: (context, index) { @@ -264,12 +277,7 @@ class _ExampleAppState extends State { ); }, filterExpansionData: (p0) { - final filteredMap = { - for (final entry in mapOfActors.entries) - entry.key: (mapOfActors[entry.key] ?? []) - .where((element) => element.name.contains(p0)) - .toList() - }; + final filteredMap = {for (final entry in mapOfActors.entries) entry.key: (mapOfActors[entry.key] ?? []).where((element) => element.name.contains(p0)).toList()}; return filteredMap; }, textStyle: const TextStyle(fontSize: 25), diff --git a/lib/searchable_listview.dart b/lib/searchable_listview.dart index 7989eba..b232ca8 100644 --- a/lib/searchable_listview.dart +++ b/lib/searchable_listview.dart @@ -66,6 +66,8 @@ class SearchableList extends StatefulWidget { this.textAlignVertical = TextAlignVertical.center, this.labelText, this.showSearchField = true, + this.fieldValidator, + this.formFieldKey, }) : super(key: key) { searchTextController ??= TextEditingController(); expansionListBuilder = null; @@ -126,6 +128,8 @@ class SearchableList extends StatefulWidget { this.textAlignVertical = TextAlignVertical.center, this.labelText, this.showSearchField = true, + this.fieldValidator, + this.formFieldKey, }) : super(key: key) { assert(asyncListCallback != null); searchTextController ??= TextEditingController(); @@ -184,6 +188,8 @@ class SearchableList extends StatefulWidget { this.textAlignVertical = TextAlignVertical.center, this.labelText, this.showSearchField = true, + this.fieldValidator, + this.formFieldKey, }) : super(key: key) { searchTextController ??= TextEditingController(); separatorBuilder = null; @@ -237,6 +243,8 @@ class SearchableList extends StatefulWidget { this.textAlignVertical = TextAlignVertical.center, this.labelText, this.showSearchField = true, + this.fieldValidator, + this.formFieldKey, }) : super(key: key) { asyncListCallback = null; asyncListFilter = null; @@ -288,8 +296,7 @@ class SearchableList extends StatefulWidget { /// [expansionGroupIndex] : expansion group index /// [listItem] the current item model that will be rendered. /// Used only for expansion list constructor - late Widget Function(int expansionGroupIndex, T listItem)? - expansionListBuilder; + late Widget Function(int expansionGroupIndex, T listItem)? expansionListBuilder; /// The widget to be displayed when the filter returns an empty list. /// Defaults to `const SizedBox.shrink()`. @@ -478,6 +485,10 @@ class SearchableList extends StatefulWidget { /// by default `showSearchField = true` final bool showSearchField; + final FormFieldValidator? fieldValidator; + + final Key? formFieldKey; + bool isExpansionList = false; @override @@ -487,8 +498,7 @@ class SearchableList extends StatefulWidget { class _SearchableListState extends State> { /// Create scroll controller instance /// attached to the listview widget - late ScrollController scrollController = - widget.scrollController ?? ScrollController(); + late ScrollController scrollController = widget.scrollController ?? ScrollController(); List asyncListResult = []; late List filtredListResult = widget.initialList; List filtredAsyncListResult = []; @@ -500,19 +510,18 @@ class _SearchableListState extends State> { void initState() { super.initState(); scrollController.addListener(() { - if (widget.closeKeyboardWhenScrolling && - widget.focusNode?.hasFocus == true) { + if (widget.closeKeyboardWhenScrolling && widget.focusNode?.hasFocus == true) { FocusScope.of(context).requestFocus(FocusNode()); } - if (widget.onPaginate != null && - scrollController.position.pixels == - scrollController.position.maxScrollExtent) { + if (widget.onPaginate != null && scrollController.position.pixels == scrollController.position.maxScrollExtent) { setState(() { widget.onPaginate?.call(); }); } }); - widget.searchTextController?.addListener(_textControllerListener); + if (widget.searchMode == SearchMode.onEdit) { + widget.searchTextController?.addListener(_textControllerListener); + } } @override @@ -538,8 +547,7 @@ class _SearchableListState extends State> { filterList(searchText); } } - if (widget.isExpansionList && - oldWidget.expansionListData != widget.expansionListData) { + if (widget.isExpansionList && oldWidget.expansionListData != widget.expansionListData) { filterList(searchText); } } @@ -549,10 +557,7 @@ class _SearchableListState extends State> { return widget.sliverScrollEffect ? renderSliverEffect() : Column( - verticalDirection: - widget.searchTextPosition == SearchTextPosition.top - ? VerticalDirection.down - : VerticalDirection.up, + verticalDirection: widget.searchTextPosition == SearchTextPosition.top ? VerticalDirection.down : VerticalDirection.up, crossAxisAlignment: CrossAxisAlignment.start, children: [ if (widget.showSearchField) @@ -561,41 +566,11 @@ class _SearchableListState extends State> { child: SizedBox( width: widget.searchFieldWidth, height: widget.searchFieldHeight, - child: SearchTextField( - filterList: filterList, - focusNode: widget.focusNode, - inputDecoration: widget.inputDecoration, - keyboardAction: widget.keyboardAction, - obscureText: widget.obscureText, - onSubmitSearch: widget.onSubmitSearch, - searchFieldEnabled: widget.searchFieldEnabled, - searchMode: widget.searchMode, - searchTextController: widget.searchTextController, - textInputType: widget.textInputType, - displayClearIcon: widget.displayClearIcon, - displaySearchIcon: widget.displaySearchIcon, - defaultSuffixIconColor: widget.defaultSuffixIconColor, - defaultSuffixIconSize: widget.defaultSuffixIconSize, - textStyle: widget.textStyle, - cursorColor: widget.cursorColor, - maxLength: widget.maxLength, - maxLines: widget.maxLines, - textAlign: widget.textAlign, - autoCompleteHints: widget.autoCompleteHints, - secondaryWidget: widget.secondaryWidget, - onSortTap: sortList, - sortWidget: widget.sortWidget, - verticalTextAlign: widget.textAlignVertical, - labelText: widget.labelText, - ), + child: _buildSearchTextField(), ), ), Expanded( - child: widget.isExpansionList - ? renderExpandableListView() - : (widget.asyncListCallback != null && !dataDownloaded - ? renderAsyncListView() - : renderSearchableListView()), + child: widget.isExpansionList ? renderExpandableListView() : (widget.asyncListCallback != null && !dataDownloaded ? renderAsyncListView() : renderSearchableListView()), ), ], ); @@ -620,9 +595,7 @@ class _SearchableListState extends State> { } Widget renderSearchableListView() { - List renderedList = widget.asyncListCallback != null - ? filtredAsyncListResult - : filtredListResult; + List renderedList = widget.asyncListCallback != null ? filtredAsyncListResult : filtredListResult; return buildSearchableListView( list: renderedList, ); @@ -676,8 +649,7 @@ class _SearchableListState extends State> { } Widget renderExpandableListView() { - if (widget.expansionListData.isEmpty || - widget.expansionListData.values.every((element) => element.isEmpty)) { + if (widget.expansionListData.isEmpty || widget.expansionListData.values.every((element) => element.isEmpty)) { return widget.emptyWidget ?? const SizedBox.shrink(); } else { expansionTileControllers.addAll( @@ -726,43 +698,14 @@ class _SearchableListState extends State> { return widget.scrollDirection == Axis.horizontal ? Column( crossAxisAlignment: CrossAxisAlignment.start, - verticalDirection: - widget.searchTextPosition == SearchTextPosition.top - ? VerticalDirection.down - : VerticalDirection.up, + verticalDirection: widget.searchTextPosition == SearchTextPosition.top ? VerticalDirection.down : VerticalDirection.up, children: [ if (widget.showSearchField) Padding( padding: widget.searchFieldPadding ?? const EdgeInsets.all(0), child: SizedBox( width: widget.searchFieldWidth, - child: SearchTextField( - filterList: filterList, - focusNode: widget.focusNode, - inputDecoration: widget.inputDecoration, - keyboardAction: widget.keyboardAction, - obscureText: widget.obscureText, - onSubmitSearch: widget.onSubmitSearch, - searchFieldEnabled: widget.searchFieldEnabled, - searchMode: widget.searchMode, - searchTextController: widget.searchTextController, - textInputType: widget.textInputType, - displayClearIcon: widget.displayClearIcon, - displaySearchIcon: widget.displaySearchIcon, - defaultSuffixIconColor: widget.defaultSuffixIconColor, - defaultSuffixIconSize: widget.defaultSuffixIconSize, - textStyle: widget.textStyle, - cursorColor: widget.cursorColor, - maxLength: widget.maxLength, - maxLines: widget.maxLines, - textAlign: widget.textAlign, - autoCompleteHints: widget.autoCompleteHints, - secondaryWidget: widget.secondaryWidget, - onSortTap: sortList, - sortWidget: widget.sortWidget, - verticalTextAlign: widget.textAlignVertical, - labelText: widget.labelText, - ), + child: _buildSearchTextField(), ), ), Expanded( @@ -785,33 +728,7 @@ class _SearchableListState extends State> { if (widget.showSearchField) SliverAppBar( backgroundColor: Colors.transparent, - flexibleSpace: SearchTextField( - filterList: filterList, - focusNode: widget.focusNode, - inputDecoration: widget.inputDecoration, - keyboardAction: widget.keyboardAction, - obscureText: widget.obscureText, - onSubmitSearch: widget.onSubmitSearch, - searchFieldEnabled: widget.searchFieldEnabled, - searchMode: widget.searchMode, - searchTextController: widget.searchTextController, - textInputType: widget.textInputType, - displayClearIcon: widget.displayClearIcon, - displaySearchIcon: widget.displaySearchIcon, - defaultSuffixIconColor: widget.defaultSuffixIconColor, - defaultSuffixIconSize: widget.defaultSuffixIconSize, - textStyle: widget.textStyle, - cursorColor: widget.cursorColor, - maxLength: widget.maxLength, - maxLines: widget.maxLines, - textAlign: widget.textAlign, - autoCompleteHints: widget.autoCompleteHints, - secondaryWidget: widget.secondaryWidget, - onSortTap: sortList, - sortWidget: widget.sortWidget, - verticalTextAlign: widget.textAlignVertical, - labelText: widget.labelText, - ), + flexibleSpace: _buildSearchTextField(), ), renderSliverListView(), ], @@ -838,8 +755,7 @@ class _SearchableListState extends State> { searchText = value; if (widget.isExpansionList) { setState(() { - widget.expansionListData = - widget.filterExpansionData?.call(value) ?? {}; + widget.expansionListData = widget.filterExpansionData?.call(value) ?? {}; }); for (var controller in expansionTileControllers) { try { @@ -880,4 +796,37 @@ class _SearchableListState extends State> { filterList(widget.searchTextController?.text ?? ''); } } + + /// Builds and returns a SearchTextField widget with all necessary parameters + Widget _buildSearchTextField() { + return SearchTextField( + filterList: filterList, + focusNode: widget.focusNode, + inputDecoration: widget.inputDecoration, + keyboardAction: widget.keyboardAction, + obscureText: widget.obscureText, + onSubmitSearch: widget.onSubmitSearch, + searchFieldEnabled: widget.searchFieldEnabled, + searchMode: widget.searchMode, + searchTextController: widget.searchTextController, + textInputType: widget.textInputType, + displayClearIcon: widget.displayClearIcon, + displaySearchIcon: widget.displaySearchIcon, + defaultSuffixIconColor: widget.defaultSuffixIconColor, + defaultSuffixIconSize: widget.defaultSuffixIconSize, + textStyle: widget.textStyle, + cursorColor: widget.cursorColor, + maxLength: widget.maxLength, + maxLines: widget.maxLines, + textAlign: widget.textAlign, + autoCompleteHints: widget.autoCompleteHints, + secondaryWidget: widget.secondaryWidget, + onSortTap: sortList, + sortWidget: widget.sortWidget, + verticalTextAlign: widget.textAlignVertical, + labelText: widget.labelText, + validator: widget.fieldValidator, + formFieldKey: widget.formFieldKey, + ); + } } diff --git a/lib/widgets/search_text_field.dart b/lib/widgets/search_text_field.dart index 9f98910..3b90d24 100644 --- a/lib/widgets/search_text_field.dart +++ b/lib/widgets/search_text_field.dart @@ -2,32 +2,85 @@ import 'package:flutter/material.dart'; import 'package:searchable_listview/resources/arrays.dart'; class SearchTextField extends StatelessWidget { + /// Optional focus node to manage keyboard focus for the search text field final FocusNode? focusNode; + + /// Whether the search text field is enabled for user interaction final bool searchFieldEnabled; + + /// Optional custom input decoration for styling the search text field final InputDecoration? inputDecoration; + + /// Optional text editing controller to programmatically manage the search input final TextEditingController? searchTextController; + + /// The keyboard action button type (e.g., done, search, next) final TextInputAction keyboardAction; + + /// The type of keyboard to display (e.g., text, number, email) final TextInputType textInputType; + + /// Whether to hide the text input (used for password fields) final bool obscureText; + + /// Determines when filtering occurs: on text edit or on submit final SearchMode searchMode; + + /// Callback function that receives the search query to filter the list final Function(String) filterList; + + /// Optional callback function invoked when the user submits the search final Function(String)? onSubmitSearch; + + /// Whether to display a clear icon in the search field suffix final bool displayClearIcon; + + /// Whether to display a search icon in the search field suffix final bool displaySearchIcon; + + /// The color of the suffix icons (clear and search icons) final Color defaultSuffixIconColor; + + /// The size of the suffix icons final double defaultSuffixIconSize; + + /// Optional custom text style for the search input text final TextStyle? textStyle; + + /// Optional custom color for the text cursor final Color? cursorColor; + + /// Maximum number of lines for the search text field final int? maxLines; + + /// Maximum character length allowed in the search field final int? maxLength; + + /// The horizontal alignment of the text within the search field final TextAlign textAlign; + + /// The vertical alignment of the text within the search field final TextAlignVertical verticalTextAlign; + + /// List of autocomplete hints to suggest to the user while typing final List autoCompleteHints; + + /// Optional widget to display next to the search field (e.g., filter button) final Widget? secondaryWidget; + + /// Optional callback function invoked when the sort button is tapped final Function()? onSortTap; + + /// Optional custom widget for the sort button final Widget? sortWidget; + + /// Optional label text displayed when the search field is empty final String? labelText; + final FormFieldValidator? validator; + + final Key? formFieldKey; + const SearchTextField({ Key? key, required this.filterList, @@ -55,6 +108,8 @@ class SearchTextField extends StatelessWidget { this.secondaryWidget, this.sortWidget, this.labelText, + this.validator, + this.formFieldKey, }) : super(key: key); @override @@ -62,85 +117,93 @@ class SearchTextField extends StatelessWidget { return Row( children: [ Expanded( - child: autoCompleteHints.isNotEmpty - ? Autocomplete( - optionsBuilder: (textEditingValue) { - return autoCompleteHints; - }, - onSelected: (option) { - filterList(option.toString()); - FocusScope.of(context).requestFocus(FocusNode()); - }, - fieldViewBuilder: ( - context, - textController, - focusNode, - onFieldSubmitted, - ) { - return TextField( - textAlignVertical: verticalTextAlign, - cursorColor: cursorColor, - maxLength: maxLength, - maxLines: maxLines, - textAlign: textAlign, - focusNode: focusNode, - enabled: searchFieldEnabled, - decoration: inputDecoration?.copyWith( - suffix: InkWell( - onTap: () { - textController.text = ''; - filterList(textController.text); - FocusScope.of(context).requestFocus(FocusNode()); - }, - child: const Icon(Icons.close), - ), - ), - style: textStyle, - controller: textController, - onChanged: (value) { - filterList(value); - }, - ); - }, - ) - : TextField( - textAlignVertical: verticalTextAlign, - cursorColor: cursorColor, - maxLength: maxLength, - maxLines: maxLines, - textAlign: textAlign, - focusNode: focusNode, - enabled: searchFieldEnabled, - decoration: inputDecoration ?? - InputDecoration( - labelText: labelText, - suffixIcon: renderSuffixWidget(context), - ), - style: textStyle, - controller: searchTextController, - textInputAction: keyboardAction, - keyboardType: textInputType, - obscureText: obscureText, - onSubmitted: (value) { - onSubmitSearch?.call(value); - if (searchMode == SearchMode.onSubmit) { - filterList(value); - } - }, - onChanged: (value) { - if (searchMode == SearchMode.onEdit) { - filterList(value); - } - }, - ), + child: _renderForm(context), ), if (secondaryWidget != null) secondaryWidget!, ], ); } - Widget renderSuffixWidget(BuildContext context) { - var clearIcon = renderClearIcon(); + /// Renders the search text field wrapped in a Form widget if a formFieldKey is provided, otherwise renders just the text field + Widget _renderForm(BuildContext context) { + if (formFieldKey != null) { + return Form( + key: formFieldKey, + child: _renderFieldWidget(context), + ); + } else { + return _renderFieldWidget(context); + } + } + + /// Renders either an Autocomplete widget or a standard TextFormField based on whether autocomplete hints are provided + Widget _renderFieldWidget(BuildContext context) { + if (autoCompleteHints.isNotEmpty) { + return _autoCompleteFieldBuilder(context); + } else { + return _renderTextField(context); + } + } + + /// Renders an Autocomplete widget with the provided hints and behavior for selecting options and submitting the search query + Widget _autoCompleteFieldBuilder(BuildContext context) { + return Autocomplete( + optionsBuilder: (textEditingValue) { + return autoCompleteHints; + }, + onSelected: (option) { + filterList(option.toString()); + FocusScope.of(context).requestFocus(FocusNode()); + }, + fieldViewBuilder: ( + context, + textController, + focusNode, + onFieldSubmitted, + ) { + return _renderFieldWidget(context); + }, + ); + } + + /// Renders the main search text field with appropriate styling, behavior, and suffix icons based on the provided parameters + Widget _renderTextField(BuildContext context) { + return TextFormField( + textAlignVertical: verticalTextAlign, + cursorColor: cursorColor, + maxLength: maxLength, + maxLines: maxLines, + textAlign: textAlign, + focusNode: focusNode, + enabled: searchFieldEnabled, + decoration: inputDecoration ?? + InputDecoration( + labelText: labelText, + suffixIcon: _renderSuffixWidget(context), + ), + style: textStyle, + controller: searchTextController, + textInputAction: keyboardAction, + keyboardType: textInputType, + obscureText: obscureText, + onFieldSubmitted: (value) { + onSubmitSearch?.call(value); + if (searchMode == SearchMode.onSubmit) { + filterList(value); + } + }, + validator: validator, + onChanged: (value) { + if (searchMode == SearchMode.onEdit) { + filterList(value); + } + }, + ); + } + + /// Renders the suffix widget for the search text field, including clear and sort icons if applicable + Widget _renderSuffixWidget(BuildContext context) { + var clearIcon = _renderClearIcon(); return Padding( padding: const EdgeInsets.symmetric( horizontal: 5, @@ -171,7 +234,7 @@ class SearchTextField extends StatelessWidget { ); } - Widget? renderClearIcon() { + Widget? _renderClearIcon() { if (searchTextController!.text.isNotEmpty) { return InkWell( onTap: () { diff --git a/test/basic_searchable_listview_test.dart b/test/basic_searchable_listview_test.dart index 8eab805..aff87e6 100644 --- a/test/basic_searchable_listview_test.dart +++ b/test/basic_searchable_listview_test.dart @@ -14,8 +14,7 @@ void main() { var basicSearchableList = SearchableList( initialList: intList, itemBuilder: (item) => DummyListItem(index: item), - filter: (query) => - intList.where((item) => item.toString().contains(query)).toList(), + filter: (query) => intList.where((item) => item.toString().contains(query)).toList(), ); await test.pumpWidget( @@ -52,8 +51,7 @@ void main() { border: OutlineInputBorder(), ), itemBuilder: (item) => DummyListItem(index: item), - filter: (query) => - intList.where((item) => item.toString().contains(query)).toList(), + filter: (query) => intList.where((item) => item.toString().contains(query)).toList(), ); await test.pumpWidget( @@ -71,8 +69,7 @@ void main() { expect(find.text('Search Items'), findsOneWidget); final searchField = find.byType(TextField); expect( - test.widget(searchField).decoration!.border - is OutlineInputBorder, + test.widget(searchField).decoration!.border is OutlineInputBorder, isTrue, ); }); @@ -86,8 +83,7 @@ void main() { initialList: [], emptyWidget: const Text('No items found'), itemBuilder: (item) => DummyListItem(index: item), - filter: (query) => - intList.where((item) => item.toString().contains(query)).toList(), + filter: (query) => intList.where((item) => item.toString().contains(query)).toList(), ); await test.pumpWidget( @@ -116,8 +112,7 @@ void main() { var basicSearchableList = SearchableList( initialList: intList, itemBuilder: (item) => DummyListItem(index: item), - filter: (query) => - intList.where((item) => item.toString().contains(query)).toList(), + filter: (query) => intList.where((item) => item.toString().contains(query)).toList(), ); await test.pumpWidget( @@ -159,8 +154,7 @@ void main() { var basicSearchableList = SearchableList( initialList: intList, itemBuilder: (item) => DummyListItem(index: item), - filter: (query) => - intList.where((item) => item.toString().contains(query)).toList(), + filter: (query) => intList.where((item) => item.toString().contains(query)).toList(), ); await test.pumpWidget( @@ -191,8 +185,7 @@ void main() { var basicSearchableList = SearchableList( initialList: [], itemBuilder: (item) => DummyListItem(index: item), - filter: (query) => - intList.where((item) => item.toString().contains(query)).toList(), + filter: (query) => intList.where((item) => item.toString().contains(query)).toList(), ); await test.pumpWidget( @@ -223,9 +216,7 @@ void main() { var basicSearchableList = SearchableList( initialList: stringList, itemBuilder: (item) => Text(item), - filter: (query) => stringList - .where((item) => item.toString().contains(query)) - .toList(), + filter: (query) => stringList.where((item) => item.toString().contains(query)).toList(), ); await test.pumpWidget( @@ -270,8 +261,7 @@ void main() { initialList: intList, displayClearIcon: true, itemBuilder: (item) => DummyListItem(index: item), - filter: (query) => - intList.where((item) => item.toString().contains(query)).toList(), + filter: (query) => intList.where((item) => item.toString().contains(query)).toList(), ); await test.pumpWidget( @@ -298,8 +288,7 @@ void main() { initialList: intList, displayClearIcon: true, itemBuilder: (item) => DummyListItem(index: item), - filter: (query) => - intList.where((item) => item.toString().contains(query)).toList(), + filter: (query) => intList.where((item) => item.toString().contains(query)).toList(), ); await test.pumpWidget( @@ -322,4 +311,78 @@ void main() { expect(find.byType(DummyListItem), findsNWidgets(10)); }); }); + + group( + """" + Verifying form validation and form key behavior +""", + () { + testWidgets("", (tester) async { + final intList = List.generate(3, (index) => index); + final formKey = GlobalKey(); + var basicSearchableList = SearchableList( + initialList: intList, + displayClearIcon: true, + itemBuilder: (item) => DummyListItem(index: item), + filter: (query) => intList.where((item) => item.toString().contains(query)).toList(), + fieldValidator: (value) { + if (value == null || value.isEmpty) { + return 'Search field cannot be empty'; + } + return null; + }, + formFieldKey: formKey, + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Column( + children: [ + Expanded(child: basicSearchableList), + ElevatedButton( + onPressed: () { + if (formKey.currentState!.validate()) { + // If the form is valid, the valid elements will show automatically. + } else { + ScaffoldMessenger.of(tester.element(find.byType(ElevatedButton))).showSnackBar( + const SnackBar( + content: Text('Please fix the errors in the search field'), + duration: Duration(seconds: 1), + ), + ); + } + }, + child: Text('Submit'), + ), + ], + ), + ), + ), + ); + + final submitButton = find.byType(ElevatedButton); + + expect(find.byType(TextField), findsOneWidget); + + // Try submitting empty search query + await tester.enterText(find.byType(TextField), ''); + await tester.tap(submitButton); + await tester.pumpAndSettle(); + + // Check for validation error message + expect(find.text('Please fix the errors in the search field'), findsOneWidget); + + await tester.pumpAndSettle(const Duration(seconds: 2)); + // Enter valid search query + await tester.enterText(find.byType(TextField), '1'); + await tester.tap(submitButton); + await tester.pumpAndSettle(); + + // Check that validation error message is gone and results are shown + expect(find.text('Please fix the errors in the search field'), findsNothing); + expect(find.text('Item 1'), findsOneWidget); + }); + }, + ); }