JavaScript
Elm
ElmDay 12

ElmのPortでJSを使う。

More than 1 year has passed since last update.

ElmとJavaScript(以下JS)がやり取りする方法には、PortとNativeモジュールとprogramWithFlagsの3つがあります。(違いについては最後に。)

Portの構文はよく忘れてコピペするので、Qiitaに書いておこうかなと思いました。

PortはElmに用意されているJSとやり取りする仕組みです。
Elmで以下のように名前と型だけの関数を用意し、Cmd/Subとして実行します。

port hello : String -> Cmd msg
port jsHello : (String -> msg) -> Sub msg

するとJS側で以下のように利用できます。

const elm = document.getElementById('elm');
const app = Elm.Test.embed(elm);   //Elmアプリケーションを起動

//ElmからJSへはsubscribe
app.ports.hello.subscribe(function(fromElm) {

  console.log(fromElm);
  //JSからElmへはsend
  app.ports.jsHello.send("Hi!");

});

//JSからElmへsend
app.ports.jsHello.send("Elm! hellooooo");


結構簡単です。

型の対応表

ElmとJSの型の対応表です。

Elm Javascript
Bool Bool
String String
Int Float Number
List array
Arrays array
Tuples array(固定長、複数の型)
Records object
Maybe (Nothing) null
Maybe (Just 42) 42
Json.Encode.Value JSON

Portの書き方。

Portを書いてみます。

port moduleに変更する

まず先頭行をmoduleからport moduleに変更します。(よく忘れてコンパイラに怒られる。)

port module Test exposing (..)

メモ:port moduleを使っているパッケージはElm Packageで公開できません。同時にJSのインストールも必要になるからです。

ElmからJSへCmdを出す

Elmから、外のJSを呼び出します。

Elm側

port 関数名 : 送る型 -> Cmd msg
port hello : String -> Cmd msg

portと書いて関数名と型を定義します。この時、型は送る型->Cmd msgとします。(msgは小文字で書かなければなりません。)

そしてこの関数をinit関数や、update関数で使います。

init = "" ! [hello "Hello!JS!"]

initに書くと、Elm初期化後すぐのタイミングでJSへ値を送ります。

JS側

JS側はports.定義した関数名.subscribeとして、関数を登録して受け取ります。(portsの「s」をよく忘れる)

var elm = document.getElementById('elm');
var app = Elm.Test.embed(elm);

//Elmから受け取る!
app.ports.hello.subscribe(function(str) {
  console.log(str);
});

これでElmから好きなタイミングでJSコードを呼び出すことが出来ました。

JSからElmに送る

次は反対にJS側からElmに値を渡して、Elmは受け取ります。

Elm側

port 関数名  (JSからやってくる型-> msg) -> Sub msg
port jsHello : (String -> msg) -> Sub msg

と書いて関数を定義します(msgは小文字)。

JSからやってくる値はElmではMsgになります。なのでMsg型を定義します。
そしてportで定義した関数を以下のように使います。

type Msg = GetHello Strign                     --Msgの定義

port jsHello : (String -> msg) -> Sub msg      --JS -> Elm のport

main = program {... , subscriptions = subscriptions} --subscriptions関数に使う。

subscriptions : Model -> Sub Msg
subscriptions model = jsHello GetHello         --例。

update msg model =
  case msg of
    GetHello str -> ...                        --JSからの値はMsgになる。

JS側

JS側は、app名.ports.関数名.send(送る値)でElmに送ります。

app.ports.jsHello.send("hellooooo"); //Elmへ送る

例えばJQueryを呼び出してみる。

最後にElmからJQueryを呼び出してみます。

この前ふとチャット画面のように自動で一番下までスクロールする機能が欲しくなりました。グーグルで検索してみると、JQueryのコードが出てきます。

$(スクロール対象のセレクタ).animate({"scrollTop": $(スクロール対象のセレクタ)[0].scrollHeight}, スクロール時間);

一行でスクロールの時間やアニメーションまで指定できるとはJQuery便利ですね。

portで呼び出してみます。

Elm

port scrollDown : () -> Cmd msg

update msg model =
    ...  -> ... ! [scrollDown ()]

consoleView =
    div [id "console"] [ ...]

js

const app = Elm.Main.embed(...);

app.ports.scrollDown.subscribe((_) => {

    $("#console").animate({"scrollTop": $("#console")[0].scrollHeight}, 5);

});

これで、画面に文字が追加されるたびにスクロールするようになりました。

このようにElmでPortを使えばJSの資産もすぐ使えます。

トラブルシューティング

Elmのスケジューラーとの兼ね合いだと思われるのですが、Elmを呼び出した後にDOMを取り出す処理を記述した所、取り出せないという事がありました。

const App = Elm.Main.embed(document.getElementById("main"));

document.getElementById('hoge').addEventListener("pointerdown" , (event) => {
        console.log(event);
    });

setTimeoutで囲むことで動きます。

setTimeout(function () {
    document.getElementById('hoge').addEventListener("pointerdown" , (event) => {
        console.log(event);
    });

}, 0);

PortとNativeモジュール

programWithFlagsはElmに初期値を渡す仕組みだとして、PortとNativeモジュールについて。

Nativeモジュールはapiのラップしたい、(またさらにElmパッケージとして公開したい時)に使うと思います。それでだいたい主要なWeb apiはライブラリとして公開されているので、「JSコード使いたい」と思ったときはだいたいPortで済むはずとなっています。

Nativeモジュールは非サポート(使い方を公開しない、apiがアナウンス無しで変わる)となっていて、公式側からのみ提供する方針になっています。またNativeを使ったパッケージは現在Elmパッケージから公開出来ないようになっています。

この理由は(自分の解釈でまとめると)、NativeモジュールはJSコードをElmランタイムに直接展開するので、ユーザーに開放してライブラリが増えていくとランタイムが壊れやすくなっていってしまう。ElmはJS以外(webassembly)も見ているのでJSをいろんなライブラリが、別にそれぞれラップしたコード使うみたいなことがないように必要最小限にしたい。といったあたりです。

しかしながらTestコード上など、Nativeの方が便利な場面、Portで冗長になる場面もあります。以下のリンクが参考になります。
参考:Elmを本番運用するためのツールあれこれ - Qiita
参考:Elm 0.17~0.18版 NativeModuleの書き方 - Qiita

Portで呼んだJSコードはElm化出来る/されるかもしれないので、あとで移行しやすいような関数名とか付けると、すこし良いかもしれません。

Nativeで副作用のある関数をラップするときは、副作用をElmに持ち込まないように、また使いやすいようにTask化すると良いかもしれません。

まとめ

  • ElmとJSがやり取りする方法には、port構文とNativeモジュールとprogramWithFlagsの3つ。
  • port簡単。基本的にはこいつを使う。JSとは疎結合でElmの純粋は保たれる。
  • port moduleはElm Packageで公開できない。
  • ElmのportでJS資産はすぐ使える。