Svelte Material UIでComboBoxを実装する

# 背景
最近Svelteを使って開発をしています。
モダンな技術なので日本の記事が結構少なかったりして苦戦することも多いのでナレッジの共有と自分のポートフォリオの一環としてこれからSvelteやその他諸々の記事を書こうと思います。
# なぜSvelte Material UIなのか
- [GitHub](https://github.com/hperrin/svelte-material-ui)のStar数も3.3k（2024/12/04 時点）とSvelte界隈では多め
- 安心安定のComponentの数
- Reactでいろいろなデザインライブラリ使ったけど結局material-uiが一番だった

以上の観点からSvelteでも利用することにしました

## 実装
[svelte-material-ui](https://sveltematerialui.com/) のComponent一覧を見てもComboBoxがない…
プルダウンを実装していましたが、ComboBoxがほしいと言われたので実装
このためだけに、デザインライブラリを増やすのはイケてないのでComponentとして作成してどこからでも使えるようにと実装をしました
細かな実装はSvelte始めて2ヶ月弱なので目を瞑ってください

基本的にはsvelte-material-uiの「TextField」と「Menu」を組み合わせています

### ポイント
- Component分割した際に親Componentに選択した値がわたる様になっている
- ``{ id: '値に紐づくID', text: '表示したい値' }``を配列で渡すことで結果をIDをキーに検索できたりする
- 子で使う想定なので汎用的に作った

```svelte: ComboBox.svelte
<script lang='ts'>
  import { createEventDispatcher } from 'svelte';
  
  import List, { Item } from '@smui/list';
  import Menu from '@smui/menu';
  import TextField from '@smui/textfield';
  
  import ChevronDown from 'svelte-material-icons/ChevronDown.svelte';

  type Value = {
    id: string,
    text: string
  };

  // props
  export let values: Value[] = []; // ドロップダウンリスト内で表示したい値
  export let selectedValue: string = values[0].text; // 選択した値（初期値は先頭の値を入れてる）

  const dispatch = createEventDispatcher();

  let searchText = selectedValue;
  let menu: Menu;
  let filteredValues = values; // 検索した値でフィルタリングされた一覧
  
  $: { // 入力された値が大文字、小文字区別せずリアルタイムで検索してドロップダウンリストを変更
    filteredValues = values.filter((value) =>
      value.text.toLowerCase().includes(searchText.toLowerCase())
    );
  }

  const selectOption = (e: CustomEvent, option:Value) => {
    selectedValue = option.text;
    searchText = option.text; // 入力欄に選択した値を反映
    const target = e.target as HTMLElement;
    dispatch('changeValue', { text: target.textContent, id: target.id }); // 親コンポーネントに流す値。今回はドロップダウンリスト内の文字と、その値に紐づくIDを流している。
    menu.setOpen(false);
  };
</script>

<div>
　<!-- TextBox押下でComboBoxが開いて、フォーカスが外れると閉じる -->
  <TextField
    bind:value={searchText}
    on:click={() => menu.setOpen(true)}
    style='height:40px; padding-right: 30px;'
    variant='outlined'
  >
    <!-- Styleでいい感じにに矢印を固定してる（無理やり） -->
    <div class='material-icons'>
      <ChevronDown />
    </div>
  </TextField>
  <!-- StyleでTextBoxの下にメニューを表示している -->
  <Menu
    bind:this={menu}
    style='top: 40px; position: absolute; z-index: 10; width: 100%;'
  >
    <List>
      {#if filteredValues.length}
        {#each filteredValues as v}
          <!-- Itemにidを付与することでEvent内でIDが拾える -->
          <Item
            on:click={(e) => selectOption(e, v)}
            value={v.text}
            style='cursor: pointer; padding: 8px;'
            id={v.id}
          >
            {v.text}
          </Item>
        {/each}
      {:else}
        <Item>{'検索結果無し'}</Item>
      {/if}
    </List>
  </Menu>
</div>

<style>
  .material-icons {
    position: absolute;
    right: 3px;
    top: -3px;
  }
</style>
```

```svelte: 親.svelte
<script lang='ts'>
  /** 
   * ドロップダウンリストが選択されるたびに呼ばれる
  */
  const changeValue = (value: CustomEvent) => {
    value.detail.text; // textの取り方
    value.detail.id; // idの取り方
  }
</script>

<CombBox bind:values bind:selectValue on:changeValue={changeValue}/>

```

# 最後に
親子で依存関係を作るとSvelteは個人的には書きづらいなって思います。
これは慣れていないだけなのか、Reactが書きやすいのか…

あとChatGPTに聞いたりもしてたんですが、全然的外れで無駄足でした。
結局は公式ページ読んで、いい感じに組み合わせていくのが一番だなって再度認識。
最近、教える側なのでついつい生成AIに質問しがちですよくない。

かなりコード内にコメント（説明）を書きましたが、不明点などありましたらコメントなりなんなりしてください。