iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 26
0
自我挑戰組

不用前端框架 手把手打造基礎SPA網站系列 第 26

[DAY26]番外篇-用Javascript在SPA中實做Bootstrap Modal

大家好,前幾天實做中有用到Bootstrap UI的Modal(彈出互動視窗)元件,因應這幾年使用者越來越注重UI的精緻程度,所以應用在開發上也會常來取代瀏覽器原生的alert與confirm。目前也有許多類似的套件也支援應用在框架中,像是Sweet Alert。今天我們就來嘗試一下用Javascript自己寫個Modal模組,並搭配Bootstrap應用在SPA的開發,在實做前先來了解一下Modal這個元件的特性:

Modal

Modal本身其實就是html加上CSS樣式顯示的元件,結構最外層是div class="modal",可以搭配option來設定視窗的狀態;再來內部有div class="modal-dialog",這層可以設定顯示的位置與大小,div class="modal-content"這層裡面是包真正要顯示在Modal的內容,內容可以分為modal-header / modal-body / modal-footer 三個部份。

Modal本身預設顯示屬性是隱藏的(display:none),所以要搭配jQuery綁定DOM元素操作達成互動效果。Bootstrap官方給的Methods中操作顯示/隱藏的方法如下:

//顯示
$('#myModal').modal('show')
//隱藏
$('#myModal').modal('hide')

或是對目標設定data開頭的標籤,直接在要觸發Modal的目標上綁定data-toggle="modal"與data-target這兩個屬性,data-target對應Modal本身id進行綁定,以下是綁定在按鈕觸發Modal方式:

<button type="button" data-toggle="modal" data-target="#myModal">Launch modal</button>

來看看實際彈出視窗時DOM會怎麼變化:

可以看到視窗彈出後,本身樣式display的屬性由none改變為block,另外在body最末端會多出一個div class="modal-backdrop fade show"黑色半透明背景,當視窗關閉時只有modal-backdrop會從DOM消失,但Modal本身還存在。了解基本原理後,我們來說說SPA中使用原生的Bootstrap Modal可能造成的狀況。

原生Bootstrap Modal的限制

從上面的例子可以看出Modal需要事先在HTML中加入顯示內容並給予id值,透過jQuery對DOM的操作控制Modal顯示(display:block),而關閉時再改回隱藏(display:none),Modal從出現到隱藏其實都一直存在DOM中。在Modal只有少數幾個的情況下可能不會有什麼問題,但若有非常多個的Modal,例如一長串列表中的每筆資料裡放個按鈕,按下後都要能彈出視窗,可能就會造成以下情況:

  • 每個Modal內容不同時,需要事先建立多個Modal
  • 程式碼難以維護,會堆疊重複的html代碼並混雜在畫面中。
  • 大量Modal存在DOM中會影響前端效能

我們希望Modal的內容可以搭配JS動態改變,把共用到的HTML整合在一起,且不需要事先建立HTML程式碼在畫面中,要顯示時才出現在DOM,隱藏時就從DOM消失,多個願望一次滿足,看來這裡需要建立一個函式模組做這件事情,秉持著實做的精神,今天就嘗試一下做個改良吧!

建立Modal函式模組

這邊要搭配之前用webpack實做SPA建立的環境,首先在components目錄下建立一個Modal.js,這是個函式模組作為給SPA裡呼叫Modal使用,因為會用到Modal的API,所以開頭記得要引入jQuery:

src/components/Modal.js

import $ from 'jquery'
 
export const Modal = () => {
}

再來一樣規劃寫程式的步驟,首先看到函式的預設參數,因為要用jQuery對Modal控制顯示/隱藏,需要對Modal指定一個ID值。接著我們希望Modal的內容是可以動態改變的,所以指定一個content,將HTML內容放在modal-content裡;再來我們希望在視窗彈出後,可以帶入要執行的回呼函式callback,來進行控制後續行為;最後再設定一個參數removeModal,用來判斷顯示前是否將畫面中的所有視窗給移除(預設為true時代表要移除)。一切準備就緒就來寫function內的程式:

export const Modal = (id, content, callback, removeModal = true) => {
  //1.預設處理畫面中原有modal
  
  //2.建立modal物件
  
  //3.新增至DOM
  
  //4.執行跳出modal
  
  //5.顯示時執行的回呼函式
  
  //6.隱藏時移除modal
}

1.預設處理畫面中原有modal

這裡我們先用選擇器找尋畫面中是否存在Modal,並用forEach來做移除DOM的動作。

//1.預設處理畫面中原有modal
if (removeModal) {
  if (document.querySelectorAll('[data-modal="true"]').length) {
    document
      .querySelectorAll('[data-modal="true"]')
      .forEach((v) => v.remove())
  }
}

2.建立modal物件

接著我們宣告modal,使用document.createElement這個語法建立DOM元件,給予id、class與modal的data標籤屬性,然後在裡面放入content這個參數的HTML程式碼。

//2.定義新創建的modal物件
const modal = document.createElement('div')
//賦予modal ID
modal.id = id
//賦予modal class
modal.className = 'modal fade'
modal.setAttribute('data-modal', 'true')
modal.setAttribute('data-backdrop', 'static')
 
modal.innerHTML = `
  <div class="modal-dialog">
    <div class="modal-content">
      ${content}
    </div>
  </div>
`

3.新增至DOM

建立完畢後用appendChild語法,直接插入元素到body末端前面。

//3.新增至DOM
document.querySelector('body').appendChild(modal)

4.執行跳出

這個比較簡單,直接用jQuery呼叫顯示。

//4.執行跳出modal
$(`#${id}`).modal('show')

5.顯示時執行的回呼函式

這個部份要搭配Modal的Event API,監聽Modal彈出後事件,並在觸發的函式中放入callback參數。

//5.modal顯示時執行的回呼函式
$(`#${id}`).on('shown.bs.modal', function (e) {
  callback ? callback() : null
})

6.隱藏時移除modal

//6.隱藏時移除modal
$(`#${id}`).on('hidden.bs.modal', function (e) {
  document.querySelector(`#${id}`).remove()
})

以上完成後就開始來試試成果。

使用Modal模組

我們之前在Home.js中有實做彈出視窗,就來試試看剛剛建立的Modal模組吧,首先貼出原來的程式碼:

src/pages/Home.js

import { App } from './App'
import $ from 'jquery'
 
export const Home = {
  mount: function () {
    $('#modal').modal('show')
  },
  render: () => {
    const modal = `
      <div class="modal" tabindex="-1" id="modal">
        <div class="modal-dialog">
          <div class="modal-content">
            <div class="modal-header">
              <h5 class="modal-title">重要公告</h5>
              <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                <span aria-hidden="true">×</span>
              </button>
            </div>
            <div class="modal-body">
              <p>這裡是重要公告的內容....blablabla</p>
            </div>
          </div>
        </div>
      </div>
    `
    const content = `
      <div class="container">
        <h1>Home page</h1>
        <div>Welcome to my page!</div>
      </div>
      ${modal}
    `
 
    return App.render(content)
  },
}

然後這裡可以看到原本是將Modal顯示的內容放在render方法裡,在mount裡呼叫顯示視窗,我們要做的是將Modal內容從畫面中抽離出來,塞到mount方法中搭配剛建立的Modal模組,最後完成會是以下的結果:

import { App } from './App'
//引入模組
import { Modal } from '../components/Modal'

export const Home = {
  mount: function () {
    //Modal內容
    const content = `
      <div class="modal-header">
        <h5 class="modal-title">重要公告</h5>
        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
          <span aria-hidden="true">×</span>
        </button>
      </div>
      <div class="modal-body">
        <p>這裡是重要公告的內容....blablabla</p>
      </div>
    `
    
    //callback
    const showLog = () => {
      console.log('show modal!')
    }
    
    //呼叫彈出視窗模組
    Modal('modal', content, showLog, true)
  },
  render: () => {
    const content = `
      <div class="container">
        <h1>Home page</h1>
        <div>Welcome to my page!</div>
      </div>
    `

    return App.render(content)
  },
}

成果一覽:

登登!這樣一個警示視窗就算完成了,比原生的alert好看許多,Modal內容不用事先加入到HTML裡,取而代之的是將內容設定在JS中,Modal只有在顯示時會出現在DOM,隱藏時自動會從DOM消失。以上就是今天實做模組化的內容,透過這樣的方法,可以來有效管理我們的程式碼,雖然這是操作DOM來達成的方法,不過因為在顯示前後都會把Modal給移除,所以也不會造成效能的影響,明天我們再來看看如何做出可以產生更多互動的確認視窗。

/images/emoticon/emoticon12.gif


上一篇
[DAY25]番外篇-使用Font Awesome來加入個性化圖示
下一篇
[DAY27]番外篇-用Javascript在SPA中實做Bootstrap Modal 之二
系列文
不用前端框架 手把手打造基礎SPA網站30

尚未有邦友留言

立即登入留言