2021, Jun 25

【Rails】Stimulus.jsでmodal(モーダル)を作る

Stimulus.jsでモーダル画面の実装をしました。

使用環境

Rails 6.1.3.2
Stimulus.js 2.0.0

参考

こちらを参考にさせてもらいました。

https://github.com/excid3/tailwindcss-stimulus-components/blob/master/src/modal.js

事前準備

Rails 新規プロジェクト作成

rails new modal-sample
cd modal-sample

Stimulus.js のインストール

bundle exec rails webpacker:install:stimulus

※私の環境では TailwindCSS を使ってスタイルしています。

適当なコントローラーを作る

ページがないと始まらないので適当なコントローラーを作りましょう。

rails g controller modals index

view に生成された/modals/index.html.erbにモーダル用のボタンを配置します。

<!-- index.html.erb -->
<div data-controller="modal" class="flex justify-center my-20">
  <button data-action="click->modal#open" class="p-4 bg-gray-600 text-white">
    モーダルボタン
  </button>
  <%= render 'modal' %>
</div>

モーダルの部分はパーシャルで分割しました。

<!-- _modal.html.erb -->
<!-- Modal Container -->
<div
  data-target="modal.container"
  data-action="click->modal#closeBackground keyup@window->modal#closeWithKeyboard"
  class="hidden bg-gray-300 bg-opacity-50 fixed inset-0 overflow-y-auto flex items-center justify-center"
  style="z-index: 9999;"
>
  <div class="max-h-screen w-full max-w-2lg relative">
    <div class="bg-white shadow-xl">
      <div class="p-8">
        <h2 class="text-xl mb-4">モーダルウィンドウ</h2>
        <p class="mb-4">テストモーダル.</p>
        <div class="flex justify-end items-center flex-wrap mt-6">
          <button
            class="bg-blue-600 hover:bg-blue-700 text-white py-2 px-4"
            data-action="click->modal#close"
          >
            Close
          </button>
        </div>
      </div>
    </div>
  </div>
</div>

これで HTML 部分は完成です。

javascript を書く

以下のパスにmodal_controller.jsを新規作成します。 app/javascript/controllers/modal_controller.js

そして以下のように記述します。(参考サイトそのまま)

// app/javascript/controllers/modal_controller.js

import { Controller } from 'stimulus'

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

  connect() {
    // The class we should toggle on the container
    this.toggleClass = this.data.get('class') || 'hidden'

    // The ID of the background to hide/remove
    this.backgroundId = this.data.get('backgroundId') || 'modal-background'

    // The HTML for the background element
    this.backgroundHtml =
      this.data.get('backgroundHtml') || this._backgroundHTML()

    // Let the user close the modal by clicking on the background
    this.allowBackgroundClose =
      (this.data.get('allowBackgroundClose') || 'true') === 'true'

    // Prevent the default action of the clicked element (following a link for example) when opening the modal
    this.preventDefaultActionOpening =
      (this.data.get('preventDefaultActionOpening') || 'true') === 'true'

    // Prevent the default action of the clicked element (following a link for example) when closing the modal
    this.preventDefaultActionClosing =
      (this.data.get('preventDefaultActionClosing') || 'true') === 'true'
  }

  disconnect() {
    this.close()
  }

  open(e) {
    if (this.preventDefaultActionOpening) {
      e.preventDefault()
    }

    e.target.blur()

    // Lock the scroll and save current scroll position
    this.lockScroll()

    // Unhide the modal
    this.containerTarget.classList.remove(this.toggleClass)

    // Insert the background
    if (!this.data.get('disable-backdrop')) {
      document.body.insertAdjacentHTML('beforeend', this.backgroundHtml)
      this.background = document.querySelector(`#${this.backgroundId}`)
    }
  }

  close(e) {
    if (e && this.preventDefaultActionClosing) {
      e.preventDefault()
    }

    // Unlock the scroll and restore previous scroll position
    this.unlockScroll()

    // Hide the modal
    this.containerTarget.classList.add(this.toggleClass)

    // Remove the background
    if (this.background) {
      this.background.remove()
    }
  }

  closeBackground(e) {
    if (this.allowBackgroundClose && e.target === this.containerTarget) {
      this.close(e)
    }
  }

  closeWithKeyboard(e) {
    if (
      e.keyCode === 27 &&
      !this.containerTarget.classList.contains(this.toggleClass)
    ) {
      this.close(e)
    }
  }

  _backgroundHTML() {
    return `<div id="${this.backgroundId}" class="fixed top-0 left-0 w-full h-full" style="background-color: rgba(0, 0, 0, 0.8); z-index: 9998;"></div>`
  }

  lockScroll() {
    // Add right padding to the body so the page doesn't shift
    // when we disable scrolling
    const scrollbarWidth =
      window.innerWidth - document.documentElement.clientWidth
    document.body.style.paddingRight = `${scrollbarWidth}px`

    // Save the scroll position
    this.saveScrollPosition()

    // Add classes to body to fix its position
    document.body.classList.add('fixed', 'inset-x-0', 'overflow-hidden')

    // Add negative top position in order for body to stay in place
    document.body.style.top = `-${this.scrollPosition}px`
  }

  unlockScroll() {
    // Remove tweaks for scrollbar
    document.body.style.paddingRight = null

    // Remove classes from body to unfix position
    document.body.classList.remove('fixed', 'inset-x-0', 'overflow-hidden')

    // Restore the scroll position of the body before it got locked
    this.restoreScrollPosition()

    // Remove the negative top inline style from body
    document.body.style.top = null
  }

  saveScrollPosition() {
    this.scrollPosition = window.pageYOffset || document.body.scrollTop
  }

  restoreScrollPosition() {
    document.documentElement.scrollTop = this.scrollPosition
  }
}

完成

ページを表示してみると、、、

2021 06 25 095006

ボタンを押すと、、、

2021 06 25 095020

モーダルが表示されます! モーダルの外をクリックしても解除され、モーダルの裏はスクロールがロックされています。

素晴らしい!

今回はこれで以上です。