skip to Main Content

img

I want to allow users to input tags or chips directly into a TextField, similar to how email addresses are displayed in email input fields on websites. The goal is to have chips appear in line with the text, and users should be able to add, edit, or remove these chips easily.

2

Answers


  1. As you have not provided the question with your own work or code, so I am just pointing you to the package that can help you with what you want.

    Please check the following package and it’s implementation:

    https://pub.dev/packages/textfield_tags

    Login or Signup to reply.
  2. There is an example of doing this exact thing in the Flutter docs under the InputChip class:

    class ChipsInput<T> extends StatefulWidget {
      const ChipsInput({
        super.key,
        required this.values,
        this.decoration = const InputDecoration(),
        this.style,
        this.strutStyle,
        required this.chipBuilder,
        required this.onChanged,
        this.onChipTapped,
        this.onSubmitted,
        this.onTextChanged,
      });
    
      final List<T> values;
      final InputDecoration decoration;
      final TextStyle? style;
      final StrutStyle? strutStyle;
    
      final ValueChanged<List<T>> onChanged;
      final ValueChanged<T>? onChipTapped;
      final ValueChanged<String>? onSubmitted;
      final ValueChanged<String>? onTextChanged;
    
      final Widget Function(BuildContext context, T data) chipBuilder;
    
      @override
      ChipsInputState<T> createState() => ChipsInputState<T>();
    }
    
    class ChipsInputState<T> extends State<ChipsInput<T>> {
      @visibleForTesting
      late final ChipsInputEditingController<T> controller;
    
      String _previousText = '';
      TextSelection? _previousSelection;
    
      @override
      void initState() {
        super.initState();
    
        controller = ChipsInputEditingController<T>(
          <T>[...widget.values],
          widget.chipBuilder,
        );
        controller.addListener(_textListener);
      }
    
      @override
      void dispose() {
        controller.removeListener(_textListener);
        controller.dispose();
    
        super.dispose();
      }
    
      void _textListener() {
        final String currentText = controller.text;
    
        if (_previousSelection != null) {
          final int currentNumber = countReplacements(currentText);
          final int previousNumber = countReplacements(_previousText);
    
          final int cursorEnd = _previousSelection!.extentOffset;
          final int cursorStart = _previousSelection!.baseOffset;
    
          final List<T> values = <T>[...widget.values];
    
          // If the current number and the previous number of replacements are different, then
          // the user has deleted the InputChip using the keyboard. In this case, we trigger
          // the onChanged callback. We need to be sure also that the current number of
          // replacements is different from the input chip to avoid double-deletion.
          if (currentNumber < previousNumber && currentNumber != values.length) {
            if (cursorStart == cursorEnd) {
              values.removeRange(cursorStart - 1, cursorEnd);
            } else {
              if (cursorStart > cursorEnd) {
                values.removeRange(cursorEnd, cursorStart);
              } else {
                values.removeRange(cursorStart, cursorEnd);
              }
            }
            widget.onChanged(values);
          }
        }
    
        _previousText = currentText;
        _previousSelection = controller.selection;
      }
    
      static int countReplacements(String text) {
        return text.codeUnits
            .where(
                (int u) => u == ChipsInputEditingController.kObjectReplacementChar)
            .length;
      }
    
      @override
      Widget build(BuildContext context) {
        controller.updateValues(<T>[...widget.values]);
    
        return TextField(
          minLines: 1,
          maxLines: 3,
          textInputAction: TextInputAction.done,
          style: widget.style,
          strutStyle: widget.strutStyle,
          controller: controller,
          onChanged: (String value) =>
              widget.onTextChanged?.call(controller.textWithoutReplacements),
          onSubmitted: (String value) =>
              widget.onSubmitted?.call(controller.textWithoutReplacements),
        );
      }
    }
    
    class ChipsInputEditingController<T> extends TextEditingController {
      ChipsInputEditingController(this.values, this.chipBuilder)
          : super(
              text: String.fromCharCode(kObjectReplacementChar) * values.length,
            );
    
      // This constant character acts as a placeholder in the TextField text value.
      // There will be one character for each of the InputChip displayed.
      static const int kObjectReplacementChar = 0xFFFE;
    
      List<T> values;
    
      final Widget Function(BuildContext context, T data) chipBuilder;
    
      /// Called whenever chip is either added or removed
      /// from the outside the context of the text field.
      void updateValues(List<T> values) {
        if (values.length != this.values.length) {
          final String char = String.fromCharCode(kObjectReplacementChar);
          final int length = values.length;
          value = TextEditingValue(
            text: char * length,
            selection: TextSelection.collapsed(offset: length),
          );
          this.values = values;
        }
      }
    
      String get textWithoutReplacements {
        final String char = String.fromCharCode(kObjectReplacementChar);
        return text.replaceAll(RegExp(char), '');
      }
    
      String get textWithReplacements => text;
    
      @override
      TextSpan buildTextSpan(
          {required BuildContext context,
          TextStyle? style,
          required bool withComposing}) {
        final Iterable<WidgetSpan> chipWidgets =
            values.map((T v) => WidgetSpan(child: chipBuilder(context, v)));
    
        return TextSpan(
          style: style,
          children: <InlineSpan>[
            ...chipWidgets,
            if (textWithoutReplacements.isNotEmpty)
              TextSpan(text: textWithoutReplacements)
          ],
        );
      }
    }
    

    Source: https://api.flutter.dev/flutter/material/InputChip-class.html

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search