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 select

Usage

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.
            }
          },
        ),
      ],
    ),
  );
}

On this page