🦓

[Rails]stimulus-componentsを使ってみる

2023/08/02に公開

はじめに

stimulus-componentsを使って開発中のRailsアプリのフロント部分を作っていきます。

stimulus-componentsは、Stimulusと組み合わせたフロントエンドコンポーネントのライブラリです。

Stimulusは軽量なJavaScriptフレームワークであり、HTMLに直接カスタムデータ属性を使ってコントローラーやアクションを指定することで、特定のDOM要素に対してJavaScriptの機能を追加することができます。

https://www.stimulus-components.com/

環境

Rails 7.0.4.3
ruby 3.2.1

stimulusをインストール

stimulusが必要です。ない場合インストールしましょう。

yarn add @hotwired/stimulus

https://stimulus.hotwired.dev/handbook/installing

<!-- HTMLでCDNを使用する場合 -->
<script src="https://unpkg.com/stimulus@2.0.0/dist/stimulus.umd.js"></script>

Stimulusの使い方を簡単に説明します。

Stimulusのコントローラーを作成する

rails 7でrails newしたのでデフォルトで入ってるapp/javascript/controllers/hello_controller.jsを使って説明します。

Stimulusのコントローラーを定義する

コントローラーは特定のDOM要素に対するJavaScriptの機能を担当します。

data-xxxは、StimulusJSを使用する際にHTMLに追加するカスタムデータ属性(Custom Data Attributes)です。
これらの属性を使用することで、特定のDOM要素に対してStimulusJSのコントローラーを関連付けたり、イベントリスナーやアクションを定義したりすることができます。

data-controller属性には、コントローラーのクラス名を指定します。

data-target:

  • コントローラーのクラス定義でstatic targetsプロパティを使用して指定された要素の名前を記述します。
  • コントローラーのアクション内で、thisキーワードを使用してdata-targetで指定された要素にアクセスすることができます。
  • data-targetにはCSSセレクターを指定します。

data-action:

  • data-actionは、イベントリスナーやアクションを定義するための属性です。
  • この属性には、イベントとアクションを関連付けるための構文を記述します。
  • イベントリスナーやアクションの構文は、イベント名->コントローラー名.アクション名 という形式です。
  • コントローラーのクラス定義で対応するアクションを定義しておく必要があります。

StimulusJSのコントローラーであるhello<div>要素に適用し、data-action属性を使って<button>要素にclickイベントを追加し、helloコントローラーのgreetアクションに関連付けています(click->hello.greet)。

<!-- HTML -->
<div data-controller="hello">
  <input type="text" data-target="hello.name" placeholder="名前を入力してください">
  <button data-action="click->hello.greet">挨拶</button>
</div>

helloコントローラーのクラス定義でstatic targetsプロパティを使用して、<input>要素とnameという名前のターゲットを指定します:

app/javascript/controllers/hello_controller.js
import { Controller } from 'stimulus';

export default class extends Controller {
  static targets = ['name'];

  greet() {
    const name = this.nameTarget.value;
    alert(`こんにちは, ${name}さん!`);
  }
}

このように、StimulusJSではHTMLの属性を活用してJavaScriptの機能を管理するため、直感的で分かりやすいコードを記述できます。Railsと組み合わせることで、効率的かつメンテナンス性の高いJavascriptを書くことができます。


今回ではドロップダウンメニューのコンポーネントをインストールしアプリに入れたいと思います。
https://www.stimulus-components.com/docs/stimulus-dropdown

stimulus-componentsをインストールする

アプリに必要なコンポーネントだけをインストールすることができます。
importmapでインストールします。

bin/importmap pin stimulus-dropdown         
Pinning "stimulus-dropdown" to https://ga.jspm.io/npm:stimulus-dropdown@2.1.0/dist/stimulus-dropdown.mjs
Pinning "@hotwired/stimulus" to https://ga.jspm.io/npm:@hotwired/stimulus@3.2.1/dist/stimulus.js
Pinning "hotkeys-js" to https://ga.jspm.io/npm:hotkeys-js@3.11.2/dist/hotkeys.esm.js
Pinning "stimulus-use" to https://ga.jspm.io/npm:stimulus-use@0.51.3/dist/index.js
bin/rails g stimulus dropdown
      create  app/javascript/controllers/dropdown_controller.js
app/javascript/controllers/drowdown_controller.js
import Dropdown from 'stimulus-dropdown'

export default class extends Dropdown {
  connect() {
    super.connect()
  }

  toggle(event) {
    super.toggle()
  }

  hide(event) {
    super.hide(event)
  }
}
  1. connect(): コントローラーがDOMに接続されたときに実行されるメソッドです。親クラスであるDropdownコントローラーのconnect()メソッドを呼び出して、デフォルトの接続処理を保持します。

  2. toggle(event): ドロップダウンメニューを開閉するためのメソッドです。親クラスであるDropdownコントローラーのtoggle()メソッドを呼び出して、デフォルトの開閉処理を実行します。toggle(event)メソッドは、ドロップダウンメニューの開閉がトリガーされるイベント(例: ボタンのクリック)を受け取ることができるように、event引数を受け取っています。

  3. hide(event): ドロップダウンメニューを非表示にするためのメソッドです。親クラスであるDropdownコントローラーのhide(event)メソッドを呼び出して、デフォルトの非表示処理を実行します。hide(event)メソッドは、非表示がトリガーされるイベント(例: ウィンドウのクリック)を受け取ることができるように、event引数を受け取っています。

データ属性を設定する

TailwindCSSのドロップダウンコンポーネントを使ってます。
https://tailwindui.com/components/application-ui/elements/dropdowns

app/views/shared/_header.html.erb
<div class="relative inline-block text-left" data-controller="dropdown">
  <div>
    <button type="button" data-action="dropdown#toggle click@windown->dropdown#hide" class="inline-flex w-full justify-center gap-x-1.5 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50" id="menu-button" aria-expanded="true" aria-haspopup="true">
      メニュー
      <svg class="-mr-1 h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
        <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
      </svg>
    </button>
  </div>

  <div class="absolute right-0 z-10 mt-2 w-56 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="menu-button" tabindex="-1"
    data-dropdown-target="menu"
    class="hidden transition transform origin-top-right absolute right-0"
    data-transition-enter-from="opacity-0 scale-95"
    data-transition-enter-to="opacity-100 scale-100"
    data-transition-leave-from="opacity-100 scale-100"
    data-transition-leave-to="opacity-0 scale-95"
  >
    <div class="py-1" role="none">
<% if user_signed_in? %>
    <%= link_to 'プロフィール', edit_user_registration_path, 
    class: "text-grey-700 block px-4 py-2 text-sm",
    role: "menuitem", tabindex: "-1", id: "menu-item-0",
    data:{ action: "click->dropdown#toggle", target: "dropdown" } %>
    <%= button_to 'ログアウト', destroy_user_session_path, method: :delete, 
    class: "text-grey-700 block px-4 py-2 text-sm",
    role: "menuitem", tabindex: "-1", id: "menu-item-3",
    data:{ action: "click->dropdown#toggle", target: "dropdown" } %>
<% else %>
    <%= link_to 'ログイン', new_user_session_path,
        class: "text-grey-700 block px-4 py-2 text-sm",
    role: "menuitem", tabindex: "-1", id: "menu-item-2",
    data:{ action: "click->dropdown#toggle", target: "dropdown" }%>
    <%= link_to '新規登録', new_user_registration_path,
        class: "text-grey-700 block px-4 py-2 text-sm",
    role: "menuitem", tabindex: "-1", id: "menu-item-2",
    data:{ action: "click->dropdown#toggle", target: "dropdown" } %>
<% end %>
    </div>
  </div>
</div>

data-action="dropdown#toggle click@windown->dropdown#hide"が2つのアクションを指定していますね。
dropdown#toggle: ドロップダウンメニューを開閉するアクションを示しています。
dropdownはコントローラー名を表し、toggleはコントローラー内で定義されたtoggleアクションを指します。

click@window->dropdown#hide: ウィンドウのクリックイベントに対してドロップダウンメニューを非表示にするアクションを示しています。
これは特定のDOM要素ではなく、ウィンドウ全体に対して定義されているアクションです。click@windowは、ウィンドウのクリックイベントに対応しています。
dropdown#hideは、コントローラー名dropdownによって呼び出されるhideアクションを指します。

出来上がりました。
Image from Gyazo

トースト通知

次は投稿にトースト通知を追加していきます。

https://www.stimulus-components.com/docs/stimulus-notification

やりたいこと

静的に作成したフラッシュメッセージですが、stimulus-notificationを使っての動的にしていきたいと思います。

やりたいこと:

  • 閉じるボタンをクリックしたらメッセージが消えること
  • クリックしない場合3秒後にフェードアウトさせること


stimulus-notificationをインストールする

bin/importmap pin stimulus-notification
Pinning "stimulus-notification" to https://ga.jspm.io/npm:stimulus-notification@2.2.0/dist/stimulus-notification.mjs
Pinning "@hotwired/stimulus" to https://ga.jspm.io/npm:@hotwired/stimulus@3.2.1/dist/stimulus.js
Pinning "hotkeys-js" to https://ga.jspm.io/npm:hotkeys-js@3.11.2/dist/hotkeys.esm.js
Pinning "stimulus-use" to https://ga.jspm.io/npm:stimulus-use@0.51.3/dist/index.js

stimulusコントローラーを作成する

bin/rails g stimulus notification
      create  app/javascript/controllers/notification_controller.js
app/javascript/controllers/notification_controller.js
import Notification from 'stimulus-notification'

export default class extends Notification {
  connect() {
    super.connect()
  }
}

データ属性を指定する

TailwindCSSのクラスとアイコンを使ってます。

app/views/shared/_flash_messages.html.erb
<% flash.each do |message_type, message| %>
  <div class="flex items-center alert-<%= flash_class(message_type) %> ring-1 ring-inset p-4 rounded-md fixed top-20 right-5 transition transform duration-1000" role="alert"
  data-controller="notification"
  data-notification-delay-value="3000"
  data-transition-enter-from="opacity-0 translate-x-6"
  data-transition-enter-to="opacity-100 translate-x-0"
  data-transition-leave-from="opacity-100 translate-x-0"
  data-transition-leave-to="opacity-0 translate-x-6"
  >
    <%= message %>
    <button data-action="notification#hide">
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
        <path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
      </svg>
    </button>
  </div>
<% end %>
</div>

data-notification-delay-valueは通知が消えるまでの遅延(delay)に関する設定になります。3000は3秒になります。

Image from Gyazo
Image from Gyazo

終わりに

stimulus-componentsを使ってたドロップダウンメニューとトースト通知でした。
stimulus-componentsをTailwindCSS組み合わせたライブラリーも見つけました。

https://github.com/excid3/tailwindcss-stimulus-components

Discussion