photo credit: neorevoxos1 HOT_076 via photopin (license)

Flags を Elm に渡して初期化する

JavaScript から Elm へ作用するような使い方として、 Flags を用いてElmのプログラムを初期化する方法がある。
これは、外部の JavaScript から Elm へ初期化用のパラメーター(*1) 渡し、それを用いて Elm のプログラム内で初期化処理を行うというものである。
用途としては、例えば JavaScript で LocalStrage に保存されたデータを取得し、それを Elm の初期状態作成に用いることが挙げられる。

P.S. Flags だと LocalStrage からデータを取得して Elm を初期化するというのはできるけれど、LocalStrage へ保存することは出来ないので、あまり実用的な例とは言えなさそう。ちなみに、Flags でなく Ports を使えば、Elm から JS を介して LocalStrage への読み書きができる。
別の用途を挙げるなら、開発用と本番用で Elm プログラムの挙動や表示を変えたいときに、それを表す値を Flags で渡して初期化する、というケースも考えられるだろう。他には、Elm 側で環境変数を参照したいときなんかに、JS で環境変数を取得しておいて Flags で渡すとか。

*1 コマンドラインのフラグに見られるような感じで追加パラメーターを渡すので、ElmではこれをFlagsと呼ぶ。

Flags を使うためには、大まかに次のことが必要になる。

  1. Flags を受け取れる init 関数を持つような、Elm のプログラムを用意する
  2. Elm をコンパイルして JavaScript を生成する
  3. 外部の JavaScript プログラムから Elm の初期化処理を行う

ここから先では
「JavaScript から Elm へ数値を渡し、Elm がその数値の回数分だけHello World!と表示すること」
を目標として、そのためには上記1〜3をどうすればよいのか を追っていく。

(例えば 4を渡すと以下の表示がされるようにする)

Hello World!
Hello World!
Hello World!
Hello World!

1. Flags を受け取れる init 関数を持つような、Elm のプログラムを用意する

まず、Elm のmain関数にどのBrowser.xxxを使えばよいかを考えてみる。

Elm 0.19 には4種類のxxxがあり、https://package.elm-lang.org/packages/elm/browser/1.0.1/ によると次に引用する順番で次第に機能が拡張されたものとなっている。

sandbox — react to user input, like buttons and checkboxes
element — talk to the outside world, like HTTP and JS interop
document — control the <title> and <body>
application — create single-page apps

今回は JavaScript が Elm に作用するような使い方がしたいので、Browser.element以降のいずれかを用いればよいとわかる。

Browser.elementが今回の用途には必要最小限なので、これを用いることにする。

main関数はBrowser.elementを用いて次のようになる。

main =
  Browser.element
    { init = init
    , view = view
    , update = update
    , subscriptions = subscriptions
    }

次にinit関数を定義する。
今回は、JavaScript から Elm へ渡されるFlag(s)Int型であるものとしよう。
init関数はInt型の値を JavaScript から受け取るので、次のように定義する。

init : Int -> ( Model, Cmd msg )
init flag =
  ( { times = flags }, Cmd.none )

type alias Model =
  { times : Int }

あとは適当に view,update,subscriptionも用意してやれば、Elmプログラムは完成だ。
ちなみに、viewmodel.timesの回数分だけHello World!と表示するようなものになっている。updatesubscriptionは何の変化ももたらさない関数とした。

Main.elm

import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)
import List

main =
  Browser.element
    { init = init
    , view = view
    , update = update
    , subscriptions = subscriptions
    }

init : Int -> ( Model, Cmd msg )
init flags =
  ( { times = flags }, Cmd.none )

type alias Model =
  { times : Int }

type Msg
  = NoMessage

update : Msg -> Model -> ( Model, Cmd msg)
update msg model =
  ( model, Cmd.none)

view : Model -> Html Msg
view model =
  div [] <|
    List.repeat model.times <| div [] [ text "Hello World!" ]

subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.none

2. Elm をコンパイルして JavaScript を生成する

1 で作成した Elm をただ単に$ elm make Main.elmとしてコンパイルすると、HTMLファイルにコンパイルされてしまう。

今回のように JavaScript から Elm が作用を受けるためには、Elm を JavaScript へコンパイルする必要がある。そのためには、--outputオプションで拡張子を.jsとなるように指定しよう。

$ elm make --output=main.js Main.elm

これで、main.jsという JavaScriptのファイルへコンパイルされる。

main.jsにおいて、Elm のプログラム全体はElm.Mainというモジュールになっており、
init関数はElm.Main.initという関数として外部から利用できる。

3. 外部の JavaScript プログラムから Elm の初期化処理を行う

次のindex.htmlでは、埋め込みの JavaScript プログラムによって Elm を初期化している。

index.html

<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <title>Main</title>
  <script src="main.js"></script>
</head>

<body>
  <div id="elm"></div>
  <script>
  var app = Elm.Main.init({
    node: document.getElementById('elm'),
    flags: 4
  });
  </script>
</body>
</html>

<script src="main.js"></script>と記述することで、Elm をコンパイルして生成したmain.jsをロードしElm.Main.init関数を利用できるようにしている。

JavaScript 部分でElm.Main.init関数を呼び出しているが、ここで
Elm へ割り当てるノードをnodeキーで渡し、初期化に用いる Flags をflagsキーで渡す。

これにより、Elm は4で初期化され、<div id="elm"></div>のノードとして表示される。

以上で完了だ。

Flags に key-value pair (Dictionary) を用いる

これまでのコードでは、JavaScript から Elm にflags: 4を渡していた。
(このflagsを用いて Elm のプログラムは初期化され、結果として Hello World!4回表示された。)

しかし 1つの値を渡しただけでは不十分で、複数の値をFlagsで渡したくなることもあるだろう。

そこでここからは、表示する文字列も、表示する回数も、両方とも JavaScript から指定するように変更を加えていく。

flags: {greeting: <表示させる文字列>, times: <回数>}のようにして、key-value のペアとして渡すことにする。

例えばflags: {greeting: "Hello Again, World!", times: 8}とすれば以下のように表示されることが目標だ。

Hello Again, World!
Hello Again, World!
Hello Again, World!
Hello Again, World!
Hello Again, World!
Hello Again, World!
Hello Again, World!
Hello Again, World!

JavaScript 側の変更

index.htmlに埋め込んだ JavaScript のコードを次のように変更する。
(変わったのはflagsのみ)

  <script>
  var app = Elm.Main.init({
    node: document.getElementById('elm'),
    flags: {greeting: "Hello Again, World!", times: 8}
  });
  </script>

Elm 側の変更

Main.Elm も合わせて変更していく。

まずFlags型を Dictionary として定義。
JavaScript から渡されるflagsと Elm 側のFlags型で key を合わせている。

type alias Flags =
  { greeting : String
  , times : Int
  }

次にinit関数がFlags型の値を引数にとるよう修正する。
引数flagsを用いて初期状態を作成している。
それと、Modelgreetingtimesを両方持てるように変えておこう (view 関数に渡せるようにするため)。

init : Flags -> ( Model, Cmd msg )
init flags =
  ( { greeting = flags.greeting
    , times = flags.times
    }
  , Cmd.none
  )

type alias Model =
  { greeting : String
  , times : Int
  }

最後の変更はview関数。
greetingstimesの回数分、繰り返し表示する。

view : Model -> Html Msg
view model =
  div [] <|
    List.repeat model.times <| div [] [ text model.greeting ]

これで、JavaScript から JSON の Flags を Elm に渡せるようになり、それによって表示も変更出来た。

変更後のコード全体

index.html

<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <title>Main</title>
  <script src="main.js"></script>
</head>

<body>
  <div id="elm"></div>
  <script>
  var app = Elm.Main.init({
    node: document.getElementById('elm'),
    flags: {greeting: "Hello Again, World!", times: 8}
  });
  </script>
</body>
</html>

Main.elm

import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)
import List

main =
  Browser.element
    { init = init
    , view = view
    , update = update
    , subscriptions = subscriptions
    }

-- INIT

init : Flags -> ( Model, Cmd msg )
init flags =
  ( { greeting = flags.greeting
    , times = flags.times
    }
  , Cmd.none
  )

type alias Flags =
  { greeting : String
  , times : Int
  }

-----


type alias Model =
  { greeting : String
  , times : Int
  }

type Msg
  = NoMessage

update : Msg -> Model -> ( Model, Cmd msg)
update msg model =
  ( model, Cmd.none)

view : Model -> Html Msg
view model =
  div [] <|
    List.repeat model.times <| div [] [ text model.greeting ]

subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.none

GitHubはこちら

参考サイト

カテゴリー: Tips

hahnah

はーな Webエンジニア。モバイルアプリも少々。Elmが好き。

0件のコメント

コメントを残す

メールアドレスが公開されることはありません。