💻

JavaScript1行も書きたくないモチベーションでHeadless UIライブラリをJSで作ってみた

2022/09/24に公開

作ったもの

TypeScriptで実DOM向けのヘッドレスUIライブラリを作ってみました。
https://zerolinesjs.40yd.app/

なぜ作った

普段はRailsエンジニアをしていて、プライベートではHotwireを使って小さなWebアプリを作ったりしています。その中で面倒だと思っていたことがありました。

  • Hotwire(Turbo, Stimulus.js)では実DOMの更新を主軸に置いた振る舞いをする。
  • UIインタラクションについて、データの更新を伴うものは基本的にはTurboFrameやTurboStreamで操作すれば良い。
  • しかし、データ更新を伴わないモーダル表示やドロップダウンの表示といった、よくあるコンポーネントについて、毎度毎度Stimulus.jsでJSを書きたくない。(面倒)

そこで、以下のようなものが欲しいなと思って色々ライブラリを探しました。

  • スタイルは自前でヘッドレスで動くコンポーネント群(Headless UIみたいな)
  • ある程度インタラクションをカスタマイズできる
  • JS書かないでHTML上で完結する
  • TurboでDOM更新した後も動く
  • TailwindCSSみたいに要素にアタッチしていく感じで

Bootstrapのような、HTMLに書くだけで動いて、Headless UIみたいなやつを探してみるも、自分では探し切れず、Alpine.js使うならStimulusで良いし、結局、「自分で作ればいいのでは?」と思い至り開発を始めました。

コンセプト

上記に記載した内容と被りますが、

  • Headless(スタイルは自分で)
  • JS1行も書かない
  • カスタムdata属性data-zlにロールと振る舞いをアタッチするだけで動く
  • TurboでDOM更新しても動く

という感じです。

サンプル

実際のコードサンプルはリンクを見てみて欲しいですが、こんな感じになります。以下はドロップダウンのコードです。

<button type="button" data-zl="dropdown-toggle target-[#dropdown1]">
  Dropdown Button
</button>
<ul id="dropdown1" zl-cloak>
  <!-- List Content Here -->
</ul>

実際にはbutton要素に対してabsoluteつけるなどのスタイルが必要なりますが、最低限これで動きます。dropdown-toggleと、クリックした際に開くtargetを指定するのみです。

他にもDrawerはこんな感じです。

<button type="button" data-zl="drawer-toggle target-[#drawer1]">
  Drawer Button
</button>
<div id="drawer1" zl-cloak>
  <nav data-zl="drawer-content drawer-right slowly">
    <button type="button" data-zl="drawer-dismiss">Dismiss Button</button>
    <!-- Drawer Content Here -->
  </nav>
  <div data-zl="drawer-backdrop"></div>
</div>

動きは同じような感じですが、ポイントはdrawer-contentdrawer-rightdrawer-leftという「振る舞い」を指定することで、右開きや左開きを指定できる所です。また、slowly,
quicklyなどを付与してアニメーションスピードをある程度コントロールできます。どうでしょう。これでJS1行も自分で書かなくても大丈夫です。

公式サイトのDrawerインタラクションはこのライブラリを使用して自分でJS1行も書いてないのでみてみてください。

現時点のコンポーネント

  • Modal
  • Drawer
  • Dropdown
  • Tab
  • OneTime Content
  • ScrollTop

OneTime Contentは一度クリックしたらLocalStorageで制御している期限まで非表示になるものです。

zl-cloak

zl-cloakというカスタムAttributeを使っています。これはAlpine.jsをインスパイアしたもので、ContentLoadedからZerolines.jsが初期化完了するまでの間、要素を隠しておくことで、「画面表示後一瞬チラッと出ちゃう」現象を回避することができます。

MutationObserver

そのままだと、DOMが更新された際に要素にイベントリスナーが付けられていないため動きません。そこでStimulus.jsを参考にしてMutationObserverによるDOM監視をしています。これでTurboなどで変更された要素に対して、都度イベントリスナーを付与するができます。

OSS

publicリポジトリとして公開しています。今までRubyのGemを勉強がてら作ったことがありますが、JavaScriptとしては初の試みです。OSSのお作法何にもわかっていません。TypeScriptも完全初心者です。

少しずつ勉強していくので、OSSお作法やTypeScript文法のイシューやプルリクも大歓迎でございます。

https://github.com/shoyu777/zerolinesjs

今後

とりあえずテストが書けてません(ブラウザでのマニュアルテストはしてます)。テストが書きたいです。ただ、DOMのイベント操作が中心のやつってどこまで書けばいいかわかってないのでそこも勉強しながらですね。
(余談ですが、一通り実装した後にSafariで確認したら正規表現でサポートされてないものがあって動かず、丸々書き直した箇所があります。ブラウザ依存のテストも難しい。。)

テストを書いて、ドキュメントのサンプルを拡充したらv1に上げる予定です。

また、作ったり調べていてアクセリビリティが気になってきたので、アクセシビリティ対応できたらv2にしようと思います。

あと、Rails上で使いたいのでgem作ったりもしたいです。(CDNで読み込んで動くことは確認済み)

最後に

同じようなライブラリを知っていたら教えて欲しいです。ここまでやったけど自分が開発する必要ないので。

あるいはもし方向性に気に入っていっただけたら、ぜひリポジトリに⭐️お願いします。やる気が出ます。また、いろんな意見(特にネガティブ意見)が欲しいのでIssueなりTwitterで感想なりいただけると嬉しいです。

JavaScript1行も書きたくないが為に、ここ10日くらいJavaScriptしか書いてないというなんとも本末転倒な感じですが、以上よろしくお願いします。

Discussion