コード例
カルーセルはよく使われるUIウィジェットで広く使われているライブラリも存在します。しかし自作できるのであれば、設定方法に悩む必要もなくなり、却って使いやすくなることも珍しくありません。ここではStimlusで自作したカルーセルを紹介します。
下記のようなUIになります。
[デモはこちら]からご覧ください。
<div data-controller="carousel" class="relative"> <div class="w-full h-[360px]"> <% @carousel_images.each_with_index do |filename, i| %> <div class="<%= "invisible opacity-0" unless i == 0 %> transition-all duration-1000" data-carousel-target="slide" data-carousel-key="<%= i %>"> <%= image_tag "hotel_images/#{filename}", class: "absolute w-full h-[360px] object-cover" %> </div> <% end %> </div> <%= button_tag type: "button", class: "absolute w-8 h-8 p-1 rounded-full block top-[170px] left-[10px] bg-white opacity-40 hover:opacity-100", data: { action: "click->carousel#previous" } do %> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"> <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5"/> </svg> <% end %> <%= button_tag type: "button", class: "absolute w-8 h-8 p-1 rounded-full block top-[170px] right-[10px] bg-white opacity-40 hover:opacity-100", data: { action: "click->carousel#next" } do %> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"> <path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5"/> </svg> <% end %> <div class="inline-block absolute bottom-4 left-[50%] -translate-x-1/2"> <% 0.upto(@carousel_images.size).each do |index| %> <%= button_tag "⚫︎", type: :button, class: "text-white #{index == 0 ? "opacity-100" : "opacity-50"}", data: { action: "click->carousel#move", carousel_index_param: index, carousel_target: "pagination" } %> <% end %> </div> </div> <!-- ... -->
data-controller="carousel"のところでStimulus controllerを繋げていますdata-actionを設定している箇所です。data: { action: "click->carousel#previous" }(左矢印ボタン), data: { action: "click->carousel#next" }(右矢印ボタン), data: { action: "click->carousel#move"...(ページネーションボタン)の箇所です。それぞれcarouse controllerのprevious(), next(), move()を読んでいます。move()のところはcarousel_index_param: indexがありますので、何番目のボタンがクリックされたかもmove()に伝えていますtargetとなっている箇所です。data-carousel-target="slide"は画像の箇所です。選択されている画像を表示し、他を非表示にする必要があります。carousel_target: "pagination"のところはページネーションボタンです。現在選択されているものだけをハイライトする必要がありますimport {Controller} from "@hotwired/stimulus" // Connects to data-controller="carousel" export default class extends Controller { static targets = ["slide", "pagination"] static values = { currentSlide: {type: Number, default: 0}, autoplay: {type: Boolean, default: true}, interval: {type: Number, default: 4000}, } #hideClasses; #paginationSelectedClasses; #paginationUnselectedClasses; initialize() { this.#hideClasses = ["invisible", "opacity-0"] this.#paginationSelectedClasses = ["opacity-100"] this.#paginationUnselectedClasses = ["opacity-50"] } connect() { if (this.autoplayValue) { this.slideInterval = setInterval(() => { this.#moveNext() }, this.intervalValue) } } disconnect() { this.#clearSlideInterval() } move(event) { this.currentSlideValue = event.params.index this.#clearSlideInterval() } next() { this.#moveNext() this.#clearSlideInterval() } previous() { this.#movePrevious(); this.#clearSlideInterval() } currentSlideValueChanged() { this.#render() } get slideCount() { return this.slideTargets.length } #clearSlideInterval() { this.autoPlayValue = false if (this.slideInterval) { clearInterval(this.slideInterval) } } #render() { this.#renderSlideTargets(); this.#renderPaginationTargets(); } #renderPaginationTargets() { this.paginationTargets.forEach((target, index) => { if (index === this.currentSlideValue) { target.classList.remove(...this.#paginationUnselectedClasses) target.classList.add(...this.#paginationSelectedClasses) } else { target.classList.remove(...this.#paginationUnselectedClasses) target.classList.add(...this.#paginationUnselectedClasses) } }) } #renderSlideTargets() { this.slideTargets.forEach((target, index) => { if (index === this.currentSlideValue) { target.classList.remove(...this.#hideClasses) } else { target.classList.add(...this.#hideClasses) } }) } #moveNext() { if (this.currentSlideValue + 1 < this.slideCount) { this.currentSlideValue = this.currentSlideValue + 1 } else { this.currentSlideValue = 0 } } #movePrevious() { if (this.currentSlideValue - 1 >= 0) { this.currentSlideValue = this.currentSlideValue - 1 } else { this.currentSlideValue = this.slideCount - 1 } } }
static targets =のところは、Controllerで処理した結果を画面に反映するためのtargetの指定です。上述した画像を表示するところ(“slide”)、およびページネーションをするところ(“pagination”)がtargetになりますstatic values =はこのStimulus controllerのステートです
currentSlideは現在選択されている画像の番号ですautoplayは自動再生をするかどうかのブール値ですintervalは自動再生する時の時間間隔ですdata-carousel-*-valueなどで外部から指定することもできます。つまりサーバでERBを生成するときにdata-carousel-*-valueを設定すれば、Stimulus controllerの初期値を任意に設定できるわけです。またStimulus controllerの外から別のJavaScriptなどで変えることもできます。実際開発者用コンソールからこの値を変更すれば、瞬時に反映されます。initialize()はStimulus controllerの初期化です。接続されるよりも先に実行されるべき内容を記述します
*ValueChanged()を使っていますが、これはconnect()よりも先に呼び出されます。したがってその中で使われるような初期設定はinitialize()の中で行っておく必要がありますcurrentSlideValueChanged()の中の#render()で使用されますので、ここで設定していますconnect()はStimulus controllerが接続されたときに呼び出されるものです。ここでは自動再生をするためにsetInterval()を使っていますdisconnect()はStimulus controllerが消える時(例えば接続されているHTML要素が消える時など)に呼び出されます。先ほどのsetInterval()をclearしていますmove(), next(), previous()はそれぞれイベントハンドラです。HTMLに記載したActionから呼び出されます。それぞれcurrentSlideValueステートを更新し、さらに自動再生をオフにする処理をしています。currentSlideValueChanged()はcurrentSlideValueステートが変更された時に自動的に呼ばれるコールバックです。ここで#render()を呼び、Stimulus controllerが管理するtargetを再描画します。こうすることで action ==> value (ステート) ==> targetの再描画 のデータフローになりますので、アクションとステートの管理がしやすくなります。これはReactのデータフローと似ています#renderPaginationTargets(), #renderSlideTargets()は実際にtargetを再描画しているところです。currentSlideValueステートに応じて、古いCSSクラスを外して、新しいCSSクラスを当てるパターンになっていますcurrentSlideValueChanged()に委ねるべきですdata-*-valuesとしてHTML要素の属性になっています。これを変更すれば、リアルタイムでStimulus controllerのステートを変更できますので、バックエンドのERBからカスタマイズしたり、他のライブラリと接続する時に便利です今回のカルーセルは一通りの機能を持っていますが、コードの流れが直線的でわかりやすくなっています。これはReactと同様のデータフローを採用したためです。StimulusでもReactのようなデータフローを簡単に実装できることが実感できたのではないかと思います。
Stimulusのキャッチフレーズが“modest JavaScript framework”であることからも分かるとおり、多機能は目指していません。今回やったことはもちろんjQueryでもできます。ただしjQueryは複雑なUIを作る時にスパゲッティコードになりやすいという悪評がありました。Stimulusが目指しているのは、jQueryの欠点を解消し、クリーンでメンテナンスしやすいコードを書くための枠組みです。それはこういう小さな機能で実現されています。
https://www.nngroup.com/articles/designing-effective-carousels/