Form

Autocomplete

An autocomplete provides a list of suggestions based on the user's input and shows typeahead text for the first match. It is a form-field and can therefore be used in a form.

FAutocomplete(
  label: const Text('Autocomplete'),
  hint: 'What can it do?',
  items: const ['Keyboard navigation', 'Typeahead suggestions', 'Tab to complete'],
);

CLI

To generate and customize this style:

dart run forui style create autocomplete

Usage

FAutocomplete(...)

FAutocomplete(
  controller: FAutocompleteController(vsync: this),
  style: (style) => style,
  label: const Text('Country'),
  description: const Text('Select your country of residence'),
  hint: 'Type to search countries',
  onChange: (value) => print('Selected country: $value'),
  onSaved: (value) => print('Saved country: $value'),
  onReset: () => print('Reset'),
  autovalidateMode: AutovalidateMode.onUserInteraction,
  builder: (context, styles, child) => child!,
  prefixBuilder: (context, style, states) => Icon(FIcons.globe),
  suffixBuilder: (context, style, states) => Icon(FIcons.search),
  contentConstraints: const FAutoWidthPortalConstraints(maxHeight: 400),
  clearable: (value) => value.text.isNotEmpty,
  rightArrowToComplete: true,
  initialText: 'Canada',
  items: [
    'United States',
    'Canada',
    'Japan',
    'United Kingdom',
    'Germany',
    'France',
    'Australia',
  ],
);

FAutocomplete.builder(...)

FAutocomplete.builder(
  filter: (text) async {
    const countries = ['United States', 'Canada', 'Japan'];
    return countries.where((country) => country.toLowerCase().contains(query.toLowerCase()));
  },
  contentBuilder: (context, text, suggestions) => [
    for (final suggestion in suggestions) FAutocompleteItem(value: suggestion)
  ],
  controller: FAutocompleteController(vsync: this),
  style: (style) => style,
  label: const Text('Country'),
  description: const Text('Select your country of residence'),
  hint: 'Type to search countries',
  onChange: (value) => print('Selected country: $value'),
  onSaved: (value) => print('Saved country: $value'),
  onReset: () => print('Reset'),
  autovalidateMode: AutovalidateMode.onUserInteraction,
  builder: (context, styles, child) => child!,
  prefixBuilder: (context, style, states) => Icon(FIcons.globe),
  suffixBuilder: (context, style, states) => Icon(FIcons.search),
  contentConstraints: const FAutoWidthPortalConstraints(maxHeight: 300),
  rightArrowToComplete: true,
  clearable: (value) => value.text.isNotEmpty,
);

Examples

Detailed

FAutocomplete.builder(
  hint: 'Type to search',
  filter: (query) => const ['Bug', 'Feature', 'Question'].where((i) => i.toLowerCase().contains(query.toLowerCase())),
  contentBuilder: (context, query, suggestions) => [
    for (final suggestion in suggestions)
      switch (suggestion) {
        'Bug' => FAutocompleteItem(
          value: 'Bug',
          prefix: const Icon(FIcons.bug),
          title: const Text('Bug'),
          subtitle: const Text('An unexpected problem or behavior'),
        ),
        'Feature' => FAutocompleteItem(
          value: 'Feature',
          prefix: const Icon(FIcons.filePlus2),
          title: const Text('Feature'),
          subtitle: const Text('A new feature or enhancement'),
        ),
        'Question' => FAutocompleteItem(
          value: 'Question',
          prefix: const Icon(FIcons.messageCircleQuestionMark),
          title: const Text('Question'),
          subtitle: const Text('A question or clarification'),
        ),
        _ => FAutocompleteItem(value: suggestion),
      },
  ],
);

Sections

const _timezones = {
  'North America': [
    'Eastern Standard Time (EST)',
    'Central Standard Time (CST)',
    'Mountain Standard Time (MST)',
    'Pacific Standard Time (PST)',
    'Alaska Standard Time (AKST)',
    'Hawaii Standard Time (HST)',
  ],
  'South America': [
    'Argentina Time (ART)',
    'Bolivia Time (BOT)',
    'Brasilia Time (BRT)',
    'Chile Standard Time (CLT)',
  ],
  'Europe & Africa': [
    '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)',
  ],
  'Asia': [
    'Moscow Time (MSK)',
    'India Standard Time (IST)',
    'China Standard Time (CST)',
    'Japan Standard Time (JST)',
    'Korea Standard Time (KST)',
    'Indonesia Standard Time (IST)',
  ],
  'Australia & Pacific': [
    'Australian Western Standard Time (AWST)',
    'Australian Central Standard Time (ACST)',
    'Australian Eastern Standard Time (AEST)',
    'New Zealand Standard Time (NZST)',
    'Fiji Time (FJT)',
  ],
};

FAutocomplete.builder(
  hint: 'Type to search timezones',
  filter: (query) => timezones.values
      .expand((list) => list)
      .where((timezone) => timezone.toLowerCase().contains(query.toLowerCase())),
  contentBuilder: (context, query, suggestions) => [
    for (final MapEntry(key: label, value: zones) in timezones.entries)
      if (zones.where(suggestions.contains).toList() case final zones when zones.isNotEmpty)
        FAutocompleteSection(label: Text(label), items: zones),
  ],
);

Dividers

FAutocomplete.builder(
  hint: 'Type to search levels',
  filter: (query) => const ['1A', '1B', '2A', '2B', '3', '4'].where((i) => i.toLowerCase().contains(query.toLowerCase())),
  contentBuilder: (context, query, suggestions) => <FAutocompleteItemMixin>[
    FAutocompleteRichSection(
      label: const Text('Level 1'),
      divider: FItemDivider.indented,
      children: [
        if (suggestions.contains('1A')) FAutocompleteItem(value: '1A'),
        if (suggestions.contains('1B')) FAutocompleteItem(value: '1B'),
      ],
    ),
    FAutocompleteSection(label: const Text('Level 2'), items: ['2A', '2B'].where(suggestions.contains).toList()),
    if (suggestions.contains('3')) FAutocompleteItem(value: '3'),
    if (suggestions.contains('4')) FAutocompleteItem(value: '4'),
  ].where((item) => item is! FAutocompleteSection || item.children.isNotEmpty).toList(),
);

Behavior

Async

const fruits = ['Apple', 'Banana', 'Orange', 'Grape', 'Strawberry', 'Pineapple'];

FAutocomplete.builder(
  hint: 'Type to search fruits',
  filter: (query) async {
    await Future.delayed(const Duration(seconds: 3));
    return query.isEmpty ? fruits : fruits.where((fruit) => fruit.toLowerCase().startsWith(query.toLowerCase()));
  },
  contentBuilder: (context, query, values) => [
    for (final fruit in values) FAutocompleteItem(value: fruit),
  ],
);

Async with Custom Loading

const fruits = ['Apple', 'Banana', 'Orange', 'Grape', 'Strawberry', 'Pineapple'];

FAutocomplete.builder(
  hint: 'Type to search fruits',
  filter: (query) async {
    await Future.delayed(const Duration(seconds: 3));
    return query.isEmpty ? fruits : fruits.where((fruit) => fruit.toLowerCase().startsWith(query.toLowerCase()));
  },
  contentBuilder: (context, query, suggestions) => [
    for (final suggestion in suggestions) FAutocompleteItem(value: suggestion),
  ],
  contentLoadingBuilder: (context, style) => Padding(
    padding: const EdgeInsets.all(14.0),
    child: Text('Here be dragons...', style: style.emptyTextStyle),
  ),
);

Async with Custom Error Handling

FAutocomplete.builder(
  hint: 'Type to search fruits',
  filter: (query) async {
    await Future.delayed(const Duration(seconds: 3));
    throw StateError('Error loading data');
  },
  contentBuilder: (context, query, values) => [for (final fruit in values) FAutocompleteItem(value: fruit)],
  contentErrorBuilder: (context, error, trace) => Padding(
    padding: const EdgeInsets.all(14.0),
    child: Icon(FIcons.circleX, size: 15, color: context.theme.colors.primary),
  ),
);

Clearable

FAutocomplete(
  hint: 'Type to search fruits',
  clearable: (value) => value.text.isNotEmpty,
  items: const ['Apple', 'Banana', 'Orange', 'Grape', 'Strawberry', 'Pineapple'],
);

Form

class FormAutocompletePage extends StatefulWidget {
  @override
  State<FormAutocompletePage> createState() => _FormAutocompletePageState();
}

class _FormAutocompletePageState extends State<FormAutocompletePage> {
  final _key = GlobalKey<FormState>();

  @override
  Widget build(BuildContext context) => Form(
    key: _key,
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        FAutocomplete(
          label: const Text('Department'),
          description: const Text('Type to search your dream department'),
          hint: 'Search departments',
          validator: (department) => department == null || department.isEmpty ? 'Please select a department' : null,
          items: const ['Engineering', 'Marketing', 'Sales', 'Human Resources', 'Finance'],
        ),
        const SizedBox(height: 25),
        FButton(
          child: const Text('Submit'),
          onPress: () {
            if (_key.currentState!.validate()) {
              // Form is valid, do something with department.
            }
          },
        ),
      ],
    ),
  );
}

On this page