hauntedとは
最近何かと話題に上る「React hooks」ですが、React以外でも「hook」したいですよね。
hauntedは、標準のWeb Componentで「hook」する為のライブラリです。
ライセンスは「BSD-2-Clause」です。1
読みは、「ホーンテッド」?かと。浦安のハツカネズミの生息地にそんなマンションがあったような
執筆時点で、バージョンは「4.5.4」です。
使い方
haunted
はデフォルトでlit-html
を使う前提になっています。
以下は、githubにあるコードです。
〇〇.html
で保存し、ブラウザで開けば動きます。(僕の場合、Goole Chrome 76.0.3809.132)
カスタムエレメントを定義する場合の例です。
<!doctype html>
<html lang="en">
<my-counter></my-counter>
<script type="module">
import { html } from 'https://unpkg.com/lit-html/lit-html.js';
import { component, useState } from 'https://unpkg.com/haunted/haunted.js';
function Counter() {
const [count, setCount] = useState(0);
return html`
<div id="count">${count}</div>
<button type="button" @click=${() => setCount(count + 1)}>Increment</button>
`;
}
customElements.define('my-counter', component(Counter));
</script>
Counter
関数は、lit-htmlのTemplateResultを作る関数です。
component
関数は、lit-htmlのrenderTemplateResult
を渡し、HTMLElement
のクラスを返します。
つまり、
component
関数 = Web Componentにおける「コンポーネント = HTMLElementを作る関数」
と捉えておいてください。
そのままカスタムエレメントとして登録できるということです。
別のDOM生成ライブラリも使える
「haunted」の目玉機能として、adoptableなところがあります。
lit-html
以外のhyperhtml
などのDOM生成ライブラリも使える仕組みです。
しかしせっかくなので、「hauntedのadoptableなところ」をお見せする為に、素のDOM APIを使ってみます。
先ほどのサンプルを少し書き換えました。
<!DOCTYPE html>
<html lang="en">
<my-counter></my-counter>
<script type="module">
import { html } from "https://unpkg.com/lit-html/lit-html.js";
import haunted, { useState } from "https://unpkg.com/haunted/haunted.js";
const { component } = haunted({
// DOM総入れ替えの為、とてつもなく効率が悪い
render(what, where) {
// 親要素の中身をすべてクリア
while (where.hasChildNodes()) {
where.removeChild(where.firstChild);
}
// Counter関数で作ったDOM要素をぶち込む
what.forEach(element => {
where.appendChild(element);
});
}
});
function Counter() {
const [count, setCount] = useState(0);
// カウントする数字を見せる枠
const countContainer = document.createElement("div");
const countText = document.createTextNode(count);
countContainer.appendChild(countText);
// カウントアップのボタン
const countupButton = document.createElement("button");
countupButton.innerText = "Increment";
countupButton.setAttribute("type", "button");
countupButton.addEventListener("click", e => {
setCount(count + 1);
});
return [countContainer, countupButton];
}
customElements.define("my-counter", component(Counter));
</script>
</html>
今回作ったCounter
関数は、「DOM要素の配列」を返します。
haunted
関数に渡すrender
関数には、「what
とwhere
」が引数として渡されますが、
what
には、Counter
関数の戻り値 = 「DOM要素の配列」が入っています。
where
は、親要素が渡されます。
今回は、親要素を空にして、DOM要素の配列を順番に入れていくだけの非効率ロジックですが、説明の為なのでご勘弁を。
属性を渡すには
作ったコンポーネントに属性を渡すには、observedAttributes
を使います。
下は、value
属性を渡して、初期値にする場合の例です。
<!DOCTYPE html>
<html lang="en">
<my-counter value="100"></my-counter>
<script type="module">
import { html } from "https://unpkg.com/lit-html/lit-html.js";
import { component, useState } from "https://unpkg.com/haunted/haunted.js";
function Counter({ value }) {
const [count, setCount] = useState(Number(value) || 0);
return html`
<div id="count">${count}</div>
<button type="button" @click=${() => setCount(count + 1)}>
Increment
</button>
`;
}
Counter.observedAttributes = ["value"];
customElements.define("my-counter", component(Counter));
</script>
</html>
属性には文字列かundefined
が来ると思うので、その前提でハンドリングするといいと思います。
今回の例は、Number()
に渡して、NaN
だったら強制的に0
にしています。
属性で文字列しか渡せないのは嫌だ
「属性として」じゃなくて、「JavaScript関数の引数」として値を渡したいですよね。
コンポーネントを関数として、配列とかオブジェクトを引数として渡せるようにしたいです。
そんなときは、virtual
関数を使いましょう。
<!DOCTYPE html>
<html lang="en">
<my-app></my-app>
<script type="module">
import { html } from "https://unpkg.com/lit-html/lit-html.js";
import {
component,
useState,
virtual
} from "https://unpkg.com/haunted/haunted.js";
function Counter({ value }) {
const [count, setCount] = useState(value);
return html`
<div id="count">${count}</div>
<button type="button" @click=${() => setCount(count + 1)}>
Increment
</button>
`;
}
const VirtualCounter = virtual(Counter);
function App() {
return html`
${VirtualCounter({ value: 100 })}
`;
}
customElements.define("my-app", component(App));
</script>
</html>
さっきと変わったのは、Counter
関数をvirtual
関数でラップしたVirtualCounter
を作っている点です。
VirtualCounter({ value: 100 })
というように、普通に関数として呼び出せてます。
あとは、<my-app>
コンポーネントを定義して、描写しているだけです。簡単。
しかしこちらは、lit-htmlのdirectiveを使っているようなので、別のDOM生成ライブラリの利用はできないようです。
「hook」を自作する
もちろん「hook」を自作できます。
useMyUseState
という「hook」を作ってみました。
本家のuseState
の1000倍くらい遅くて、危なっかしい俺のuseState
を使えというオラオラhookです。
import {
hook,
Hook
} from "https://unpkg.com/haunted/haunted.js";
const useMyUseState = hook(
class extends Hook {
constructor(id, el, value) {
super(id, el);
this.value = value;
}
update(value) {
return [
this.value,
value => {
this.value = value;
this.el.update();
}
];
}
teardown() {}
}
);
update
メソッドは、setState
が呼び出される毎に呼ばれます。
useMyUseState
では、そのたびにthis.value
を更新し、this.el.update()
でコンポーネントを再度描写しています。
teardown
は、
-
カスタムエレメントだと
disconnectedCallback
-
virtual component
だと、「DOMに変更があったとき、親ノードがShadowRoot
かどうかで判定している」のでたぶんコンポーネントが消えた時でいいと思います。
まとめ
正直、lit-html
を昔使ったとき不満が多くてやめたんですが、virtual component
とか、lit-html
使ってみたくなる機能が入ってて、universal-routerとか組み合わせて、アプリ作ってみたら楽しそうだなあと思いました。
あと、ドキュメント読んで分からないところとかはソースコードを読んだので、もしかしたら間違ってるかもしれません。
その時はご指摘いただけるとありがたいです。
正直、わかりやすい記事書けた感が無いので、もっと文才のある方の記事をお待ちしております。(投げやり)
そんなこんなで、hountedの紹介でした。
-
2019/09/19 現在 ↩