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-lg 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

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

素晴らしい!

今回はこれで以上です。