これは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に渡す」です。