ElmとJavaScriptを組み合わせたアプリケーションの作り方


この記事は、Elm入門前の方向けの記事です。すこしReactの話もでてきます。
Elmって何？という方は、ちょっとあんま参考にならないかもしれませんが、[こちら](https://speakerdeck.com/boiyaa/shi-jian-moteringu-reduxkaraelmhe)あたりをご覧ください。

Elmの事を話すと一番よく聞かれるのが、JSのライブラリ使う時はどうするのか、なので、
今日はまずその話から。

ElmはAltJSというだけあって、TypeScriptのようにJSのライブラリをインポートしたりできそうな感じしますが、それはできません。

Elmの安全な世界を守るために、型の無い（もしくはanyみたいな可能性のある）JSコードは使えないようになっています。
[Elm Packages](http://package.elm-lang.org/)という、Elmで書かれたモジュールのエコシステムを利用するしかありません。

しかしながら、Elm Packagesも割と充実してきてはいるものの、npmに比べると絶対数はとても少なく、npmだとあるこれっていうのが無かったりします。

どうしてもJSの何かを使いたいという時は、JSからElmとデータを受け渡しできる[JavaScript Interop](https://guide.elm-lang.org/interop/javascript.html)を使って、JS側で実行してから結果をElmに渡す、みたいな作り方をします。

サンプルのElmアプリケーションをベースにJavaScript Interopの方法を解説します。

```hs:Hello.elm
import Html

init =
    ("Hello, World!", Cmd.none)

view model =
    Html.text model

main =
    Html.program
        { init = init
        , update = \msg model -> (model, Cmd.none)
        , subscriptions = \model -> Sub.none
        , view = view
        }
```

このHello.elmは、The Elm Architectureに従い、丁寧にinit, update, subscriptions, view関数を定義していますが、initで`Hello, World!`と初期化したmodelをviewで画面に出力しているだけです。update, subscriptionsは後々使います。
Elmアプリケーションの実行は、elm-makeでコンパイル後、このようにJSで実行します。

```js:index.js
import Elm from "./Hello"

const app = Elm.Main.fullscreen()
```

## Flags

modelはElmアプリケーション全体の状態を保持する、いわゆるSingle source of truthで、Reduxのstateに相当します。
ReduxではcreateStoreの際にstateの初期値を渡すことができますが、`Flags`はそんな感じでElmアプリケーションに初期値を渡す機能です。
JS側では、実行の際に引数に渡すだけです。

```js:index.js
import Elm from "./Hello"

const app = Elm.Main.fullscreen("Hello, World!")
```

Elm側は、`Html.program`の代わりに`Html.programWithFlags`を使用して、`init`を初期値を受け取るように変更します。

```hs:Hello.elm
import Html

init : String -> ( String, Cmd msg )
init flags =
    (flags, Cmd.none)

view model =
    Html.text model

main =
    Html.programWithFlags
        { init = init
        , update = \msg model -> (model, Cmd.none)
        , subscriptions = \model -> Sub.none
        , view = view
        }
```

## Ports

`Ports`はElmアプリケーション起動後にJS-Elm間でPubSubする機能です。
まずJSからElmに値を渡してみます。

```js:index.js
import Elm from "./Hello"

const app = Elm.Main.fullscreen("Hello, World!")

setTimeout(() => app.ports.newState.send("Done!"), 3000)
```

このnewStateをElm側で定義します。

```hs:Hello.elm
port module Main exposing (..)

import Html

port newState : (String -> msg) -> Sub msg

type Msg
    = ReceiveNewModel String

subscriptions model =
    newState ReceiveNewModel

update msg model =
    case msg of
        ReceiveNewModel newModel ->
            (newModel, Cmd.none)

init : String -> ( String, Cmd msg )
init flags =
    (flags, Cmd.none)

view model =
    Html.text model

main =
    Html.programWithFlags
        { init = init
        , update = update
        , subscriptions = subscriptions
        , view = view
        }
```

定義したport newStateを購読するsubscriptionsを定義し、JSからsendされたらReceiveNewModelというメッセージが発行され、updateが実行され、modelが更新される、という処理を追加しました。

次にElmからJSに値を渡してみます。
onStateChangeというportを定義して、initとupdateの時にmodelをJSに渡す処理を追加しました。

```hs:Hello.elm
port module Main exposing (..)

import Html

port onStateChange : String -> Cmd msg

port newState : (String -> msg) -> Sub msg

type Msg
    = ReceiveNewModel String

subscriptions model =
    newState ReceiveNewModel

update msg model =
    case msg of
        ReceiveNewModel newModel ->
            (newModel, onStateChange newModel)

init : String -> ( String, Cmd msg )
init flags =
    (flags, onStateChange flags)

view model =
    Html.text model

main =
    Html.programWithFlags
        { init = init
        , update = update
        , subscriptions = subscriptions
        , view = view
        }
```

JSでは`subscribe`で購読します。

```js:index.js
import Elm from "./Hello"

const app = Elm.Main.fullscreen("Yes!")

app.ports.onStateChange.subscribe(state =>
  setTimeout(() => app.ports.newState.send(state + " Yes!"), 1000)
)
```

以上がJavaScript Interopになります。


## [react-elm-state](https://github.com/boiyaa/react-elm-state)

何かJSライブラリとElmの組み合わせサンプルをと思って作ってみました。
react-reduxみたいにElmをReactコンポーネントの状態管理FWとして使うためのライブラリです。

ProviderにElmモジュールと初期値を渡し、

```js:index.js
import React from "react"
import { render } from "react-dom"
import { Provider } from "react-elm-state"
import Counter from "./Counter"
import Elm from "./Main"

render(
  <Provider module={Elm.Main} flags={{ count: 0 }}>
    <Counter />
  </Provider>,
  document.body.appendChild(document.createElement('div'))
)
```

withElmに接続対象のコンポーネントとそのPropNameを渡し、

```js:Counter.js
import React from "react"
import { withElm } from "react-elm-state"

const Counter = ({ count, onIncrease }) =>
  <div>
    <span>{count}</span>
    <button onClick={onIncrease}>+</button>
  </div>

export default withElm(["count", "onIncrease"])(Counter)
```

ElmでFlagsとPropNameと同名のPortsを定義します。

```hs:Main.elm
port module Main exposing (..)

import Json.Decode exposing (Value)

port count : Int -> Cmd msg

port onIncrease : (Value -> msg) -> Sub msg

type Msg
    = Increase

subscriptions model =
    onIncrease <| always Increase

update msg model =
    case msg of
        Increase ->
            let
                newCount =
                    model.count + 1
            in
                ( { model | count = newCount }, count newCount )

type alias Model =
    { count : Int }

init : Model -> ( Model, Cmd Msg )
init flags =
    ( flags, Cmd.none )

main =
    Platform.programWithFlags
        { init = init
        , update = update
        , subscriptions = subscriptions
        }
```

まぁ、Reduxでいいよねって感じですが。。

ということで、JavaScript Interopの実用方法を紹介してみたものの、Elmだけで作りにくいものなら無理にElm使わなくていい気がしますが、自分はJSにElmを組み込むアプローチから入ったことで、JS脳を変えるためにはいい入門になったかと思います。

今回の動作サンプルは[こちら](https://github.com/boiyaa/elm-js-interop-examples)にあります。

DOMイベントや非同期処理なども扱ったもう少し複雑な[サンプル](https://github.com/boiyaa/counter-js/tree/master/react-elm-state)もあわせてどうぞ。