大家好,前幾天實做中有用到Bootstrap UI的Modal(彈出互動視窗)元件,因應這幾年使用者越來越注重UI的精緻程度,所以應用在開發上也會常來取代瀏覽器原生的alert與confirm。目前也有許多類似的套件也支援應用在框架中,像是Sweet Alert。今天我們就來嘗試一下用Javascript自己寫個Modal模組,並搭配Bootstrap應用在SPA的開發,在實做前先來了解一下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可能造成的狀況。
從上面的例子可以看出Modal需要事先在HTML中加入顯示內容並給予id值,透過jQuery對DOM的操作控制Modal顯示(display:block),而關閉時再改回隱藏(display:none),Modal從出現到隱藏其實都一直存在DOM中。在Modal只有少數幾個的情況下可能不會有什麼問題,但若有非常多個的Modal,例如一長串列表中的每筆資料裡放個按鈕,按下後都要能彈出視窗,可能就會造成以下情況:
我們希望Modal的內容可以搭配JS動態改變,把共用到的HTML整合在一起,且不需要事先建立HTML程式碼在畫面中,要顯示時才出現在DOM,隱藏時就從DOM消失,多個願望一次滿足,看來這裡需要建立一個函式模組做這件事情,秉持著實做的精神,今天就嘗試一下做個改良吧!
這邊要搭配之前用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()
})
以上完成後就開始來試試成果。
我們之前在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給移除,所以也不會造成效能的影響,明天我們再來看看如何做出可以產生更多互動的確認視窗。