🤪

タグ入力の自作をする

2024/08/06に公開

tagを自作してみた

Flutterの入力フォームで、タグ機能の自作をしようとライブラリの選定をしていたが、自分たちの考えているものと違うものしかなかったので、実験で自作してみた。

こんなものを作った📺
https://youtu.be/si_i8FYiMTg

しかし困ったことに、そのままリストを渡すと、Tagのインスタンスを渡しているようで、Swiftとかテキストを渡せていなかった😨

タイトルだけ欲しいので抽出する必要があった。こんな感じでロジックを書いてみた。Listの操作が複雑だったので、Dartのロジックの理解が必要でしたね😅

import 'package:flutter/material.dart';

class TagWidget extends StatelessWidget {
  const TagWidget({super.key});

  
  Widget build(BuildContext context) {
    return CustomTagInput(
      initialTags: [
        TagData('Flutter', Colors.blue),
        TagData('Dart', Colors.teal),
      ],
      suggestedTags: [
        TagData('Swift', Colors.orange),
        TagData('Kotlin', Colors.purple),
        TagData('React', Colors.blue),
        TagData('AWS', Colors.yellow),
        TagData('Rails', Colors.red),
        TagData('Sigma', Colors.green),
        TagData('AdobeXD', Colors.pink),
      ],
    );
  }
}

// データモデル
class TagData {
  final String tag;
  final Color color;

  TagData(this.tag, this.color);
}

class CustomTagInput extends StatefulWidget {
  final List<TagData> initialTags;
  final List<TagData> suggestedTags;

  const CustomTagInput({
    Key? key,
    this.initialTags = const <TagData>[], // この行を修正
    this.suggestedTags = const <TagData>[], // この行を修正
  }) : super(key: key);

  
  _CustomTagInputState createState() => _CustomTagInputState();
}

class _CustomTagInputState extends State<CustomTagInput> {
  final TextEditingController _textEditingController = TextEditingController();
  final FocusNode _focusNode = FocusNode();
  List<TagData> _tags = [];

  
  void initState() {
    super.initState();
    _tags = List<TagData>.from(widget.initialTags);
  }

  void _addTag(String tagText) {
    if (tagText.isNotEmpty && !_tags.any((tag) => tag.tag == tagText)) {
      setState(() {
        _tags.add(TagData(tagText, Colors.grey)); // デフォルトの色を設定
      });
      _textEditingController.clear();
    }
  }

  void _removeTag(TagData tag) {
    setState(() {
      _tags.remove(tag);
    });
  }

  void _onSuggestedTagTap(TagData tag) {
    if (!_tags.contains(tag)) {
      setState(() {
        _tags.add(tag);
      });
    }
  }

    
  void dispose() {
    _textEditingController.dispose();
    _focusNode.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Wrap(
          spacing: 8,
          runSpacing: 4,
          children: _tags
              .map((tag) => Chip(
                    label: Text(tag.tag),
                    backgroundColor: tag.color,
                    onDeleted: () => _removeTag(tag),
                  ))
              .toList(),
        ),
        TextField(
          controller: _textEditingController,
          focusNode: _focusNode,
          decoration: const InputDecoration(
            hintText: 'タグを入力してください',
            helperText: 'スペースまたはカンマで区切ってタグを追加',
          ),
          onSubmitted: (value) => _addTag(value.trim()),
          onChanged: (value) {
            if (value.endsWith(' ') || value.endsWith(',')) {
              _addTag(value.trim().replaceAll(',', ''));
            }
          },
        ),
        SizedBox(height: 10),
        Wrap(
          spacing: 8,
          runSpacing: 4,
          children: widget.suggestedTags
              .map((tag) => ActionChip(
                    label: Text(tag.tag),
                    backgroundColor: tag.color,
                    onPressed: () => _onSuggestedTagTap(tag),
                  ))
              .toList(),
        ),
        ElevatedButton(
          onPressed: () {
            // タグのタイトルだけを抽出して保存
            final tagTitles = _tags.map((tag) => tag.tag).toList();
            print(tagTitles);
            print(tagTitles.runtimeType);
          },
          child: const Text('Submit'),
        ),
      ],
    );
  }
}

実行するコードはこちら

import 'package:flutter/material.dart';
import 'package:widget_cookbook/tag_widget/tag_widget.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Demo',
      home: const MyWidget(),
    );
  }
}

class MyWidget extends StatelessWidget {
  const MyWidget({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('My Widget'),
      ),
      body: const Center(
        child: TagWidget(),
      ),
    );
  }
}

まとめ

この機能を使うかはわからないですが、参考にはなりましたね。いい感じの機能かもですね。

おまけ

flutter_hooksで書いてみたかったので、試しにやってみました。良いコードかわからないが動いて入る。皆さんもいろんなパターンで、タグを使った機能を自作してみてください。

タグを生成するWidget

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

class TagWidget extends HookWidget {
  const TagWidget({super.key});

  
  Widget build(BuildContext context) {
    final initialTags = useMemoized(() => [
      TagData('Flutter', Colors.blue),
      TagData('Dart', Colors.teal),
    ], []);

    final suggestedTags = useMemoized(() => [
      TagData('Swift', Colors.orange),
      TagData('Kotlin', Colors.purple),
      TagData('React', Colors.blue),
      TagData('AWS', Colors.yellow),
      TagData('Rails', Colors.red),
      TagData('Sigma', Colors.green),
      TagData('AdobeXD', Colors.pink),
    ], []);

    return CustomTagInput(
      initialTags: initialTags,
      suggestedTags: suggestedTags,
    );
  }
}

class TagData {

  TagData(this.tag, this.color);
  final String tag;
  final Color color;
}

class CustomTagInput extends HookWidget {

  const CustomTagInput({
    super.key,
    this.initialTags = const <TagData>[],
    this.suggestedTags = const <TagData>[],
  });
  final List<TagData> initialTags;
  final List<TagData> suggestedTags;

  
  Widget build(BuildContext context) {
    final tags = useState<List<TagData>>(initialTags);
    final textEditingController = useTextEditingController();
    final focusNode = useFocusNode();

    final addTag = useCallback((String tagText) {
      if (tagText.isNotEmpty && !tags.value.any((tag) => tag.tag == tagText)) {
        tags.value = [...tags.value, TagData(tagText, Colors.grey)];
        textEditingController.clear();
      }
    }, [tags, textEditingController]);

    final removeTag = useCallback((TagData tag) {
      tags.value = tags.value.where((t) => t != tag).toList();
    }, [tags]);

    final onSuggestedTagTap = useCallback((TagData tag) {
      if (!tags.value.contains(tag)) {
        tags.value = [...tags.value, tag];
      }
    }, [tags]);

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Wrap(
          spacing: 8,
          runSpacing: 4,
          children: tags.value.map((tag) => Chip(
            label: Text(tag.tag),
            backgroundColor: tag.color,
            onDeleted: () => removeTag(tag),
          )).toList(),
        ),
        TextField(
          controller: textEditingController,
          focusNode: focusNode,
          decoration: const InputDecoration(
            hintText: 'タグを入力してください',
            helperText: 'スペースまたはカンマで区切ってタグを追加',
          ),
          onSubmitted: (value) => addTag(value.trim()),
          onChanged: (value) {
            if (value.endsWith(' ') || value.endsWith(',')) {
              addTag(value.trim().replaceAll(',', ''));
            }
          },
        ),
        const SizedBox(height: 10),
        Wrap(
          spacing: 8,
          runSpacing: 4,
          children: suggestedTags.map((tag) => ActionChip(
            label: Text(tag.tag),
            backgroundColor: tag.color,
            onPressed: () => onSuggestedTagTap(tag),
          )).toList(),
        ),
        const SizedBox(height: 10),
        ElevatedButton(onPressed: () {
          print(tags.value.map((tag) => tag.tag).toList());
        }, child: const Text('Submit')),
      ],
    );
  }
}

実行するコード

import 'package:flutter/material.dart';
import 'package:widget_cookbook/tag_hook/tag_widget.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Demo',
      home: const MyWidget(),
    );
  }
}

class MyWidget extends StatelessWidget {
  const MyWidget({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Hook Tag Widget'),
      ),
      body: const Center(
        child: TagWidget(),
      ),
    );
  }
}

Discussion