PureScript + Halogen でカスタムエレメント


先日 Halogen v5 が正式リリースされました1。早速ガイドを読んでみましたが、いろいろとわかりやすくなってていい感じです。

ところで、Halogen でアプリケーションを作るとページを丸ごとそのアプリケーションにするような形になります。ガイドで説明されているアプリケーションの実行方法でもそうなっています2

新しく作るアプリケーションであればそれでいいのですが、JavaScript なアプリケーションを一部だけ置き換えて段階的に導入していくようなときにはちょっと困ります。そういう時に便利なのがカスタムエレメントなのですが、PureScript + Halogen でカスタムエレメントは使えるのでしょうか?

というわけでやってみました。

サンプルアプリ

サンプルとしてまず以下のコードがあるとします。

Main.purs
module Main where

import Prelude
import Effect (Effect)
import Halogen.Aff as HA
import Halogen.VDom.Driver (runUI)
import MyComponent (component)

main :: Effect Unit
main = HA.runHalogenAff do
  body <- HA.awaitBody
  runUI component unit body

ここでモジュールMyComponentは別ファイルで定義されているものとします。これを JavaScript にトランスパイルしたもの(my-component.js)を HTML で読み込んでブラウザで表示するとbody要素内末尾に Halogen アプリケーションが展開されます。

index.html
<!DOCTYPE html>
<head>
    <script src="my-component.js"></script>
</head>
<body>
</body>
</html>

もともとbodyに書かれていた要素が削除されることはありませんが、このままでは展開される位置は固定されており複数個所に挿入したくなってもできません。
今回はこれをカスタムエレメントmy-componentに突っ込めないか実験してみます。うまくいけば以下のような感じで使えるようになるはずです。

index.html
<!DOCTYPE html>
<head>
    <script src="my-component.js"></script>
</head>
<body>
    <header>
        ここは普通にHTML
    </header>
    <section>
        <h1>セクション1</h1>
        <my-component></my-component>
    </section>
    <section>
        <h1>セクション2</h1>
        <my-component></my-component>
    </section>
    <footer>
        ここもただのHTML
    </footer>
</body>
</html>

カスタムエレメントの作成

器となるカスタムエレメントを JavaScript で作ります。

class MyComponent extends HTMLElement {
    connectedCallback () {
        // カスタムエレメントの機能を定義する
    }
}
customElements.define('my-component', MyComponent);

connectedCallbackメソッドで Halogen コンポーネントを呼び出せばよさそうです。

ここでmain関数を見てみると、HA.awaitBodybody要素を取得して、それを引数にしてrunUIを実行しています。

main :: Effect Unit
main = HA.runHalogenAff do
  body <- HA.awaitBody  -- ★
  runUI component unit body

ここを先ほど作ったカスタムエレメントを渡すように変更してやればいいと思われます。しかしカスタムエレメント自体はbody要素とは違って最初から DOM にあるものではありませんので、JavaScript から引数で渡すしかないでしょう。main関数が引数を持つというのはちょっと変な感じがしますが、こんな感じでしょうか。

import Web.HTML.HTMLElement (HTMLElement)

main :: HTMLElement -> Effect Unit
main = HA.runHalogenAff <<< runUI component unit

ここで引数の型をHTMLElementとしたのはrunUIがそうなっているからです。カスタムエレメントはHTMLElementクラスを継承しているのでそれっぽいですね。

これをビルドして output ディレクトリにモジュールができたとしまして、カスタムエレメント側での呼び出しは以下のようにしてみます。

my-component.js
const PS = require('./output/Main');

class MyComponent extends HTMLElement {
    connectedCallback () {
        PS.main(this)();
    }
}
customElements.define('my-component', MyComponent);

これを parcel などを使ってブラウザで読み込める形にしてやれば出来上がりです。

結果

ブラウザで表示したらこうなりました。コンポーネントの中身として Halogen ガイド2にあるカウンターを使っています。

どうやらうまくいっているようです

まとめ

今回の実験で以下のことがわかりました。

  • Halogen でカスタムエレメントは作れる
  • main関数にも引数を設定できて、JavaScript から呼び出すことができる

おまけ

ちなみに実験してみた後で「PureScript Halogen customElement」で検索したら、そのものずばりなリポジトリがありました。

ソースを見てみたら今回実験で作ったようなカスタムエレメントの定義を、丸ごと FFI で PureScript にしたものといった感じです。
実際にカスタムエレメントを使いたいときはこちらのライブラリを使わせてもらうのがいいかも。ただし package-sets3 には含まれていないので、インストールするには手動でパッケージリストへの登録が必要です。

また Pursuit4 で CustomElement を検索すると purescript-webcomponents というパッケージが見つかります5。しかしこちらは使用例の記載がないため、どういうものなのかいまいちよくわかりません…。