Edited at

Elmでportもネイティブモジュールも使わずJSと通信する方法

800px-English_Elm_avenue.jpg

ElmでウェブブラウザのAPIを直接叩けるのは公式のパッケージだけで、サードパーティのパッケージではネイティブなモジュールもportも使うこともできません。でも、joakin/elm-canvas というパッケージが存在していて、公式で対応していないはずのCanvas APIを叩いているのです。どないなっとんねん!と思って調べたので、どんな方法でやっているのか簡単に紹介しておきます。


1. 外部に送信したいデータを、propertyを使って要素の"cmds"というプロパティに突っ込む

commands : Commands -> Attribute msg

commands list =
list
|> Encode.list identity
|> property "cmds"

既存のプロパティでなければどんな名前でもいいはずですが、em-canvasでは"cmds"というプロパティに突っ込んでいるようです。ちなみのこのデータの中身は、次のような感じのJSONになっていました。

[

{type: "function", name: "restore", args: []},
{type: "function", name: "fill", args: ["nonzero"]},
{type: "field", name: "fillStyle", value: "rgba(0%,0%,0%,0.3)"},
...


2. elm-canvasというCustom Elementsを描画する

toHtml : ( Int, Int ) -> List (Attribute msg) -> List Renderable -> Html msg

toHtml ( w, h ) attrs entities =
Html.node "elm-canvas"
[ commands (render entities) ]
[ canvas (height h :: width w :: attrs) []
]

1で定義したcommandsを使って、そのelm-canvascmdsプロパティにデータを突っ込んでいるわけです。ついでに、その内側に実際の描画先となるcanvas要素も作ってあります。


3. そのCustom Elementsの定義のsetアクセサで、Elm側から送られたデータを受け取る

customElements.define(

"elm-canvas",
class extends HTMLElement {

...

set cmds(values) {
this.commands = values;
this.render();
}

...

}
);

JavaScript側のelm-canvas要素の定義で、cmdssetアクセサを用意して、そこでデータを受け取ります。あとは受け取ったデータを元にCanvasの描画を行うだけです。


まとめ

というわけで、Custom Elementsのプロパティ越しにデータを送れば、elm-canvas.jsというスクリプトを別に読み込む必要はあるものの、ネイティブモジュールもportも使わずに、Elmのパッケージから外部のAPIを叩くことができるということがわかりました。このやり方ではJavaScript側からElm側へデータを送ることはできないので、measureTextのような機能は実現できないという制限はあるでしょうが、簡単な描画であればこれで十分でしょう。

……闇の魔術じゃん!


追記

……と思いきや、akira_さんから カスタムイベントでJSからElmへデータを送る方法が提案され、ElmからJSだけでなくJSからElmへとデータを送ることも問題なくできることがわかりました。portガン無視でデータを送りまくれるぞ! 悪の枢軸ッ!