Skip to content

Commit

Permalink
Merge pull request #93 from artflutter/feature/icon-dropdown-buttons-…
Browse files Browse the repository at this point in the history
…builder

clear and dropdown icon builders, add onBefore change
  • Loading branch information
salim-lachdhaf committed Jan 18, 2021
2 parents c3b8363 + 7f7eee6 commit 70fa719
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 37 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 1 addition & 16 deletions example/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand All @@ -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;
Expand All @@ -39,13 +33,11 @@
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = "<group>"; };
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 = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
Expand All @@ -58,8 +50,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */,
3B80C3941E831B6300D905FE /* App.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -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 */,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -331,7 +318,6 @@
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
Expand Down Expand Up @@ -387,7 +373,6 @@
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
Expand Down
100 changes: 94 additions & 6 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,92 @@ class _MyHomePageState extends State<MyHomePage> {
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<String>(
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<UserModel>(
Expand Down Expand Up @@ -180,12 +266,14 @@ class _MyHomePageState extends State<MyHomePage> {
.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
Expand Down
81 changes: 67 additions & 14 deletions lib/dropdown_search.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ typedef Widget ErrorBuilder<T>(
BuildContext context, String searchEntry, dynamic exception);
typedef Widget EmptyBuilder<T>(BuildContext context, String searchEntry);
typedef Widget LoadingBuilder<T>(BuildContext context, String searchEntry);
typedef Widget IconButtonBuilder(BuildContext context);
typedef Future<bool> BeforeChange<T>(T prevItem, T nextItem);

enum Mode { DIALOG, BOTTOM_SHEET, MENU }

Expand Down Expand Up @@ -125,9 +127,18 @@ class DropdownSearch<T> 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;
Expand All @@ -145,10 +156,14 @@ class DropdownSearch<T> 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<T> onBeforeChange;

DropdownSearch({
Key key,
this.onSaved,
Expand Down Expand Up @@ -182,14 +197,18 @@ class DropdownSearch<T> 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,
this.popupBarrierColor,
this.onPopupDismissed,
this.searchBoxController,
this.searchDelay,
this.onBeforeChange,
}) : assert(isFilteredOnline != null),
assert(dropdownBuilderSupportsNullItem != null),
assert(enabled != null),
Expand Down Expand Up @@ -244,7 +263,7 @@ class DropdownSearchState<T> extends State<DropdownSearch<T>> {
: Text(_selectedItemAsString(data),
style: Theme.of(context).textTheme.subtitle1),
),
_manageTrailingIcons(data),
if (!widget.showAsSuffixIcons) _manageTrailingIcons(data),
],
);
}
Expand All @@ -270,7 +289,7 @@ class DropdownSearchState<T> extends State<DropdownSearch<T>> {
(widget.dropdownBuilder == null ||
widget.dropdownBuilderSupportsNullItem),
isFocused: isFocused,
decoration: _manageDropdownDecoration(state),
decoration: _manageDropdownDecoration(state, value),
child: _defaultSelectItemWidget(value),
);
});
Expand All @@ -279,7 +298,7 @@ class DropdownSearchState<T> extends State<DropdownSearch<T>> {
}

///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),
Expand All @@ -289,6 +308,8 @@ class DropdownSearchState<T> extends State<DropdownSearch<T>> {
enabled: widget.enabled,
labelText: widget.label,
hintText: widget.hint,
suffixIcon:
widget.showAsSuffixIcons ? _manageTrailingIcons(data) : null,
errorText: state.errorText);
}

Expand All @@ -305,20 +326,33 @@ class DropdownSearchState<T> extends State<DropdownSearch<T>> {

///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: <Widget>[
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,
),
],
);
}
Expand Down Expand Up @@ -437,8 +471,23 @@ class DropdownSearchState<T> extends State<DropdownSearch<T>> {

///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);
}

Expand Down Expand Up @@ -476,6 +525,10 @@ class DropdownSearchState<T> extends State<DropdownSearch<T>> {
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;

Expand Down

0 comments on commit 70fa719

Please sign in to comment.