Svelte/SvelteKitのbind:filesに潜むバグたち

<input type="file">要素を手なずける

  • Svelte
  • SvelteKit

投稿日:

もくじ

はじめに

SvelteKitで<input type="file">要素を扱おうとしたところ、複数のバグに遭遇した。

いずれもGitHubでissueが立っているが、発見するまでにかなり時間がかかったので記録に残しておく。

bind:fileson:change属性の位置によるバグ

<input>要素の属性内でbind:filesを記述する位置によって、値が正常にバインドされなくなるバグがある。sveltejs/svelteリポジトリ上にある類似のissueは「解決済み」としてマークされているが、今もon:change属性などと組み合わせた際に同様の症状が出ることがあるようだ。

以下に変更前後のコードを掲載する。

<!--変更前(正常に動作しない)-->
<input type="file" accept="image/png,image/jpeg,image/webp" bind:files={files} on:change={callBackFunction} />

<!--変更後-->
<input type="file" bind:files={files} on:change={callBackFunction} accept="image/png,image/jpeg,image/webp" />

ファイルのバインド先変数とDOM要素の不整合

(2023/11/21追記)続報がないかとGitHubを確認していたところ、この問題を解決するとされるPRがマージされていた。このバグが発生しているのであれば、Svelteを更新することで解決するかもしれない

bind:filesでバインドした変数にnullundefinedを代入することで値をクリアしようとすると変数の状態とDOM上の状態に不整合が発生してしまう。これによってファイルAを入力→バインド先変数にnullを代入→再びファイルAを入力という操作を行った際にバインド先の変数がnullだったにも関わらず、on:change属性のハンドラが呼ばれないという直感に反する挙動が起きることになる。

この不具合に言及しているissueに付いたコメントにもある通り、HTMLの仕様ではHTMLInputElement.filesに空値を代入する操作は無視される。そのため正常に値をクリアするには要素のfilesプロパティとバインド先の変数の両方に(new DataTransfer()).filesを代入しなければならない。

バグの再現方法や解決法については当該issueの作成者によるREPLが詳しいが、一応サンプルコードを掲載しておく。

<script>
    let inputElement
    let files

    // inputElement.filesは変更されないままfilesのみがnullとなり、不整合が発生する
    function clearFilesInTheWrongWay() {
        inputElements.files = null
        files = null
    }

    // 正しく値をクリアできる
    function clearFilesInTheRightWay() {
        inputElements.files = (new DataTransfer()).files
        files = (new DataTransfer()).files
    }
</script>

<!--"bind:files"は"bind:files={files}"の省略形-->
<input type="file" bind:files bind:this={inputElement}>

おわりに

ただでさえユーザーの少ないSvelteのエッジケースを踏み抜いてしまい大変な目に遭ったが、大量の英語記事とissueを読み漁ることでなんとか解決できた。

本稿によって同じ現象に苦しむ人をひとりでも救えれば幸いである。

著者プロフィール