Form
Multi Select
A multi select displays a list of drop-down options for the user to pick from. It is a form-field and can therefore be used in a form.
For single selections, consider using a select.
For touch devices, a select tile group or select menu tile is generally recommended over this.
FMultiSelect<String>.rich(
hint: const Text('Select a fruit'),
format: Text.new,
children: [for (final fruit in fruits) FSelectItem(title: Text(fruit), value: fruit)],
);CLI
To generate and customize this style:
dart run forui style create multi-selectUsage
FMultiSelect(...)
FMultiSelect<Locale>(
items: {
'United States': Locale('en', 'US'),
'Canada': Locale('en', 'CA'),
'Japan': Locale('ja', 'JP'),
},
controller: FMultiSelectController<Locale>(vsync: this, min: 1, max: 2),
style: FMultiSelectStyle.inherit(...),
label: const Text('Country'),
description: const Text('Select your country of residence'),
hint: Text('Choose a country'),
keepHint: true,
format: (value) => Text(value.toUpperCase()),
sort: (a, b) => a.compareTo(b),
onChange: (value) => print('Selected country: $value'),
onSaved: (value) => print('Saved country: $value'),
onReset: () => print('Reset'),
autovalidateMode: AutovalidateMode.onUserInteraction,
prefixBuilder: (context, styles) => Icon(FIcons.globe),
suffixBuilder: (context, styles) => Icon(FIcons.arrowDown),
tagBuilder: (context, controller, styles, value, label) => FMultiSelectTag(label: label),
popoverConstraints: const FAutoWidthPortalConstraints(maxHeight: 400),
clearable: true,
contentScrollHandles: true,
min: 1,
max: 2,
initialValue: {Locale('en', 'US')},
);FMultiSelect.rich(...)
FMultiSelect<String>.rich(
controller: FMultiSelectController<String>(vsync: this, min: 1, max: 2),
style: FMultiSelectStyle(...),
label: const Text('Country'),
description: const Text('Select your country of residence'),
hint: Text('Choose a country'),
keepHint: true,
format: (value) => Text(value.toUpperCase()),
sort: (a, b) => a.compareTo(b),
onChange: (value) => print('Selected country: $value'),
onSaved: (value) => print('Saved country: $value'),
onReset: () => print('Reset'),
autovalidateMode: AutovalidateMode.onUserInteraction,
prefixBuilder: (context, styles) => Icon(FIcons.globe),
suffixBuilder: (context, styles) => Icon(FIcons.arrowDown),
tagBuilder: (context, controller, styles, value, label) => FMultiSelectTag( label: label),
popoverConstraints: const FAutoWidthPortalConstraints(maxHeight: 400),
clearable: true,
contentDivider: FItemDivider.none,
contentScrollHandles: true,
min: 1,
max: 2,
initialValue: {'ca'},
children: [
FSelectSection.rich(
label: const Text('North American Countries'),
divider: FItemDivider.none,
children: [
FSelectItem( title: const Text('United States'), value: 'us'),
FSelectItem(title: Text('Canada'), value: 'ca'),
],
),
FSelectItem(title: Text('Japan'), value: 'jp'),
],
);FMultiSelect.search(...)
FMultiSelect<User>.search(
items: {
'Bob Ross': User(firstName: 'Bob', lastName: 'Ross'),
'John Doe': User(firstName: 'John', lastName: 'Doe'),
'Mary Jane': User(firstName: 'Mary', lastName: 'Jane'),
'Peter Parker': User(firstName: 'Peter', lastName: 'Parker'),
},
controller: FMultiSelectController<User>(vsync: this, min: 1, max: 2),
style: FMultiSelectStyle.inherit(...),
label: const Text('User'),
description: const Text('Search and select a user'),
format: (user) => Text('${user.firstName} ${user.lastName}'),
sort: (a, b) => a.compareTo(b),
hint: Text('Search users...'),
popoverConstraints: const FAutoWidthPortalConstraints(maxHeight: 400),
clearable: true,
onChange: (value) => print('Selected country: $value'),
tagBuilder: (context, controller, styles, value, label) => FMultiSelectTag( label: label),
min: 1,
max: 2,
initialValue: {'value'},
contentDivider: FItemDivider.none,
contentScrollHandles: false,
contentPhysics: const BouncingScrollPhysics(),
contentEmptyBuilder: (context, style) => Text('No results'),
filter: (query) async {
// Fetch users based on search query
return fetchUsers(query);
},
contentBuilder: (context, _, users) => [
for (final user in users)
FSelectItem(title: Text('${user.firstName} ${user.lastName}'), value: user),
],
searchFieldProperties: FSelectSearchFieldProperties(
autofocus: true,
hint: 'Type to search...',
),
contentLoadingBuilder: (context, style) => Text('Loading...'),
contentErrorBuilder: (context, error, stackTrace) => Text('Error...'),
);FMultiSelect.searchBuilder(...)
FMultiSelect<User>.search(
controller: FMultiSelectController<User>(vsync: this, min: 1, max: 2),
style: FMultiSelectStyle.inherit(...),
label: const Text('User'),
description: const Text('Search and select a user'),
format: (user) => Text('${user.firstName} ${user.lastName}'),
sort: (a, b) => a.compareTo(b),
hint: Text('Search users...'),
tagBuilder: (context, controller, styles, value, label) => FMultiSelectTag( label: label),
popoverConstraints: const FAutoWidthPortalConstraints(maxHeight: 400),
clearable: true,
onChange: (value) => print('Selected country: $value'),
min: 1,
max: 2,
initialValue: {'value'},
contentDivider: FItemDivider.none,
contentScrollHandles: false,
contentPhysics: const BouncingScrollPhysics(),
contentEmptyBuilder: (context, style) => Text('No results'),
filter: (query) async {
// Fetch users based on search query
return fetchUsers(query);
},
contentBuilder: (context, _, users) => [
for (final user in users)
FSelectItem(title: Text('${user.firstName} ${user.lastName}'), value: user),
],
searchFieldProperties: FSelectSearchFieldProperties(
autofocus: true,
hint: 'Type to search...',
),
contentLoadingBuilder: (context, style) => Text('Loading...'),
contentErrorBuilder: (context, error, stackTrace) => Text('Error...'),
);Examples
Detailed
FMultiSelect<String>.rich(
hint: const Text('Type'),
format: Text.new,
children: [
FSelectItem(
prefix: const Icon(FIcons.bug),
title: const Text('Bug'),
subtitle: const Text('An unexpected problem or behavior'),
value: 'Bug',
),
FSelectItem(
prefix: const Icon(FIcons.filePlus2),
title: const Text('Feature'),
subtitle: const Text('A new feature or enhancement'),
value: 'Feature',
),
FSelectItem(
prefix: const Icon(FIcons.messageCircleQuestionMark),
title: const Text('Question'),
subtitle: const Text('A question or clarification'),
value: 'Question',
),
],
);Sections
FMultiSelect<String>.rich(
hint: const Text('Select a timezone'),
format: Text.new,
children: [
FSelectSection(
label: const Text('North America'),
items: {
for (final item in [
'Eastern Standard Time (EST)',
'Central Standard Time (CST)',
'Mountain Standard Time (MST)',
'Pacific Standard Time (PST)',
'Alaska Standard Time (AKST)',
'Hawaii Standard Time (HST)',
])
item: item,
},
),
FSelectSection(
label: const Text('South America'),
items: {
for (final item in [
'Argentina Time (ART)',
'Bolivia Time (BOT)',
'Brasilia Time (BRT)',
'Chile Standard Time (CLT)',
])
item: item,
},
),
FSelectSection(
label: const Text('Europe & Africa'),
items: {
for (final item in [
'Greenwich Mean Time (GMT)',
'Central European Time (CET)',
'Eastern European Time (EET)',
'Western European Summer Time (WEST)',
'Central Africa Time (CAT)',
'Eastern Africa Time (EAT)',
])
item: item,
},
),
FSelectSection(
label: const Text('Asia'),
items: {
for (final item in [
'Moscow Time (MSK)',
'India Standard Time (IST)',
'China Standard Time (CST)',
'Japan Standard Time (JST)',
'Korea Standard Time (KST)',
'Indonesia Standard Time (IST)',
])
item: item,
},
),
FSelectSection(
label: const Text('Australia & Pacific'),
items: {
for (final item in [
'Australian Western Standard Time (AWST)',
'Australian Central Standard Time (ACST)',
'Australian Eastern Standard Time (AEST)',
'New Zealand Standard Time (NZST)',
'Fiji Time (FJT)',
])
item: item,
},
),
],
);Dividers
FMultiSelect<String>.rich(
hint: const Text('Select a level'),
contentDivider: FItemDivider.full,
format: Text.new,
children: [
FSelectSection(
label: const Text('Level 1'),
divider: FItemDivider.indented,
items: {
for (final item in ['A', 'B']) item: '1$item',
},
),
FSelectSection(
label: const Text('Level 2'),
items: {
for (final item in ['A', 'B']) item: '2$item',
},
),
FSelectItem(title: const Text('Level 3'), value: '3'),
FSelectItem(title: const Text('Level 4'), value: '4'),
],
);Searchable
Sync
FMultiSelect<String>.searchBuilder(
hint: const Text('Select a fruit'),
format: Text.new,
filter: (query) => query.isEmpty ? fruits : fruits.where((f) => f.toLowerCase().startsWith(query.toLowerCase())),
contentBuilder: (context, _, fruits) => [for (final fruit in fruits) FSelectItem(title: Text(fruit), value: fruit)],
);Async
FMultiSelect<String>.searchBuilder(
hint: const Text('Select a fruit'),
format: Text.new,
filter: (query) async {
await Future.delayed(const Duration(seconds: 1));
return query.isEmpty ? fruits : fruits.where((fruit) => fruit.toLowerCase().startsWith(query.toLowerCase()));
},
contentBuilder: (context, _, fruits) => [for (final fruit in fruits) FSelectItem(title: Text(fruit), value: fruit)],
);Async with Custom Loading
FMultiSelect<String>.searchBuilder(
hint: const Text('Select a fruit'),
format: Text.new,
filter: (query) async {
await Future.delayed(const Duration(seconds: 1));
return query.isEmpty ? fruits : fruits.where((fruit) => fruit.toLowerCase().startsWith(query.toLowerCase()));
},
contentLoadingBuilder: (context, style) => Padding(
padding: const EdgeInsets.all(8.0),
child: Text('Here be dragons...', style: style.textFieldStyle.contentTextStyle.resolve({})),
),
contentBuilder: (context, _, fruits) => [for (final fruit in fruits) FSelectItem(title: Text(fruit), value: fruit)],
);Async with Custom Error Handling
FMultiSelect<String>.searchBuilder(
hint: const Text('Select a fruit'),
format: Text.new,
filter: (query) async {
await Future.delayed(const Duration(seconds: 1));
throw StateError('Error loading data');
},
contentBuilder: (context, _, fruits) => [for (final fruit in fruits) FSelectItem(title: Text(fruit), value: fruit)],
contentErrorBuilder: (context, error, trace) {
final style = context.theme.selectStyle.iconStyle;
return Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(FIcons.messageCircleX, size: style.size, color: style.color),
);
},
);Behavior
Clearable
FMultiSelect<String>.rich(
hint: const Text('Select a fruit'),
format: Text.new,
clearable: true,
children: [for (final fruit in fruits) FSelectItem(title: Text(fruit), value: fruit)],
);Custom Formatting
FMultiSelect<({String firstName, String lastName})>.rich(
hint: const Text('Select a user'),
format: (user) => Text('${user.firstName} ${user.lastName}'),
children: [for (final user in users) FSelectItem(title: Text(user.firstName), value: user)],
);Min & Max
FMultiSelect<String>.rich(
control: const FMultiSelectControl.managed(min: 1, max: 3),
hint: const Text('Select favorite fruits'),
format: Text.new,
children: [for (final fruit in fruits) FSelectItem(title: Text(fruit), value: fruit)],
);With Scroll Handles
FMultiSelect<String>.rich(
hint: const Text('Select a fruit'),
format: Text.new,
contentScrollHandles: true,
children: [for (final fruit in fruits) FSelectItem(title: Text(fruit), value: fruit)],
);Sorted
const fruits = [
'Apple',
'Banana',
'Blueberry',
'Grapes',
'Lemon',
'Mango',
'Kiwi',
'Orange',
'Peach',
'Pear',
'Pineapple',
'Plum',
'Raspberry',
'Strawberry',
'Watermelon',
];
FMultiSelect<String>.rich(
hint: const Text('Select favorite fruits'),
format: Text.new,
sort: (a, b) => a.compareTo(b),
children: [for (final fruit in fruits) FSelectItem(title: Text(fruit), value: fruit)],
);Form
class FormMultiSelectPage extends StatefulWidget {
@override
State<FormMultiSelectPage> createState() => _FormMultiSelectPageState();
}
class _FormMultiSelectPageState extends State<FormMultiSelectPage> {
final _key = GlobalKey<FormState>();
@override
Widget build(BuildContext context) => Form(
key: _key,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FMultiSelect<String>.rich(
label: const Text('Department'),
description: const Text('Choose your dream department(s)'),
hint: const Text('Select departments'),
format: Text.new,
validator: (departments) => departments.isEmpty ? 'Please select departments' : null,
children: [
for (final department in const ['Engineering', 'Marketing', 'Sales', 'Human Resources', 'Finance'])
FSelectItem(title: Text(department), value: department),
],
),
const SizedBox(height: 25),
FButton(
child: const Text('Submit'),
onPress: () {
if (_key.currentState!.validate()) {
// Form is valid, do something with departments
}
},
),
],
),
);
}Label
Describes a form field with a label, description, and error message (if any). This widget is usually used for custom form fields. All form fields in Forui come with this widget wrapped.
Picker
A generic picker that allows an item to be selected. It is composed of one or more wheels, optionally with separators between those wheels. The picker supports arrow key navigation. Recommended for touch devices.