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

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

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

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

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

# サンプルアプリ

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

```haskell: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 アプリケーションが展開されます。

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

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

```html: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 で作ります。

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

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

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

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

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

```haskell
import Web.HTML.HTMLElement (HTMLElement)

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

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

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

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

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

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

# 結果

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

![Screenshot_2020-06-13 Screenshot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216864/24b6079f-5d12-be29-5c16-a83b162416c3.png)

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

# まとめ

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

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

[^1]: https://github.com/purescript-halogen/purescript-halogen/releases/tag/v5.0.0
[^2]: https://github.com/purescript-halogen/purescript-halogen/tree/master/docs/guide
[^3]: https://guide.elm-lang.org/interop/custom_elements.html

# おまけ

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

* https://github.com/nonbili/purescript-halogen-custom-element

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

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

[^*1]: https://github.com/purescript/package-sets
[^*2]: https://pursuit.purescript.org
[^*3]: https://pursuit.purescript.org/packages/purescript-webcomponents/0.1.0