Melt UI
みなさん、Melt UIというSvelteのヘッドレスUIライブラリをご存知でしょうか?
Melt Introductionページで使い方を見てみましょう〜!
使い方
<script>
import { createCollapsible, melt } from '@melt-ui/svelte'
const {
elements: { root, content, trigger },
states: { open }
} = createCollapsible()
</script>
<div use:melt={$root}>
<button use:melt={$trigger}>{$open ? 'Close' : 'Open'}</button>
<div use:melt={$content}>Obi-Wan says: Hello there!</div>
</div>
こんな感じで使うみたいです。シンプルですね。
createCollapsible
とuse:melt={$xxx}
を見たところ、どうやらelements
とstates
からSvelteStoreを返しているようですね。
何を提供してくれるのか
Collapsible
という名前の関数なのでおそらくアコーディオン(ディスクロージャー)のような機能を提供してくれるのでしょう。
アコーディオンはW3Cのアクセシビリティに配慮した実装方法がまとまっているページをみるとキーボード操作やrole
の付与など、いくらか実装が必要なようです。
Melt UIはelementsのtrigger
やcontent
の部分がHTML属性の付与やキーボード操作機能を提供してくれます。
完全ではありません。注意。
特に気にしなくてもアクセシビリティがある程度担保されるのは嬉しいポイントです。
コンポーネント化するとコンポーネントからexport let
していない属性やon:event
していないイベントが受け取れないので自由度が下がってしまいます。
Melt UIはヘッドレスなUIライブラリなのでそこが解決されていてとてもいいですね。マークアップが自由になるのもメリットです。
Svelte Actions
さて、SvelteのActionsをご存知でしょうか?
SvelteのActionsは第一引数にnodeを持つ関数で、以下のように使用します。
<div use:actionName>
以下がMelt UIのコードです。
似ていますね。
<div use:melt={$root}>
SvelteのActionsは以下のようにしてactionName関数に値を渡すことができ、Melt UIの記述もそれに似ています。
<div use:actionName={props}>
さて、SvelteのActionsにはuse:
した要素にActionsからreturn
したオブジェクトを展開してくれる機能はありません。
でも例えばアコーディオンを見てみると、
<button use:melt={$trigger(id)}>
が
<button aria-expanded="true">
としてレンダリングされます。
onMount時で属性が付与されているのか?と思いソースを見ると属性がすでに付いています。
SvelteのActionsにはそんな機能はありません。
これをしようと思うと、
<button use:action {...$trigger(id)}>
こんな感じでuseでのアクションの指定とHTMLに付与する属性を展開する部分の二つが必要なはずです。
これはどういうことでしょうか?
Preprocessor
これはMelt UIが提供しているPreprocessorに秘密があります。
How it worksを読んでみるとどうやら、
use:melt={$root}
を
{...$root} use:$root.action
に変換しているみたいです。
これは賢いですね…
メリット
それではMelt UIのこの実装のメリットについて考えてみましょう。
まず、先ほど説明したように
<button use:action {...$trigger(id)}>
このように書く手間が省けており、手を動かす量が減っています。
コードは少なければ少ないほどバグも少なくなりますし、{...$trigger(id)}
し忘れて必要な属性が付与されないミスも考えられます。
use:melt={$root}
にまとまっていることは大きなメリットでしょう。
Actionsの第一引数のnodeの型
また、SvelteのActionsの第一引数はHTMLInputElementなど型を絞ることができます。
Melt UIは特に絞っていないようですが、例えば
const action: Action<HTMLElement & { disabled: boolean | null }> = (node) => ...
のようにすればuse:action
する要素をdisabled
属性が許可されている要素だけに絞ることができます。
<button use:action {...$trigger(id)}>
Preprocessorを使用しない場合{...$trigger(id)}
の部分はどの要素にも使用できてしまいます。
ヘッドレスUIライブラリは必要な属性をスプレッド構文で展開する都合上、本来渡せない属性まで渡せてしまってエラーも出ず、そのことに気づけない欠点があります。
SvelteのActionsはnodeの型を絞ることができるのでactionとpropsの展開が一つでできるならそれはかなり嬉しいです。
デメリット(というか懸念点)
use:melt={}
でPreprocessorを通すことをどう思うかです。
Melt UIもActionsも知らない人がuse:melt
を見た時にまずSvelteのドキュメントを見に行くでしょう。Svelteは人口が少ないので後から入ってきたメンバーがこれを知らない可能性は十分あります。
なるほどActionsはSvelteの機能なのね、あれ、でも実はPreprocessorで処理されている独自の書き方?というように混乱してしまわないでしょうか?
Svelteの構文に似ている独自の書き方を持ち込むのは僕は好みません。
Svelteが公式にActionsとpropsの展開を同時に行う実装を用意して欲しいなと思っています。
Svelte5ではon:click
などの書き方がonclick
を単に渡す実装に変更されますが、use:
はどうなるのか気になっています。
use:
では大体イベントリスナーを張ることがほとんどなので、Melt UIは{...$trigger(id)}
からonclick
などを返すようになってuse:
部分がなくなるのかな?と予想していますが、スプレッド構文での展開は不必要なpropsを渡してしまうことを防げない問題があるのでどうなるのか気になっています。
感想
デメリットの部分は気になっていますが、createHoge
関数からリアクティブな値を持つオブジェクトとActionsを返す実装はコンポーネントを作成するよりもいいなと感じています。
この設計は真似したいですね。