diff --git a/CHANGELOG.md b/CHANGELOG.md index e82957b..68baa1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [0.4.9] - 2021.01.12 +* added an ability to override the clear and dropdown icon buttons with builder +* `suffixIcons` adds an ability to switch icon management through the `suffixIcon` of `InputDecoration` + ## [0.4.8] - 2020.11.20 * fix bug caused by last flutter SDK breaking changes [#69](https://github.com/salim-lachdhaf/searchable_dropdown/issues/69) * Add a getter for the selected item diff --git a/README.md b/README.md index a8a7644..c903882 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,9 @@ You can customize the layout of the DropdownSearch and its items. [EXAMPLE](http |`popupItemDisabled`|defines if an item of the popup is enabled or not, if the item is disabled, it cannot be clicked| |`popupBarrierColor`|set a custom color for the popup barrier| |`searchBoxController`|search box controller| +|`clearButtonBuilder`|custom clear button builder| +|`dropdownButtonBuilder`|custom dropdown button builder| +|`onBeforeChange`|callback executed before applying value change| |`searchDelay`|delay before searching| # Attention diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 7d30b69..f1159c3 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -9,11 +9,7 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -27,8 +23,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -39,13 +33,11 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -58,8 +50,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -69,9 +59,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( - 3B80C3931E831B6300D905FE /* App.framework */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, @@ -203,7 +191,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; @@ -255,7 +243,6 @@ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -331,7 +318,6 @@ }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -387,7 +373,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; diff --git a/example/lib/main.dart b/example/lib/main.dart index 81777c2..fce6d54 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -51,6 +51,92 @@ class _MyHomePageState extends State { onChanged: print, popupItemDisabled: (String s) => s.startsWith('I'), selectedItem: "Tunisia", + onBeforeChange: (a, b) { + AlertDialog alert = AlertDialog( + title: Text("Are you sure..."), + content: Text("...you want to clear the selection"), + actions: [ + FlatButton( + child: Text("OK"), + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + FlatButton( + child: Text("NOT OK"), + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + ], + ); + + return showDialog( + context: context, + builder: (BuildContext context) { + return alert; + }); + }, + ), + Divider(), + + ///Menu Mode with overriden icon and dropdown buttons + Row( + children: [ + Expanded( + flex: 2, + child: DropdownSearch( + validator: (v) => v == null ? "required field" : null, + hint: "Select a country", + mode: Mode.MENU, + dropdownSearchDecoration: InputDecoration( + filled: true, + border: UnderlineInputBorder( + borderSide: BorderSide(color: Color(0xFF01689A)), + ), + ), + showAsSuffixIcons: true, + clearButtonBuilder: (_) => Padding( + padding: const EdgeInsets.all(8.0), + child: const Icon( + Icons.clear, + size: 24, + color: Colors.black, + ), + ), + dropdownButtonBuilder: (_) => Padding( + padding: const EdgeInsets.all(8.0), + child: const Icon( + Icons.arrow_drop_down, + size: 24, + color: Colors.black, + ), + ), + showSelectedItem: true, + items: [ + "Brazil", + "Italia (Disabled)", + "Tunisia", + 'Canada' + ], + label: "Menu mode *", + showClearButton: true, + onChanged: print, + popupItemDisabled: (String s) => s.startsWith('I'), + selectedItem: "Tunisia", + ), + ), + Expanded( + child: TextField( + decoration: InputDecoration( + filled: true, + labelText: "Menu mode *", + border: UnderlineInputBorder( + borderSide: BorderSide(color: Color(0xFF01689A)), + ), + ), + )) + ], ), Divider(), DropdownSearch( @@ -180,12 +266,14 @@ class _MyHomePageState extends State { .changeSelectedItem("No"); }, child: Text("set to 'NO'")), - RaisedButton( - onPressed: () { - _openDropDownProgKey.currentState - .changeSelectedItem("Yes"); - }, - child: Text("set to 'YES'")), + Material( + child: RaisedButton( + onPressed: () { + _openDropDownProgKey.currentState + .changeSelectedItem("Yes"); + }, + child: Text("set to 'YES'")), + ), RaisedButton( onPressed: () { _openDropDownProgKey.currentState diff --git a/lib/dropdown_search.dart b/lib/dropdown_search.dart index f272f5a..ae72744 100644 --- a/lib/dropdown_search.dart +++ b/lib/dropdown_search.dart @@ -25,6 +25,8 @@ typedef Widget ErrorBuilder( BuildContext context, String searchEntry, dynamic exception); typedef Widget EmptyBuilder(BuildContext context, String searchEntry); typedef Widget LoadingBuilder(BuildContext context, String searchEntry); +typedef Widget IconButtonBuilder(BuildContext context); +typedef Future BeforeChange(T prevItem, T nextItem); enum Mode { DIALOG, BOTTOM_SHEET, MENU } @@ -125,9 +127,18 @@ class DropdownSearch extends StatefulWidget { ///custom dropdown clear button icon widget final Widget clearButton; + ///custom clear button widget builder + final IconButtonBuilder clearButtonBuilder; + ///custom dropdown icon button widget final Widget dropDownButton; + ///custom dropdown button widget builder + final IconButtonBuilder dropdownButtonBuilder; + + ///whether to manage the clear and dropdown icons via InputDecoration suffixIcon + final bool showAsSuffixIcons; + ///If true, the dropdownBuilder will continue the uses of material behavior ///This will be useful if you want to handle a custom UI only if the item !=null final bool dropdownBuilderSupportsNullItem; @@ -145,10 +156,14 @@ class DropdownSearch extends StatefulWidget { ///called when popup is dismissed final VoidCallback onPopupDismissed; + /// callback executed before applying value change ///delay before searching, change it to Duration(milliseconds: 0) ///if you do not use online search final Duration searchDelay; + /// callback executed before applying value change + final BeforeChange onBeforeChange; + DropdownSearch({ Key key, this.onSaved, @@ -182,7 +197,10 @@ class DropdownSearch extends StatefulWidget { this.autoFocusSearchBox = false, this.dialogMaxWidth, this.clearButton, + this.clearButtonBuilder, this.dropDownButton, + this.dropdownButtonBuilder, + this.showAsSuffixIcons = false, this.dropdownBuilderSupportsNullItem = false, this.popupShape, this.popupItemDisabled, @@ -190,6 +208,7 @@ class DropdownSearch extends StatefulWidget { this.onPopupDismissed, this.searchBoxController, this.searchDelay, + this.onBeforeChange, }) : assert(isFilteredOnline != null), assert(dropdownBuilderSupportsNullItem != null), assert(enabled != null), @@ -244,7 +263,7 @@ class DropdownSearchState extends State> { : Text(_selectedItemAsString(data), style: Theme.of(context).textTheme.subtitle1), ), - _manageTrailingIcons(data), + if (!widget.showAsSuffixIcons) _manageTrailingIcons(data), ], ); } @@ -270,7 +289,7 @@ class DropdownSearchState extends State> { (widget.dropdownBuilder == null || widget.dropdownBuilderSupportsNullItem), isFocused: isFocused, - decoration: _manageDropdownDecoration(state), + decoration: _manageDropdownDecoration(state, value), child: _defaultSelectItemWidget(value), ); }); @@ -279,7 +298,7 @@ class DropdownSearchState extends State> { } ///manage dropdownSearch field decoration - InputDecoration _manageDropdownDecoration(FormFieldState state) { + InputDecoration _manageDropdownDecoration(FormFieldState state, T data) { return (widget.dropdownSearchDecoration ?? InputDecoration( contentPadding: EdgeInsets.fromLTRB(12, 12, 0, 0), @@ -289,6 +308,8 @@ class DropdownSearchState extends State> { enabled: widget.enabled, labelText: widget.label, hintText: widget.hint, + suffixIcon: + widget.showAsSuffixIcons ? _manageTrailingIcons(data) : null, errorText: state.errorText); } @@ -305,20 +326,33 @@ class DropdownSearchState extends State> { ///function that manage Trailing icons(close, dropDown) Widget _manageTrailingIcons(T data) { + final clearButtonPressed = () => _handleOnChangeSelectedItem(null); + final dropdownButtonPressed = () => _selectSearchMode(data); + return Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.end, children: [ if (data != null && widget.showClearButton) - IconButton( - icon: widget.clearButton ?? const Icon(Icons.clear, size: 24), - onPressed: () => _handleOnChangeSelectedItem(null), - ), - IconButton( - icon: widget.dropDownButton ?? - const Icon(Icons.arrow_drop_down, size: 24), - onPressed: () => _selectSearchMode(data), - ), + widget.clearButtonBuilder != null + ? GestureDetector( + onTap: clearButtonPressed, + child: widget.clearButtonBuilder(context), + ) + : IconButton( + icon: widget.clearButton ?? const Icon(Icons.clear, size: 24), + onPressed: clearButtonPressed, + ), + widget.dropdownButtonBuilder != null + ? GestureDetector( + onTap: dropdownButtonPressed, + child: widget.dropdownButtonBuilder(context), + ) + : IconButton( + icon: widget.dropDownButton ?? + const Icon(Icons.arrow_drop_down, size: 24), + onPressed: dropdownButtonPressed, + ), ], ); } @@ -437,8 +471,23 @@ class DropdownSearchState extends State> { ///handle on change value , if the validation is active , we validate the new selected item void _handleOnChangeSelectedItem(T selectedItem) { - _selectedItemNotifier.value = selectedItem; - if (widget.onChanged != null) widget.onChanged(selectedItem); + final changeItem = () { + _selectedItemNotifier.value = selectedItem; + if (widget.onChanged != null) widget.onChanged(selectedItem); + }; + + if (widget.onBeforeChange != null) { + widget + .onBeforeChange(_selectedItemNotifier.value, selectedItem) + .then((value) { + if (value == true) { + changeItem(); + } + }); + } else { + changeItem(); + } + _handleFocus(false); } @@ -476,6 +525,10 @@ class DropdownSearchState extends State> { void changeSelectedItem(T selectedItem) => _handleOnChangeSelectedItem(selectedItem); + ///Change selected Value; this function is public USED to clear selected + ///value PROGRAMMATICALLY, Otherwise you can use [_handleOnChangeSelectedItem] + void clear() => _handleOnChangeSelectedItem(null); + ///get selected value programmatically T get getSelectedItem => _selectedItemNotifier.value; diff --git a/pubspec.yaml b/pubspec.yaml index c09dd3a..6a8b238 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: dropdown_search description: Simple and robust Searchable Dropdown with item search feature, making it possible to use an offline item list or filtering URL for easy customization. -version: 0.4.8 +version: 0.4.9 homepage: https://github.com/salim-lachdhaf git: https://github.com/salim-lachdhaf/searchable_dropdown repository: https://github.com/salim-lachdhaf/searchable_dropdown