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資産はすぐ使える。