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.
1const fruits = [2 'Apple',3 'Banana',4 'Blueberry',5 'Grapes',6 'Lemon',7 'Mango',8 'Kiwi',9 'Orange',10 'Peach',11 'Pear',12 'Pineapple',13 'Plum',14 'Raspberry',15 'Strawberry',16 'Watermelon',17];1819class MultiSelectExample extends StatelessWidget {20 @override21 Widget build(BuildContext _) => FMultiSelect<String>.rich(22 hint: const Text('Select a fruit'),23 format: Text.new,24 children: [25 for (final fruit in fruits) .item(title: Text(fruit), value: fruit),26 ],27 );28}29CLI
To generate and customize this style:
dart run forui style create multi-selectUsage
FMultiSelect(...)
1FMultiSelect<String>(2 style: const .delta(emptyTextStyle: .delta()),3 enabled: true,4 items: const {'Apple': 'apple', 'Banana': 'banana', 'Cherry': 'cherry'},5)FMultiSelect.rich(...)
1FMultiSelect<String>.rich(2 style: const .delta(emptyTextStyle: .delta()),3 enabled: true,4 format: Text.new,5 children: [6 .item(title: const Text('Apple'), value: 'apple'),7 .item(title: const Text('Banana'), value: 'banana'),8 .section(9 label: const Text('More'),10 items: {'Cherry': 'cherry', 'Date': 'date'},11 ),12 ],13)FMultiSelect.search(...)
1FMultiSelect<String>.search(2 const {'Apple': 'apple', 'Banana': 'banana', 'Cherry': 'cherry'},3 style: const .delta(emptyTextStyle: .delta()),4 enabled: true,5 filter: (query) =>6 ['apple', 'banana', 'cherry'].where((e) => e.startsWith(query)),7)FMultiSelect.searchBuilder(...)
1FMultiSelect<String>.searchBuilder(2 style: const .delta(emptyTextStyle: .delta()),3 enabled: true,4 filter: (query) =>5 ['apple', 'banana', 'cherry'].where((e) => e.startsWith(query)),6 format: Text.new,7 contentBuilder: (context, style, values) => [8 for (final value in values) .item(title: Text(value), value: value),9 ],10)Examples
Detailed
1@override2Widget build(BuildContext _) => FMultiSelect<String>.rich(3 hint: const Text('Type'),4 format: Text.new,5 children: [6 .item(7 prefix: const Icon(FIcons.bug),8 title: const Text('Bug'),9 subtitle: const Text('An unexpected problem or behavior'),10 value: 'Bug',11 ),12 .item(13 prefix: const Icon(FIcons.filePlusCorner),14 title: const Text('Feature'),15 subtitle: const Text('A new feature or enhancement'),16 value: 'Feature',17 ),18 .item(19 prefix: const Icon(FIcons.messageCircleQuestionMark),20 title: const Text('Question'),21 subtitle: const Text('A question or clarification'),22 value: 'Question',23 ),24 ],25);26Sections
1@override2Widget build(BuildContext _) => FMultiSelect<String>.rich(3 hint: const Text('Select a timezone'),4 format: Text.new,5 children: [6 .section(7 label: const Text('North America'),8 items: {9 for (final item in [10 'Eastern Standard Time (EST)',11 'Central Standard Time (CST)',12 'Mountain Standard Time (MST)',13 'Pacific Standard Time (PST)',14 'Alaska Standard Time (AKST)',15 'Hawaii Standard Time (HST)',16 ])17 item: item,18 },19 ),20 .section(21 label: const Text('South America'),22 items: {23 for (final item in [24 'Argentina Time (ART)',25 'Bolivia Time (BOT)',26 'Brasilia Time (BRT)',27 'Chile Standard Time (CLT)',28 ])29 item: item,30 },31 ),32 .section(33 label: const Text('Europe & Africa'),34 items: {35 for (final item in [36 'Greenwich Mean Time (GMT)',37 'Central European Time (CET)',38 'Eastern European Time (EET)',39 'Western European Summer Time (WEST)',40 'Central Africa Time (CAT)',41 'Eastern Africa Time (EAT)',42 ])43 item: item,44 },45 ),46 .section(47 label: const Text('Asia'),48 items: {49 for (final item in [50 'Moscow Time (MSK)',51 'India Standard Time (IST)',52 'China Standard Time (CST)',53 'Japan Standard Time (JST)',54 'Korea Standard Time (KST)',55 'Indonesia Standard Time (IST)',56 ])57 item: item,58 },59 ),60 .section(61 label: const Text('Australia & Pacific'),62 items: {63 for (final item in [64 'Australian Western Standard Time (AWST)',65 'Australian Central Standard Time (ACST)',66 'Australian Eastern Standard Time (AEST)',67 'New Zealand Standard Time (NZST)',68 'Fiji Time (FJT)',69 ])70 item: item,71 },72 ),73 ],74);75Dividers
1@override2Widget build(BuildContext _) => FMultiSelect<String>.rich(3 hint: const Text('Select a level'),4 contentDivider: .full,5 format: Text.new,6 children: [7 .section(8 label: const Text('Level 1'),9 divider: .indented,10 items: {11 for (final item in ['A', 'B']) item: '1$item',12 },13 ),14 .section(15 label: const Text('Level 2'),16 items: {17 for (final item in ['A', 'B']) item: '2$item',18 },19 ),20 .item(title: const Text('Level 3'), value: '3'),21 .item(title: const Text('Level 4'), value: '4'),22 ],23);24Searchable
Sync
1const fruits = [2 'Apple',3 'Banana',4 'Blueberry',5 'Grapes',6 'Lemon',7 'Mango',8 'Kiwi',9 'Orange',10 'Peach',11 'Pear',12 'Pineapple',13 'Plum',14 'Raspberry',15 'Strawberry',16 'Watermelon',17];1819class SyncMultiSelectExample extends StatelessWidget {20 @override21 Widget build(BuildContext _) => FMultiSelect<String>.searchBuilder(22 hint: const Text('Select a fruit'),23 format: Text.new,24 filter: (query) => query.isEmpty25 ? fruits26 : fruits.where((f) => f.toLowerCase().startsWith(query.toLowerCase())),27 contentBuilder: (context, _, fruits) => [28 for (final fruit in fruits) .item(title: Text(fruit), value: fruit),29 ],30 );31}32Async
1const fruits = [2 'Apple',3 'Banana',4 'Blueberry',5 'Grapes',6 'Lemon',7 'Mango',8 'Kiwi',9 'Orange',10 'Peach',11 'Pear',12 'Pineapple',13 'Plum',14 'Raspberry',15 'Strawberry',16 'Watermelon',17];1819class AsyncMultiSelectExample extends StatelessWidget {20 @override21 Widget build(BuildContext _) => FMultiSelect<String>.searchBuilder(22 hint: const Text('Select a fruit'),23 format: Text.new,24 filter: (query) async {25 await Future.delayed(const Duration(seconds: 1));26 return query.isEmpty27 ? fruits28 : fruits.where(29 (fruit) => fruit.toLowerCase().startsWith(query.toLowerCase()),30 );31 },32 contentBuilder: (context, _, fruits) => [33 for (final fruit in fruits) .item(title: Text(fruit), value: fruit),34 ],35 );36}37Async with Custom Loading
1const fruits = [2 'Apple',3 'Banana',4 'Blueberry',5 'Grapes',6 'Lemon',7 'Mango',8 'Kiwi',9 'Orange',10 'Peach',11 'Pear',12 'Pineapple',13 'Plum',14 'Raspberry',15 'Strawberry',16 'Watermelon',17];1819class AsyncLoadingMultiSelectExample extends StatelessWidget {20 @override21 Widget build(BuildContext _) => FMultiSelect<String>.searchBuilder(22 hint: const Text('Select a fruit'),23 format: Text.new,24 filter: (query) async {25 await Future.delayed(const Duration(seconds: 1));26 return query.isEmpty27 ? fruits28 : fruits.where(29 (fruit) => fruit.toLowerCase().startsWith(query.toLowerCase()),30 );31 },32 contentLoadingBuilder: (context, style) => Padding(33 padding: const .all(8.0),34 child: Text(35 'Here be dragons...',36 style: style.fieldStyle.contentTextStyle.resolve({}),37 ),38 ),39 contentBuilder: (context, _, fruits) => [40 for (final fruit in fruits) .item(title: Text(fruit), value: fruit),41 ],42 );43}44Async with Custom Error Handling
1const fruits = [2 'Apple',3 'Banana',4 'Blueberry',5 'Grapes',6 'Lemon',7 'Mango',8 'Kiwi',9 'Orange',10 'Peach',11 'Pear',12 'Pineapple',13 'Plum',14 'Raspberry',15 'Strawberry',16 'Watermelon',17];1819class AsyncErrorMultiSelectExample extends StatelessWidget {20 @override21 Widget build(BuildContext _) => FMultiSelect<String>.searchBuilder(22 hint: const Text('Select a fruit'),23 format: Text.new,24 filter: (query) async {25 await Future.delayed(const Duration(seconds: 1));26 throw StateError('Error loading data');27 },28 contentBuilder: (context, _, fruits) => [29 for (final fruit in fruits) .item(title: Text(fruit), value: fruit),30 ],31 contentErrorBuilder: (context, error, trace) {32 final style = context.theme.selectStyle.fieldStyle.iconStyle.resolve({});33 return Padding(34 padding: const .all(8.0),35 child: Icon(36 FIcons.messageCircleX,37 size: style.size,38 color: style.color,39 ),40 );41 },42 );43}44Behavior
Clearable
1const fruits = [2 'Apple',3 'Banana',4 'Blueberry',5 'Grapes',6 'Lemon',7 'Mango',8 'Kiwi',9 'Orange',10 'Peach',11 'Pear',12 'Pineapple',13 'Plum',14 'Raspberry',15 'Strawberry',16 'Watermelon',17];1819class ClearableMultiSelectExample extends StatelessWidget {20 @override21 Widget build(BuildContext _) => FMultiSelect<String>.rich(22 hint: const Text('Select a fruit'),23 format: Text.new,24 clearable: true,25 children: [26 for (final fruit in fruits) .item(title: Text(fruit), value: fruit),27 ],28 );29}30Custom Formatting
1@override2Widget build(BuildContext _) =>3 FMultiSelect<({String firstName, String lastName})>.rich(4 hint: const Text('Select a user'),5 format: (user) => Text('${user.firstName} ${user.lastName}'),6 children: [7 for (final user in users)8 .item(title: Text(user.firstName), value: user),9 ],10 );11Min & Max
1const fruits = [2 'Apple',3 'Banana',4 'Blueberry',5 'Grapes',6 'Lemon',7 'Mango',8 'Kiwi',9 'Orange',10 'Peach',11 'Pear',12 'Pineapple',13 'Plum',14 'Raspberry',15 'Strawberry',16 'Watermelon',17];1819class MinMaxMultiSelectExample extends StatelessWidget {20 @override21 Widget build(BuildContext context) => FMultiSelect<String>.rich(22 control: const .managed(min: 1, max: 3),23 hint: const Text('Select favorite fruits'),24 format: Text.new,25 children: [26 for (final fruit in fruits) .item(title: Text(fruit), value: fruit),27 ],28 );29}30Sorted
1const fruits = [2 'Apple',3 'Banana',4 'Blueberry',5 'Grapes',6 'Lemon',7 'Mango',8 'Kiwi',9 'Orange',10 'Peach',11 'Pear',12 'Pineapple',13 'Plum',14 'Raspberry',15 'Strawberry',16 'Watermelon',17];1819class SortedMultiSelectExample extends StatelessWidget {20 @override21 Widget build(BuildContext _) => FMultiSelect<String>.rich(22 hint: const Text('Select favorite fruits'),23 format: Text.new,24 sort: (a, b) => a.compareTo(b),25 children: [26 for (final fruit in fruits) .item(title: Text(fruit), value: fruit),27 ],28 );29}30Form
1class FormMultiSelectExample extends StatefulWidget {2 @override3 State<FormMultiSelectExample> createState() => _FormMultiSelectExampleState();4}56class _FormMultiSelectExampleState extends State<FormMultiSelectExample> {7 final _key = GlobalKey<FormState>();89 @override10 Widget build(BuildContext context) => Form(11 key: _key,12 child: Column(13 crossAxisAlignment: .start,14 spacing: 25,15 children: [16 FMultiSelect<String>.rich(17 label: const Text('Department'),18 description: const Text('Choose your dream department(s)'),19 hint: const Text('Select departments'),20 format: Text.new,21 validator: (departments) =>22 departments.isEmpty ? 'Please select departments' : null,23 children: [24 for (final department in const [25 'Engineering',26 'Marketing',27 'Sales',28 'Human Resources',29 'Finance',30 ])31 .item(title: Text(department), value: department),32 ],33 ),34 FButton(35 child: const Text('Submit'),36 onPress: () {37 if (_key.currentState!.validate()) {38 // Form is valid, do something with departments39 }40 },41 ),42 ],43 ),44 );45}46Label
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.