LoginSignup
3
0

Melt UIの設計をみてみる

Last updated at Posted at 2023-12-11

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>

こんな感じで使うみたいです。シンプルですね。
createCollapsibleuse:melt={$xxx}を見たところ、どうやらelementsstatesからSvelteStoreを返しているようですね。

何を提供してくれるのか

Collapsibleという名前の関数なのでおそらくアコーディオン(ディスクロージャー)のような機能を提供してくれるのでしょう。

アコーディオンはW3Cのアクセシビリティに配慮した実装方法がまとまっているページをみるとキーボード操作やroleの付与など、いくらか実装が必要なようです。

Melt UIはelementsのtriggercontentの部分が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を返す実装はコンポーネントを作成するよりもいいなと感じています。

この設計は真似したいですね。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0