| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392 |
- import 'package:dropdown_button2/dropdown_button2.dart';
- import 'package:flutter/foundation.dart';
- import 'form_field_controller.dart';
- import 'package:flutter/material.dart';
- class FlutterFlowDropDown<T> extends StatefulWidget {
- const FlutterFlowDropDown({
- super.key,
- this.controller,
- this.multiSelectController,
- this.hintText,
- this.searchHintText,
- required this.options,
- this.optionLabels,
- this.onChanged,
- this.onMultiSelectChanged,
- this.icon,
- this.width,
- this.height,
- this.maxHeight,
- this.fillColor,
- this.searchHintTextStyle,
- this.searchTextStyle,
- this.searchCursorColor,
- required this.textStyle,
- required this.elevation,
- required this.borderWidth,
- required this.borderRadius,
- required this.borderColor,
- this.focusBorderColor,
- required this.margin,
- this.hidesUnderline = false,
- this.disabled = false,
- this.isOverButton = false,
- this.menuOffset,
- this.isSearchable = false,
- this.isMultiSelect = false,
- this.labelText,
- this.labelTextStyle,
- this.optionsHasValueKeys = false,
- }) : assert(
- isMultiSelect
- ? (controller == null &&
- onChanged == null &&
- multiSelectController != null &&
- onMultiSelectChanged != null)
- : (controller != null &&
- onChanged != null &&
- multiSelectController == null &&
- onMultiSelectChanged == null),
- );
- final FormFieldController<T?>? controller;
- final FormFieldController<List<T>?>? multiSelectController;
- final String? hintText;
- final String? searchHintText;
- final List<T> options;
- final List<String>? optionLabels;
- final Function(T?)? onChanged;
- final Function(List<T>?)? onMultiSelectChanged;
- final Widget? icon;
- final double? width;
- final double? height;
- final double? maxHeight;
- final Color? fillColor;
- final TextStyle? searchHintTextStyle;
- final TextStyle? searchTextStyle;
- final Color? searchCursorColor;
- final TextStyle textStyle;
- final double elevation;
- final double borderWidth;
- final double borderRadius;
- final Color borderColor;
- final Color? focusBorderColor;
- final EdgeInsetsGeometry margin;
- final bool hidesUnderline;
- final bool disabled;
- final bool isOverButton;
- final Offset? menuOffset;
- final bool isSearchable;
- final bool isMultiSelect;
- final String? labelText;
- final TextStyle? labelTextStyle;
- final bool optionsHasValueKeys;
- @override
- State<FlutterFlowDropDown<T>> createState() => _FlutterFlowDropDownState<T>();
- }
- class _FlutterFlowDropDownState<T> extends State<FlutterFlowDropDown<T>> {
- bool get isMultiSelect => widget.isMultiSelect;
- FormFieldController<T?> get controller => widget.controller!;
- FormFieldController<List<T>?> get multiSelectController =>
- widget.multiSelectController!;
- T? get currentValue {
- final value = isMultiSelect
- ? multiSelectController.value?.firstOrNull
- : controller.value;
- return widget.options.contains(value) ? value : null;
- }
- Set<T> get currentValues {
- if (!isMultiSelect || multiSelectController.value == null) {
- return {};
- }
- return widget.options
- .toSet()
- .intersection(multiSelectController.value!.toSet());
- }
- Map<T, String> get optionLabels => Map.fromEntries(
- widget.options.asMap().entries.map(
- (option) => MapEntry(
- option.value,
- widget.optionLabels == null ||
- widget.optionLabels!.length < option.key + 1
- ? option.value.toString()
- : widget.optionLabels![option.key],
- ),
- ),
- );
- EdgeInsetsGeometry get horizontalMargin => widget.margin.clamp(
- EdgeInsetsDirectional.zero,
- const EdgeInsetsDirectional.symmetric(horizontal: double.infinity),
- );
- late void Function() _listener;
- final TextEditingController _textEditingController = TextEditingController();
- late final FocusNode _focusNode = FocusNode()
- ..addListener(() => setState(() => _isFocused = _focusNode.hasFocus));
- bool _isFocused = false;
- @override
- void initState() {
- super.initState();
- if (isMultiSelect) {
- _listener =
- () => widget.onMultiSelectChanged!(multiSelectController.value);
- multiSelectController.addListener(_listener);
- } else {
- _listener = () => widget.onChanged!(controller.value);
- controller.addListener(_listener);
- }
- }
- @override
- void dispose() {
- if (isMultiSelect) {
- multiSelectController.removeListener(_listener);
- } else {
- controller.removeListener(_listener);
- }
- _focusNode.dispose();
- super.dispose();
- }
- @override
- Widget build(BuildContext context) {
- final dropdownWidget = _buildDropdownWidget();
- final effectiveBorderColor = _isFocused && widget.focusBorderColor != null
- ? widget.focusBorderColor!
- : widget.borderColor;
- return SizedBox(
- width: widget.width,
- height: widget.height,
- child: DecoratedBox(
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(widget.borderRadius),
- border: Border.all(
- color: effectiveBorderColor,
- width: widget.borderWidth,
- ),
- color: widget.fillColor,
- ),
- child: Padding(
- padding: _useDropdown2() ? EdgeInsets.zero : widget.margin,
- child: widget.hidesUnderline
- ? DropdownButtonHideUnderline(child: dropdownWidget)
- : dropdownWidget,
- ),
- ),
- );
- }
- bool _useDropdown2() =>
- widget.isMultiSelect ||
- widget.isSearchable ||
- !widget.isOverButton ||
- widget.maxHeight != null;
- Widget _buildDropdownWidget() =>
- _useDropdown2() ? _buildDropdown() : _buildLegacyDropdown();
- Widget _buildLegacyDropdown() {
- return DropdownButtonFormField<T>(
- value: currentValue,
- hint: _createHintText(),
- items: _createMenuItems(),
- elevation: widget.elevation.toInt(),
- onChanged: widget.disabled ? null : (value) => controller.value = value,
- icon: widget.icon,
- isExpanded: true,
- dropdownColor: widget.fillColor,
- focusColor: Colors.transparent,
- decoration: InputDecoration(
- labelText: widget.labelText == null || widget.labelText!.isEmpty
- ? null
- : widget.labelText,
- labelStyle: widget.labelTextStyle,
- border: widget.hidesUnderline
- ? InputBorder.none
- : const UnderlineInputBorder(),
- ),
- );
- }
- Text? _createHintText() => widget.hintText != null
- ? Text(widget.hintText!, style: widget.textStyle)
- : null;
- ValueKey _getItemKey(T option) {
- final widgetKey = (widget.key as ValueKey).value;
- return ValueKey('$widgetKey ${widget.options.indexOf(option)}');
- }
- List<DropdownMenuItem<T>> _createMenuItems() => widget.options
- .map(
- (option) => DropdownMenuItem<T>(
- key: widget.optionsHasValueKeys ? _getItemKey(option) : null,
- value: option,
- child: Padding(
- padding: _useDropdown2() ? horizontalMargin : EdgeInsets.zero,
- child: Text(optionLabels[option] ?? '', style: widget.textStyle),
- )),
- )
- .toList();
- List<DropdownMenuItem<T>> _createMultiselectMenuItems() => widget.options
- .map(
- (item) => DropdownMenuItem<T>(
- key: widget.optionsHasValueKeys ? _getItemKey(item) : null,
- value: item,
- // Disable default onTap to avoid closing menu when selecting an item
- enabled: false,
- child: StatefulBuilder(
- builder: (context, menuSetState) {
- final isSelected =
- multiSelectController.value?.contains(item) ?? false;
- return InkWell(
- onTap: () {
- multiSelectController.value ??= [];
- isSelected
- ? multiSelectController.value!.remove(item)
- : multiSelectController.value!.add(item);
- multiSelectController.update();
- // This rebuilds the StatefulWidget to update the button's text.
- setState(() {});
- // This rebuilds the dropdownMenu Widget to update the check mark.
- menuSetState(() {});
- },
- child: Container(
- height: double.infinity,
- padding: horizontalMargin,
- child: Row(
- children: [
- if (isSelected)
- const Icon(Icons.check_box_outlined)
- else
- const Icon(Icons.check_box_outline_blank),
- const SizedBox(width: 16),
- Expanded(
- child: Text(
- optionLabels[item]!,
- style: widget.textStyle,
- ),
- ),
- ],
- ),
- ));
- },
- ),
- ),
- )
- .toList();
- Widget _buildDropdown() {
- final overlayColor = WidgetStateProperty.resolveWith<Color?>((states) =>
- states.contains(WidgetState.focused) ? Colors.transparent : null);
- final menuItemOverlayColor = kIsWeb ? null : overlayColor;
- final iconStyleData = widget.icon != null
- ? IconStyleData(icon: widget.icon!)
- : const IconStyleData();
- return DropdownButton2<T>(
- value: currentValue,
- hint: _createHintText(),
- items: isMultiSelect ? _createMultiselectMenuItems() : _createMenuItems(),
- focusNode: _focusNode,
- iconStyleData: iconStyleData,
- buttonStyleData: ButtonStyleData(
- elevation: widget.elevation.toInt(),
- overlayColor: overlayColor,
- padding: widget.margin,
- ),
- menuItemStyleData: MenuItemStyleData(
- padding: EdgeInsets.zero,
- overlayColor: menuItemOverlayColor,
- ),
- dropdownStyleData: DropdownStyleData(
- elevation: widget.elevation.toInt(),
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(4.0),
- color: widget.fillColor,
- ),
- isOverButton: widget.isOverButton,
- offset: widget.menuOffset ?? Offset.zero,
- maxHeight: widget.maxHeight,
- padding: EdgeInsets.zero,
- ),
- onChanged: widget.disabled
- ? null
- : (isMultiSelect ? (_) {} : (val) => widget.controller!.value = val),
- isExpanded: true,
- selectedItemBuilder: (context) => widget.options
- .map(
- (item) => Align(
- alignment: AlignmentDirectional.centerStart,
- child: Text(
- isMultiSelect
- ? currentValues
- .where((v) => optionLabels.containsKey(v))
- .map((v) => optionLabels[v])
- .join(', ')
- : optionLabels[item]!,
- style: widget.textStyle,
- maxLines: 1,
- )),
- )
- .toList(),
- dropdownSearchData: widget.isSearchable
- ? DropdownSearchData<T>(
- searchController: _textEditingController,
- searchInnerWidgetHeight: 50,
- searchInnerWidget: Container(
- height: 50,
- padding: const EdgeInsets.only(
- top: 8,
- bottom: 4,
- right: 8,
- left: 8,
- ),
- child: TextFormField(
- expands: true,
- maxLines: null,
- controller: _textEditingController,
- cursorColor: widget.searchCursorColor,
- style: widget.searchTextStyle,
- decoration: InputDecoration(
- isDense: true,
- contentPadding: const EdgeInsets.symmetric(
- horizontal: 10,
- vertical: 8,
- ),
- hintText: widget.searchHintText,
- hintStyle: widget.searchHintTextStyle,
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(8),
- ),
- ),
- ),
- ),
- searchMatchFn: (item, searchValue) {
- return (optionLabels[item.value] ?? '')
- .toLowerCase()
- .contains(searchValue.toLowerCase());
- },
- )
- : null,
- // This is to clear the search value when you close the menu
- onMenuStateChange: (isOpen) {
- if (!isOpen) {
- if (widget.isSearchable) {
- _textEditingController.clear();
- }
- _focusNode.requestFocus();
- }
- },
- );
- }
- }
|