LoginSignup
35
20

More than 5 years have passed since last update.

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

Posted at

この記事は、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の方法を解説します。

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で実行します。

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側では、実行の際に引数に渡すだけです。

index.js
import Elm from "./Hello"

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

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

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に値を渡してみます。

index.js
import Elm from "./Hello"

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

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

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

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に渡す処理を追加しました。

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で購読します。

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

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

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

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を渡し、

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を定義します。

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脳を変えるためにはいい入門になったかと思います。

今回の動作サンプルはこちらにあります。

DOMイベントや非同期処理なども扱ったもう少し複雑なサンプルもあわせてどうぞ。

35
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
35
20