(追記)もうすこし基本的なPortの記事も書きました ==> Portを通したElm Record と JavaScript Object の互換性 - Qiita
一般的に、Elmだけで完全なアプリを作る場合もあれば、JavaScriptプログラムと組み合わせてアプリを作成する場合もあります。Elmに無いライブラリを使いたい場合などは、当然後者を選択します。ElmにはJavaScriptと会話するためのPortという仕組みがあります。今回は以下のサイトに従って勉強しましたが、その備忘録です。ソースコードは多少修正してあります。
http://codeloveandboards.com/blog/2017/05/15/elm-and-the-outer-world/
Elmは一番基本のModel-View-Updateパタンの場合だと、汚れ仕事であるイベントの管理やDOM表示はElm Realtimeが行ってくれます。美しいパタンですが、汚れ仕事はほとんどできません。そこでCmd/Subを導入して、それ以外のWebアプリではよくある汚れ仕事も行えるようになりました。Httpや乱数などです。しかしそれも限定的で、JavaScriptでなければやれないこともあります。Portはこれを解決すべく、任意のJavaScriptプログラムと交信できる仕組みとして導入されたと考えられます。
Elmで乱数を扱う(Cmd) - Qiita
Elmでサーバと通信する(Cmd) - Qiita
Building custom DOM event handlers in Elm
1.開発環境について
今回はElmプログラムからJavaScriptのFileReaderオブジェクトを使う例を考えます。今回のプログラムは3つのファイルから構成されます。3つのプログラムファイルはプロジェクトディレクトリ直下のsrcディレクトリにあるとします。
・index.html -- ここにJavaScriptプログラムを展開します
・Main.elm -- elmのMain moduleです。index.htmlのJavaScriptと会話します
・Ports.elm -- Portsを定義したelm moduleです。
まず必要なパッケージをインストールします。
$ elm-package install elm-lang/http
以下のコマンドでコンパイルします。
$ elm make --warn --debug src/Main.elm --output src/main.js
httpサーバを立ち上げ、ブラウザでアクセスするとアプリが起動します。
$ elm-reactor -a=www.xxxxxx.jp -p=3030
2.プログラムの処理の流れ
大雑把に説明すると以下の流れになります。
画像ファイル名を選択(Elm) --> 画像ファイルの中身を読む(JavaScript) --> 画像を表示(Elm)
もう少し詳細な流れは以下のようになります。
1.ファイル選択 <input type="file">の画面を表示する(Elm)
2.ファイルを選択しイベントを発生させる(Elm)
3.イベント処理で選択されたファイル名をJavaScriptに送る(Elm)
4.ファイル名を受け取り、FileReaderで中身を読み込む(JavaScript)
5.読み込んだファイルデータをElmに送信する(JavaScript)
6.ファイルデータを受け取り、画面に表示する(Elm)
3.port module(src/Ports.elm)
Elmで以下のように名前と型だけの関数を用意し、Cmd/Subとして実行します。
port module Ports exposing (fileSelected, fileLoaded)
import Json.Decode as JD
-- Out ports
port fileSelected : JD.Value -> Cmd msg
-- In ports
port fileLoaded : (String -> msg) -> Sub msg
まずmodule宣言ですが、以下のように先頭にportを付ける必要があります。
port module Ports exposing (fileSelected, fileLoaded)
続けて2つの関数(fileSelected、fileLoaded)の名前と型だけを記述します。これだけを宣言するとElmプログラムとJavaScriptプログラムで以下のように使うことができるようになります。
以下が選択されたファイル名を送るコードです(Elm --> JavaScript)。 ファイル選択のイベントはElm内で起こり、それをJavaScriptに伝えるべくupdate関数でCmdを発行します(1)。するとJavaScript側で対応する関数が起動されます(2)。
1. Cmd.batch [ fileSelected event ] @ Elm
2. app.ports.fileSelected.subscribe(function (e) {...}); @ JavaScript
以下が読み込んだファイルコンテンツを送るコードです(JavaScript --> Elm)。上の2のJavaScriptのcallbackの中でファイルを読み込み、終了したらElmに伝えます(3)。Elm側では(4)で登録済みのSubで受取り、update関数を呼びます。
3. app.ports.fileLoaded.send(base64encoded); @ JavaScript
4. Sub.batch [ fileLoaded FileLoaded ] @ Elm
4.カスタム イベント リスナーの作成
このカスタムイベントの部分は少しわかりにくいので、細かく見ていこうと思います。まずは以下のview関数を見てください。
type Msg
= FileSelected JD.Value
| FileLoaded String
view model =
section
[ sectionStyles ]
[ imageView model.imageData
, input
[ type_ "file"
, accept "image/*"
, on "change" (JD.map FileSelected JD.value)
]
[]
]
注目してほしいのはon関数ですが、これはHtml.Eventsで定義されている関数でイベントリスナーを登録してくれます。まずは型を見てください。
http://package.elm-lang.org/packages/elm-lang/html/latest/Html-Events#on
on : String -> Decoder msg -> Attribute msg
map : (a -> value) -> Decoder a -> Decoder value
イベントが起こった時に、decoderはeventオブジェクトを Elmの値に変換してくれます。その値はupdate関数に送られます。型を考慮して実際のコードをみます。
on "change" (JD.map FileSelected JD.value)
この一行で、(1)JavaScript NativeのDOM eventsオブジェクトの取得、(2)DOM eventsのDecode、(3)Taggingの処理を行っています。
ここで(JD.map FileSelected JD.value)がdecorderになります。
JDはJson.Decodeで、JSON値をElm値に変換してくれるmoduleです。
http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Json-Decode
JD.valueはJavaScriptのJSONオブジェクトをそのままの形でElmの世界に取り入れるための、偽装の変換を行うdecorderです。実際に値そのものの変更は行っていませんが、形だけはElmの値となっています。これは後でportによってそのままJavaScriptに渡されるものです。Elmの世界ではこれ以上この値の中身に触ることはできません。ここではDOM eventオブジェクトをそのままElm値に変換しています。
またJD.valueで偽装変換された値は、タグ(FileSelected)を付けたMsgでupdate関数に渡されます。タグ付けはJD.mapで行なわれます。結論的に、update関数にはFileSelected eventというMsgが送られます。
5.JavaScriptプログラム(src/index.html)
JavaScriptプログラムは以下のようにindex.htmlに埋め込んでいます。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Elm FileReader</title>
</head>
<body>
<div id="elm-area"></div>
<script src="main.js"></script>
<script>
const app = Elm.Main.embed(document.getElementById("elm-area"));
app.ports.fileSelected.subscribe(function (e) {
const input = e.target;
const file = input.files[0];
const reader = new FileReader();
reader.onload = (function (event) {
const base64encoded = event.target.result;
app.ports.fileLoaded.send(base64encoded);
});
reader.onerror = (function (err) {
console.error(err);
});
reader.readAsDataURL(file);
});
</script>
</body>
</html>
Elmアプリもindex.htmlで起動していますが、以下のコードで行っています。
<div id="elm-area"></div>
<script src="main.js"></script>
<script>
const app = Elm.Main.embed(document.getElementById("elm-area"));
上以外はJavaScriptのプログラムになります。まずElm側からファイル名が伝えられます。
eは、Elm側からfileSelected eventというコマンドで渡されたeventオブジェクトで、Elmのカスタムイベント(on "change")で拾ったJavaScriptのeventを、そのまま渡してくれたものです。つまりJavaScriptのeventです。
app.ports.fileSelected.subscribe(function (e) {
次にそのファイルをreadします。
reader.readAsDataURL(file);
readが終わったら、データをElm側に渡します。
reader.onload = (function (event) {
const base64encoded = event.target.result;
app.ports.fileLoaded.send(base64encoded);
});
以上がJavaScriptプログラムの処理になります。
6.Elmプログラム(src/Main.elm)
以下がElmプログラムです。
module Main exposing (..)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (on)
import Json.Decode as JD
import Ports exposing (fileSelected)
import Ports exposing (fileLoaded)
init : ( Model, Cmd Msg )
init =
initialModel ! []
main : Program Never Model Msg
main =
Html.program
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
type Msg
= FileSelected JD.Value
| FileLoaded String
type ImageData e s
= NotLoaded
| Loading
| Error e
| Success s
type alias Model =
{ imageData : ImageData String String }
initialModel : Model
initialModel =
{ imageData = NotLoaded }
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
FileSelected event ->
( { model | imageData = Loading }, Cmd.batch [ fileSelected event ] )
FileLoaded binary ->
( { model | imageData = Success binary }, Cmd.batch [] )
view : Model -> Html Msg
view model =
section
[]
[ imageView model.imageData
, input
[ type_ "file"
, accept "image/*"
, on "change" (JD.map FileSelected JD.value)
]
[]
]
imageView : ImageData String String -> Html Msg
imageView imageData =
case imageData of
NotLoaded ->
p
[]
[ text "Choose an image file using the selector below..." ]
Loading ->
p
[]
[ text "Loading..." ]
Error error ->
p
[]
[ text error ]
Success binary ->
img
[ style
[ ( "max-height", "300px" )
, ( "margin-bottom", "3rem" )
]
, src binary
]
[]
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch [ fileLoaded FileLoaded ]
今回のテーマで、まだ説明していないのはupdateです。以下に説明していきたいと思います。
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
FileSelected event ->
( { model | imageData = Loading }, Cmd.batch [ fileSelected event ] )
FileLoaded binary ->
( { model | imageData = Success binary }, Cmd.batch [] )
まず FileSelected event の場合は前に見た通り、以下のイベントに対応しています。
, on "change" (JD.map FileSelected JD.value)
次に FileLoaded binary の場合は、以下のsubscriptionsに対応しています。
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch [ fileLoaded FileLoaded ]
subscriptionsは、必要なmodel更新を行うべく、任意のイベントソースをリッスンするためのものです。イベントが生じたときにupdateが呼ばれ、modelの更新が行われます。
https://guide.elm-lang.org/architecture/effects/
このsubscriptionsはJavaScript側の、以下の実行をリッスンしています。
app.ports.fileLoaded.send(base64encoded);
今回は以上です。