Form
Select
A 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 multi selections, consider using a multi select.
For touch devices, a select tile group or select menu tile is generally recommended over this.
const fruits = [
'Apple',
'Banana',
'Blueberry',
'Grapes',
'Lemon',
'Mango',
'Kiwi',
'Orange',
'Peach',
'Pear',
'Pineapple',
'Plum',
'Raspberry',
'Strawberry',
'Watermelon',
];
class SelectPage extends StatelessWidget {
@override
Widget build(BuildContext context) => FSelect<String>.rich(
hint: 'Select a fruit',
format: (s) => s,
children: [for (final fruit in fruits) FSelectItem(title: Text(fruit), value: fruit)],
);
}CLI
To generate and customize this style:
dart run forui style create selectUsage
FSelect(...)
FSelect<Locale>(
items: {
'United States': Locale('en', 'US'),
'Canada': Locale('en', 'CA'),
'Japan': Locale('ja', 'JP'),
},
controller: FSelectController<Locale>(vsync: this),
style: FSelectStyle.inherit(...),
label: const Text('Country'),
description: const Text('Select your country of residence'),
hint: 'Choose a country',
format: (value) => value.toUpperCase(),
onChange: (value) => print('Selected country: $value'),
onSaved: (value) => print('Saved country: $value'),
onReset: () => print('Reset'),
autovalidateMode: AutovalidateMode.onUserInteraction,
builder: (context, style, state, child) => child,
prefixBuilder: (context, style, states) => Icon(FIcons.globe),
suffixBuilder: (context, style, states) => Icon(FIcons.arrowDown),
popoverConstraints: const FAutoWidthPortalConstraints(maxHeight: 400),
clearable: true,
contentScrollHandles: true,
);FSelect.rich(...)
FSelect<String>.rich(
controller: FSelectController<String>(vsync: this),
style: FSelectStyle(...),
label: const Text('Country'),
description: const Text('Select your country of residence'),
hint: 'Choose a country',
format: (value) => value.toUpperCase(),
onChange: (value) => print('Selected country: $value'),
onSaved: (value) => print('Saved country: $value'),
onReset: () => print('Reset'),
autovalidateMode: AutovalidateMode.onUserInteraction,
builder: (context, style, state, child) => child,
prefixBuilder: (context, style, states) => Icon(FIcons.globe),
suffixBuilder: (context, style, states) => Icon(FIcons.arrowDown),
popoverConstraints: const FAutoWidthPortalConstraints(maxHeight: 400),
clearable: true,
contentDivider: FItemDivider.none,
contentScrollHandles: true,
initialValue: 'ca',
children: [
FSelectSection(
label: const Text('North American Countries'),
divider: FItemDivider.none,
children: [
FSelectItem(
title: const Text('United States'),
divider: FItemDivider.none,
value: 'us',
),
FSelectItem(
title: Text('Canada'),
value: 'ca',
),
],
),
FSelectItem(
title: Text('Japan'),
value: 'jp',
),
],
);FSelect.search(...)
FSelect<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: FSelectController<User>(),
style: FSelectStyle.inherit(...),
label: const Text('User'),
description: const Text('Search and select a user'),
builder: (context, styles, child) => child!,
format: (user) => '${user.firstName} ${user.lastName}',
hint: 'Search users...',
popoverConstraints: const FAutoWidthPortalConstraints(maxHeight: 400),
clearable: true,
autoHide: false,
onChange: (value) => print('Selected country: $value'),
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, style, 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...'),
);FSelect.searchBuilder(...)
FSelect<User>.searchBuilder(
controller: FSelectController<User>(),
style: FSelectStyle.inherit(...),
label: const Text('User'),
description: const Text('Search and select a user'),
builder: (context, styles, child) => child!,
format: (user) => '${user.firstName} ${user.lastName}',
hint: 'Search users...',
popoverConstraints: const FAutoWidthPortalConstraints(maxHeight: 400),
clearable: true,
autoHide: false,
onChange: (value) => print('Selected country: $value'),
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
FSelect<String>.rich(
hint: 'Type',
format: (s) => s,
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
FSelect<String>.rich(
hint: 'Select a timezone',
format: (s) => s,
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
FSelect<String>.rich(
hint: 'Select a level',
contentDivider: FItemDivider.full,
format: (s) => s,
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
FSelect<String>.searchBuilder(
hint: 'Select a fruit',
format: (s) => s,
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
FSelect<String>.searchBuilder(
hint: 'Select a fruit',
format: (s) => s,
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
FSelect<String>.searchBuilder(
hint: 'Select a fruit',
format: (s) => s,
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
FSelect<String>.searchBuilder(
hint: 'Select a fruit',
format: (s) => s,
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
Toggleable Items
FSelect<String>.rich(
control: const FSelectControl.managed(initial: 'Apple', toggleable: true),
hint: 'Select a fruit',
format: (s) => s,
children: [for (final fruit in fruits) FSelectItem(title: Text(fruit), value: fruit)],
);Clearable
FSelect<String>.rich(
hint: 'Select a fruit',
format: (s) => s,
clearable: true,
children: [for (final fruit in fruits) FSelectItem(title: Text(fruit), value: fruit)],
);Custom Formatting
FSelect<({String firstName, String lastName})>.rich(
hint: 'Select a user',
format: (user) => '${user.firstName} ${user.lastName}',
children: [for (final user in users) FSelectItem(title: Text(user.firstName), value: user)],
);Form
class FormSelectPage extends StatefulWidget {
@override
State<FormSelectPage> createState() => _FormSelectPageState();
}
class _FormSelectPageState extends State<FormSelectPage> {
final _key = GlobalKey<FormState>();
@override
Widget build(BuildContext context) => Form(
key: _key,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FSelect<String>.rich(
label: const Text('Department'),
description: const Text('Choose your dream department'),
hint: 'Select a department',
format: (s) => s,
validator: (department) => department == null ? 'Please select a department' : 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 department.
}
},
),
],
),
);
}