コード例
カルーセルはよく使われる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="aria-[hidden=true]:invisible aria-[hidden=true]:opacity-0 transition-all duration-1000" data-carousel-target="slide" aria-hidden="<%= i == 0 ? "true" : "false" %>" > <%= 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 - 1).each do |index| %> <%= button_tag "⚫︎", type: :button, aria: {selected: (index == 0 ? "true" : "false")}, class: "aria-[selected=true]:opacity-100 opacity-50 text-white", 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"...
(ページネーションボタン)の箇所です。それぞれcarousel
controllerのprevious()
, next()
, move()
を呼び出します。move()
のところはdata-carousel-index-param=[index]
がありますので、何番目のボタンがクリックされたかもmove()
メソッドに伝えていますtarget
となっている箇所です。data-carousel-target="slide"
は画像を指し示すものです。これを使って選択されている画像を表示し、他を非表示にする必要があります。data-carousel-target="pagination"
のところはページネーションボタンです。現在選択されているものだけをハイライトするために使いますslide
の箇所、およびpagination
の箇所は、それぞれaria-[hidden=true]
aria-[selected=true]
の擬似CSSセレクタを使用して、 表示状態を制御します(aria-*
属性はStimulus controllerから設定します)CarouselController
Stimulus Controllerimport {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}, } 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.ariaSelected = "true" } else { target.ariaSelected = "false" } }) } #renderSlideTargets() { this.slideTargets.forEach((target, index) => { if (index === this.currentSlideValue) { target.ariaHidden = "false" } else { target.ariaHidden = "true" } }) } #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などで変えることもできます。実際開発者用コンソールからこの値を変更すれば、瞬時に反映されます。connect()
はStimulus controllerが接続されたときに呼び出されるものです。ここでは自動再生をするためにsetInterval()
を使っていますdisconnect()
はStimulus controllerが消える時(例えば接続されているHTML要素が消える時など)に呼び出されます。先ほどのsetInterval()
をclearしていますmove()
, next()
, previous()
はそれぞれイベントハンドラです。HTMLに記載したActionから呼び出されます。それぞれcurrentSlideValue
ステートを更新し、さらに自動再生をオフにする処理をしています。currentSlideValueChanged()
はcurrentSlideValue
ステートが変更された時に自動的に呼ばれるコールバックです。ここで#render()
を呼び、Stimulus controllerが管理するtargetを再描画します。これはStimulus Controllerの構造で紹介している構造と同じです#renderPaginationTargets()
, #renderSlideTargets()
は実際にtargetを再描画しているところです。currentSlideValue
ステートに応じて、aria-*
属性を指定しています#render()
では、ActionによってValuesステートからaria-*
属性を更新します。そしてCSS擬似セレクタでaria-*
を画面表示に反映させていますdata-*-values
としてHTML要素の属性になっています。これを変更すれば、リアルタイムでStimulus controllerのステートを変更できますので、バックエンドのERBからカスタマイズしたり、他のライブラリと接続する時に便利です