この記事は、Elm入門前の方向けの記事です。すこしReactの話もでてきます。
Elmって何?という方は、ちょっとあんま参考にならないかもしれませんが、こちらあたりをご覧ください。
Elmの事を話すと一番よく聞かれるのが、JSのライブラリ使う時はどうするのか、なので、
今日はまずその話から。
ElmはAltJSというだけあって、TypeScriptのようにJSのライブラリをインポートしたりできそうな感じしますが、それはできません。
Elmの安全な世界を守るために、型の無い(もしくはanyみたいな可能性のある)JSコードは使えないようになっています。
Elm Packagesという、Elmで書かれたモジュールのエコシステムを利用するしかありません。
しかしながら、Elm Packagesも割と充実してきてはいるものの、npmに比べると絶対数はとても少なく、npmだとあるこれっていうのが無かったりします。
どうしてもJSの何かを使いたいという時は、JSからElmとデータを受け渡しできるJavaScript Interopを使って、JS側で実行してから結果をElmに渡す、みたいな作り方をします。
サンプルのElmアプリケーションをベースにJavaScript Interopの方法を解説します。
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で実行します。
import Elm from "./Hello"
const app = Elm.Main.fullscreen()
Flags
modelはElmアプリケーション全体の状態を保持する、いわゆるSingle source of truthで、Reduxのstateに相当します。
ReduxではcreateStoreの際にstateの初期値を渡すことができますが、Flags
はそんな感じでElmアプリケーションに初期値を渡す機能です。
JS側では、実行の際に引数に渡すだけです。
import Elm from "./Hello"
const app = Elm.Main.fullscreen("Hello, World!")
Elm側は、Html.program
の代わりにHtml.programWithFlags
を使用して、init
を初期値を受け取るように変更します。
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に値を渡してみます。
import Elm from "./Hello"
const app = Elm.Main.fullscreen("Hello, World!")
setTimeout(() => app.ports.newState.send("Done!"), 3000)
このnewStateを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に渡す処理を追加しました。
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
で購読します。
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
何かJSライブラリとElmの組み合わせサンプルをと思って作ってみました。
react-reduxみたいにElmをReactコンポーネントの状態管理FWとして使うためのライブラリです。
ProviderにElmモジュールと初期値を渡し、
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を渡し、
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を定義します。
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脳を変えるためにはいい入門になったかと思います。
今回の動作サンプルはこちらにあります。
DOMイベントや非同期処理なども扱ったもう少し複雑なサンプルもあわせてどうぞ。