ElmとJavaScriptの会話(Port)

　（追記）もうすこし基本的なPortの記事も書きました ==> [Portを通したElm Record と JavaScript Object の互換性 - Qiita](https://qiita.com/sand/items/afa678203c31c71580ea)

　一般的に、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](https://qiita.com/sand/items/4efd8eeafd2c33778e08)
[Elmでサーバと通信する(Cmd) - Qiita](https://qiita.com/sand/items/892d96f3c2956da4e68b)
[Building custom DOM event handlers in Elm](https://robots.thoughtbot.com/building-custom-dom-event-handlers-in-elm)





# １．開発環境について

　今回はElmプログラムからJavaScriptのFileReaderオブジェクトを使う例を考えます。今回のプログラムは３つのファイルから構成されます。３つのプログラムファイルはプロジェクトディレクトリ直下のsrcディレクトリにあるとします。
・index.html -- ここにJavaScriptプログラムを展開します
・Main.elm -- elmのMain moduleです。index.htmlのJavaScriptと会話します
・Ports.elm -- Portsを定義したelm moduleです。

　まず必要なパッケージをインストールします。

```sh:
$ elm-package install elm-lang/http
```

　以下のコマンドでコンパイルします。

```sh:
$ elm make --warn --debug src/Main.elm --output src/main.js
```

　httpサーバを立ち上げ、ブラウザでアクセスするとアプリが起動します。

```sh:
$ elm-reactor -a=www.xxxxxx.jp -p=3030
```


# ２．プログラムの処理の流れ

　大雑把に説明すると以下の流れになります。

```
画像ファイル名を選択(Elm)  --> 画像ファイルの中身を読む(JavaScript)  --> 画像を表示(Elm)
```

　もう少し詳細な流れは以下のようになります。

```
１．ファイル選択 ＜input type="file"＞の画面を表示する（Elm）
２．ファイルを選択しイベントを発生させる(Elm)
３．イベント処理で選択されたファイル名をJavaScriptに送る(Elm)
４．ファイル名を受け取り、FileReaderで中身を読み込む(JavaScript)
５．読み込んだファイルデータをElmに送信する(JavaScript)
６．ファイルデータを受け取り、画面に表示する(Elm)
```


# ３．port module(src/Ports.elm)

　Elmで以下のように名前と型だけの関数を用意し、Cmd/Subとして実行します。

```elm:src/Ports.elm
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
```


# ４．カスタム イベント リスナーの作成
　このカスタムイベントの部分は少しわかりにくいので、細かく見ていこうと思います。まずは以下のview関数を見てください。

```elm: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

```elm
on : String -> Decoder msg -> Attribute msg
map : (a -> value) -> Decoder a -> Decoder value
```
　イベントが起こった時に、decoderはeventオブジェクトを Elmの値に変換してくれます。その値はupdate関数に送られます。型を考慮して実際のコードをみます。

```elm
on "change" (JD.map FileSelected JD.value)
```

　この一行で、（１）JavaScript NativeのDOM eventsオブジェクトの取得、（２）DOM eventsのDecode、（３）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が送られます。




# ５．JavaScriptプログラム(src/index.html)
　JavaScriptプログラムは以下のようにindex.htmlに埋め込んでいます。

```html:src/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で起動していますが、以下のコードで行っています。

```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です。

```js
        app.ports.fileSelected.subscribe(function (e) {
```

　次にそのファイルをreadします。

```js
          reader.readAsDataURL(file);
```
　readが終わったら、データをElm側に渡します。

```js
          reader.onload = (function (event) {
            const base64encoded = event.target.result;
            app.ports.fileLoaded.send(base64encoded);
          });
```

　以上がJavaScriptプログラムの処理になります。


# ６．Elmプログラム(src/Main.elm)

　以下がElmプログラムです。

```elm:src/Main.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です。以下に説明していきたいと思います。


```elm: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　の場合は前に見た通り、以下のイベントに対応しています。

```elm:view関数の一部
            , on "change" (JD.map FileSelected JD.value)
```

　次に FileLoaded binary の場合は、以下のsubscriptionsに対応しています。

```elm:subscriptions関数
subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.batch [ fileLoaded FileLoaded ]
```

　subscriptionsは、必要なmodel更新を行うべく、任意のイベントソースをリッスンするためのものです。イベントが生じたときにupdateが呼ばれ、modelの更新が行われます。
https://guide.elm-lang.org/architecture/effects/

　このsubscriptionsはJavaScript側の、以下の実行をリッスンしています。

```javascript:javascriptプログラムの一部
            app.ports.fileLoaded.send(base64encoded);
```

　今回は以上です。