Edited at

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


はじめに

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

elm/file を使って画像アップロード機能を作りました。

いろいろ試して最終的には こんなやつ ができました。

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


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

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

elm packages の elm/fileUpload Example があります。CSVをアップロードして表示するプログラムです。中身はソースコードを見れば理解できると思います。

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


手順

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

次に、上げた画像を表示させてあげます。次の2つをやればOKです。

* File.toString ではなく File.toUrl を使用

* 取得したURLを <img> タグの src に指定


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


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

Just content ->
img
[ src content ]
[]

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

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


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 を使用します。


Select.files expectedTypes (\f fs -> ImageSelected (f :: fs))

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

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


ドラッグ&ドロップでアップロード

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

elm packages の File.decoder にヒントが書いてあります。


手順

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

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


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))

filesDecoderFile.decoderのサンプル そのまんまです。ドロップされたファイルたちは DataTransferオブジェクトfilesに入っています。

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


  • 一番根っこの要素が画面全体を覆うようにレイアウトを整えます。

  • 一番根っこの要素のドラッグ時(dragover)とドロップ時それぞれで preventDefaultを呼ぶ ようにします。

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


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さんの こちら とかを参考にさせてもらっています!)


おわりに

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

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

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