Admittedly something small.

ちょっと小さいのはたしかですが。

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

2018年12月20日 ( 6年前に投稿 )

Elm 闇の魔術

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ガン無視でデータを送りまくれるぞ! 悪の枢軸ッ!

(この記事は同じ筆者が Qiita に投稿した記事の複製です。オリジナル記事)