Elmでかんたん画像アップロード

## はじめに

Elmでやってみた系の記事です！

[elm/file](https://package.elm-lang.org/packages/elm/file/1.0.4/) を使って画像アップロード機能を作りました。
いろいろ試して最終的には [こんなやつ](https://qnoyxu.github.io/elm-upload-images/) ができました。

公式だけでは使いかたが分からないという珍しい方にはぜひ読んでいただきたい内容になっています！



## ファイルを選択して画像アップロード

ボタンを押すとダイアログが出てきてファイルを選ぶタイプのやつを作ってみます。

elm packages の [elm/file](https://package.elm-lang.org/packages/elm/file/1.0.4/) に `Upload Example` があります。CSVをアップロードして表示するプログラムです。中身はソースコードを見れば理解できると思います。

まずはこれをいじって画像アップロード用にしましょう。

### 手順

まずは、アップロード時に指定するファイル形式を`"image/png", "image/jpeg", "image/gif"` あたりに変更します。

次に、上げた画像を表示させてあげます。次の2つをやればOKです。
* `File.toString` ではなく `File.toUrl` を使用
* 取得したURLを `<img>` タグの `src` に指定

```elm

        ImageSelected file ->
            ( model
            , Task.attempt ImageLoaded <| File.toUrl file
            )
```

```elm

view : Model -> Html Msg
view model =
    case model.image of
        Nothing ->
            button [ onClick ImageRequested ] [ text "Upload image" ]

        Just content ->
            img
                [ src content ]
                []

```


エラーハンドリングも追加してみます。

今回は`File.mime`を使って、ファイル形式が指定したものと違ったらエラーにするみたいな処理を書いてみました。

```elm

        ImageSelected file ->
            ( model
            , Task.attempt ImageLoaded
                (guardType file
                    |> Task.andThen File.toUrl
                )
            )
...

expectedTypes : List String
expectedTypes =
    [ "image/png", "image/jpeg", "image/gif" ]


guardType : File -> Task.Task LoadErr File
guardType file =
    if List.any ((==) <| File.mime file) expectedTypes then
        Task.succeed file

    else
        Task.fail ErrInvalidFile

```

これで終わりです！

## 複数のファイルアップロードに対応

`File.Select.file` のかわりに `File.Select.files` を使用します。

```elm

  Select.files expectedTypes (\f fs -> ImageSelected (f :: fs))
```
`File.Select.files` は一見不思議な型注釈になっていますが、「ファイルが1個もアップロードされないパターン」を考えなくて良いように設計されてるんですね。すてき！

あとはコンパイラに従っていれば何も考えずにできちゃいます。すてき！

## ドラッグ＆ドロップでアップロード

ファイル選択ダイアログでしかアップロードできないのは古風すぎるので、ドラッグ＆ドロップでやるやつも追加してみます。

elm packages の [File.decoder](https://package.elm-lang.org/packages/elm/file/1.0.4/File#decoder) にヒントが書いてあります。

### 手順

まずは「ここにドロップしてくださいね」的な領域を用意します。

ドロップされた時(`drop`)に、ドロップされたファイルをデコードしたあと `ImageSelected` (ファイル選択ダイアログでファイルを選択した時に呼んだメッセージ)を渡してやるようにします。

```elm

dropArea : Html msg
dropArea =
    let
        decoder =
            Decode.map
                ImageSelected
                filesDecoder
    in
    div
        [ class "dropArea"
        , Events.on "drop" decoder
        ]
        [ text "Drag&Drop here" ]


filesDecoder : Decoder (List File)
filesDecoder =
    Decode.field "dataTransfer" (Decode.field "files" (Decode.list File.decoder))
```

`filesDecoder` は [File.decoderのサンプル](https://package.elm-lang.org/packages/elm/file/1.0.4/File#decoder) そのまんまです。ドロップされたファイルたちは [DataTransferオブジェクト](https://developer.mozilla.org/ja/docs/Web/API/DataTransfer#files) の`files`に入っています。

これでドロップ時の処理はできましたが、このままではブラウザが意図を汲んでくれずにファイルを開いちゃうので、以下の処置を施します。

* 一番根っこの要素が画面全体を覆うようにレイアウトを整えます。
* 一番根っこの要素のドラッグ時(`dragover`)とドロップ時それぞれで [preventDefaultを呼ぶ](https://package.elm-lang.org/packages/elm/html/1.0.0/Html-Events#preventDefaultOn) ようにします。

ドラッグ時には何らかのエフェクトを加えるのが普通なので、ドラッグが外れた時 (`dragleave`) のスタイル変更も考慮して、たとえばこんな感じにします。

```elm

view : Model -> Html Msg
view model =
    div
        [ Layout.fullHeight
        , Layout.fullWidth
        , Events.preventDefaultOn "dragover" <|
            Decode.succeed ( ChangeStatus Dragover, True )
        , Events.preventDefaultOn "drop" <|
            Decode.succeed ( ChangeStatus Default, True )
        , Events.on "dragleave" <|
            Decode.succeed (ChangeStatus Default)
        ]
        [ ...
```

あとはスタイルを適切に整えれば完成です！！



## ソースコード

ここまでのものを全部盛りしたソースコードです。
https://github.com/qnoyxu/elm-upload-images

(ファイルの構成とかは@arowMさんの [こちら](https://github.com/arowM/elm-atomic-design-example) とかを参考にさせてもらっています！)



## おわりに

~~本題と無関係っぽいですが、上のソースだとFireFoxで開いたとき`dragover`のイベント発生が遅れる不具合が起きちゃってます…~~

~~情報提供してくださった方の記事にいいねしまくります！~~

追記：解決してないけどプログラムじゃなくて環境の問題であることが分かりました。