これはElm 2 Advent Calendar 2017の23日目の記事です。
昨日はpastelIncさんの「Elmでもテストを書こう」でした。
以前、HTML5 File API を使用してドラッグ&ドロップでファイルをアップロードする処理を書いたことがあり、
Elmで再現するのにはどうすればいいんだろう、と試そうとしたのが出発点。
が、現状Elmではサポートしておらず、JavaScriptとの連携が必要になったのでそれで実現してみた、というお話。
参考: HTML5 Drag and Drop in Elm
昨日はpastelIncさんの「Elmでもテストを書こう」でした。
以前、HTML5 File API を使用してドラッグ&ドロップでファイルをアップロードする処理を書いたことがあり、
Elmで再現するのにはどうすればいいんだろう、と試そうとしたのが出発点。
が、現状Elmではサポートしておらず、JavaScriptとの連携が必要になったのでそれで実現してみた、というお話。
参考: HTML5 Drag and Drop in Elm
CoffeeScript版
以前CoffeeScriptで書いたのはこのようなコード(一部抜粋・改変)。
onDragOver = (f = ()->) ->
(event) ->
event.preventDefault()
return
onDrop = (f = ()->) ->
(event) ->
e = event
if event.originalEvent
e = event.originalEvent
e.preventDefault()
files = e.dataTransfer.files
if files.length == 1
# 実際には一旦 「files[0].name + 'をアップロードします。'」を表示したモーダルを表示し、
# 「はい」をクリックすると以下のpostを行うようにしていた。
formData = new FormData
formData.append 'file', files[0]
$.post
dataType: 'text'
contentType: false
processData: false
url: '/file'
data: formData
success: (data) ->
id = JSON.parse(data).id
location.href = '/file/' + id
return
$('#drag-drop-area').ondrop = onDrop()
$('#drag-drop-area').on 'drop', onDrop()
$('#drag-drop-area').ondragover = onDragOver()
$('#drag-drop-area').on 'dragover', onDragOver()
ブラウザがそのファイルを開いてしまわないようにevent.preventDefault()を呼ぶ。event.originalEventを取り出しているのはjQueryでの都合なので、今回は不要。参考: jQuery Event から DOM Event を取る
ドラッグ&ドロップを検知
まずは、ドラッグとドロップにイベントを付けてみる。これはElmだけで実現できる。
- elm-package.json (
"native-modules": trueは次の節から必要になるので先に書いておいたがまだ不要)
{ "version": "0.0.1", "summary": "file drag drop example.", "repository": "https://github.com/zaneli/drag-drop.git", "license": "NYSL", "source-directories": [ "src" ], "exposed-modules": [], "dependencies": { "elm-lang/core": "5.1.1 <= v < 6.0.0", "elm-lang/html": "2.0.0 <= v < 3.0.0" }, "native-modules": true, "elm-version": "0.18.0 <= v < 0.19.0" }src/App.elm
module App exposing (..)
import Html exposing (div, program, span, text, Attribute, Html)
import Html.Attributes exposing (class, id, style)
import Html.Events exposing (onWithOptions)
import Json.Decode as JD
import Native.FileAPI
main : Program Never Model Action
main =
program
{ init = init
, update = update
, view = view
, subscriptions = subscriptions
}
type alias Model =
{ name : String }
init : ( Model, Cmd Action )
init =
( { name = "" }, Cmd.none )
subscriptions : Model -> Sub Action
subscriptions model =
Sub.none
type Action
= Upload
| Noop
update : Action -> Model -> ( Model, Cmd Action )
update msg model =
case msg of
Upload -> ({model | name = "ファイルをドラッグドロップしました。"}, Cmd.none)
Noop -> (model, Cmd.none)
view : Model -> Html Action
view model =
div []
[ div
[ id "drag-drop-area", style dragDropArea, onDragOver, onDrop ]
[ text "ファイルをドラッグドロップしてください。" ]
, span [ class "file-name" ] [ text model.name ]
]
onDragOver : Attribute Action
onDragOver =
onWithOptions
"dragover"
{ preventDefault = True
, stopPropagation = False
}
(JD.succeed Noop)
onDrop : Attribute Action
onDrop =
onWithOptions
"drop"
{ preventDefault = True
, stopPropagation = False
}
(JD.succeed Upload)
dragDropArea : List (String, String)
dragDropArea =
[ ("width", "80%")
, ("height", "250px")
, ("border", "solid 1px #000")
, ("margin", "10px")
]
ファイルをドラッグ&ドロップすると「ファイルをドラッグドロップしました。」とだけ表示される。しかし、どのようなファイルかという情報は取れない。
event.dataTransferに触るために、そこだけ生のJavaScriptを書いてElmから呼べるようにしてみる。native moduleを書いてElmから呼び出す
native moduleの書き方はElm0.17から変更されたらしく、今回使った0.18でも新しい書き方に合わせる。参考: Elm 0.17~0.18版 NativeModuleの書き方
-
src/App.elm
module App exposing (..)
import Html exposing (div, program, span, text, Attribute, Html)
import Html.Attributes exposing (class, id, style)
import Html.Events exposing (onWithOptions)
import Json.Decode as JD
import Native.FileAPI
main : Program Never Model Action
main =
program
{ init = init
, update = update
, view = view
, subscriptions = subscriptions
}
type alias Model =
{ name : String, isReady: Bool }
type Action =
Noop
init : ( Model, Cmd Action )
init =
( { name = "", isReady = False }, Cmd.none )
subscriptions : Model -> Sub Action
subscriptions model =
Sub.none
update : Action -> Model -> ( Model, Cmd Action )
update msg model =
let m = if model.isReady then model else addOnDropEvent model
in case msg of
Noop ->
(m, Cmd.none)
view : Model -> Html Action
view model =
div []
[ div
[ id "drag-drop-area", style dragDropArea, onDragOver, onDrop ]
[ text "ファイルをドラッグドロップしてください。" ]
, span
[ class "file-name" ] [ text model.name ]
]
onDragOver : Attribute Action
onDragOver =
onWithOptions
"dragover"
{ preventDefault = True
, stopPropagation = False
}
(JD.succeed Noop)
onDrop : Attribute Action
onDrop =
onWithOptions
"drop"
{ preventDefault = True
, stopPropagation = False
}
(JD.succeed Noop)
addOnDropEvent : Model -> Model
addOnDropEvent model =
Native.FileAPI.addOnDropEvent
"drag-drop-area"
{ preventDefault = True
, stopPropagation = False
}
model
dragDropArea : List (String, String)
dragDropArea =
[ ("width", "80%")
, ("height", "250px")
, ("border", "solid 1px #000")
, ("margin", "10px")
]
src/Native/FileAPI.js
var _zaneli$drag_drop$Native_FileAPI = function() {
function addOnDropEvent(name, options, model) {
var tag = document.getElementById(name);
tag.addEventListener("drop", function(event) {
if (options.preventDefault) {
event.preventDefault();
}
if (options.stopPropagation) {
event.stopPropagation();
}
var files = event.dataTransfer.files;
if (files.length == 1) {
model.name = files[0].name + "をドラッグドロップしました。";
}
});
model.isReady = true;
return model;
}
return {
addOnDropEvent: F3(addOnDropEvent)
}
}();
どこでNative.FileAPI.addOnDropEventを呼ぶかというのに悩んで、最初はinitでやろうとしていたが、viewで作られるDOMの作成前になるようでdocument.getElementById(name);でElementが取得できず、modelにフラグを持たせてupdateの最初の一回のみ呼ぶようにしてみた…が無理やり感がある…。また、App.elm側に書いている
onDropはなくてもよくなるかと思ったが、ドロップのタイミングで
updateが呼ばれなくなり、「<ファイル名>をドラッグドロップしました。」の表示が更新されるタイミングが
次回の
onDragOverになってしまったので残している。ゴリ押しで何とか動くものにしてはみたが、あまりElmでやるのに適した題材ではなかったかもしれない。
ファイルのアップロードはよく議題に登る。との事なので、今後Elmだけで完結するようになる可能性もあるのだろうか?
参考: [Elm] Web API サポート状況
明日はarowMさんの「サーバーサイドの情報をHTMLに埋め込んでElmに渡す」です。