iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 27
0
自我挑戰組

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

[DAY27]番外篇-用Javascript在SPA中實做Bootstrap Modal 之二

延續昨日的主題,我們用Javascript搭配Bootstrap,完成了Modal模組化實做Alert Modal(警告視窗),為了延伸更多的互動,今天再來嘗試做一個Confirm Modal(確認視窗)。

先來看一下完成的結果:

這個是一個填寫表單的功能,當使用者按下「填寫表單」按鈕會跳出視窗進行填寫,填寫完畢按下送出前,會再跳出一個視窗確認是否送出,等待所有程序完成,才會把資料送出並關閉所有視窗。

建立表單

STEP1:準備UI

首先我們建立一個新的Contact元件(Component),路徑會放在src/pages下,並記得到src/routes/Route.js裡設定路徑:

src/pages/Contact.js

export const Contact = {
  render: () => {
  },
}

然後開頭引入會用到的模組,把UI放入render中,等等後面會使用到:

src/pages/Contact.js

//引入jQuery
import $ from 'jquery'
 
//引入外框組件
import { App } from './App'
 
//引入Modal模組
import { Modal } from '../components/Modal'
 
//引入fontawesome
import fontawesome from '@fortawesome/fontawesome'
import { faComment } from '@fortawesome/fontawesome-free-regular'
fontawesome.library.add(faComment)
 
export const Contact = {
  render: () => {
    const content = `
      <div class="container">
        <h1>填寫表單</h1>
        <h6>請點擊下方按鈕填寫表單</h6>
        <button class="btn btn-danger" id="button"><i class="far fa-comment"></i> 填寫表單</button>
      </div>
    `
 
    return App.render(content)
  },
}

STEP2:撰寫功能

UI準備好了,接下來依序來撰寫功能,先列出功能清單:

  • 對按鈕綁定監聽事件,按下會跳出表單視窗
  • 表單填寫後,按下送出按鈕,會再跳出確認送出的視窗
  • 按下確認送出,執行資料送出

清楚流程後,先對按鈕新增click監聽事件,前面我們有實做在Component加入監聽的方法,在Contact物件中新增一個listener屬性,運用Event Delegation對事件的冒泡階段來執行:

src/pages/Contact.js

export const Contact = {
  listener: {
    click: function (e) {
      if (
        e.target.closest('button') &&
        e.target.closest('button').id === 'button'
      ) {
        //呼叫modal
      }
    },
  },
  render: () => {
  //...

PS.若不使用上面的方式綁定監聽事件,也可以用mount方法直接對按鈕綁定onclick事件:

src/pages/Contact.js

export const Contact = {
  mount: () => {
    document.querySelector('#button').onclick = () => {
      //呼叫modal
    }
  },
  //...

接著要建立第一個彈出的表單視窗,我們先宣告要傳入Modal函式模組的參數。在作用範圍內宣告formModal,作為表單的UI,因為要在表單送出前彈出確認視窗,所以又宣告了一個formCB,作為彈出表單後的回呼函式:

src/pages/Contact.js

export const Contact = {
  listener: {
    click: function (e) {
      if (
        e.target.closest('button') &&
        e.target.closest('button').id === 'button'
      ) {
        //呼叫modal
        //表單UI
        const formModal = `
          <form>
            <div class="modal-header">
              <h5 class="modal-title">留言給river</h5>
              <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                <span aria-hidden="true">×</span>
              </button>
            </div>
            <div class="modal-body">
                <div class="form-group">
                  <label for="name">稱呼</label>
                  <input type="text" class="form-control" id="name" placeholder="請填寫您的稱呼" required>
                </div>
                <div class="form-group">
                  <label for="email">Email</label>
                  <input type="email" class="form-control" id="email" placeholder="請填寫email" required>
                </div>
                <div class="form-group">
                  <label for="message">留言內容</label>
                  <textarea type="email" class="form-control" id="message" placeholder="請填寫內容" required></textarea>
                </div>
            </div>
            <div class="modal-footer">
              <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
              <button type="submit" class="btn btn-primary" id="submit">提交</button>
            </div>
          </form>
        `
        //彈出表單後的callback function
        const formCB = () => {          
          console.log('form callback')
        }
 
        //彈出填寫表單視窗
        Modal('formModal', formModal, formCB, true)
      }
    },
  },
  render: () => {
  //...

最後執行Modal函式模組,參數的定義分別為modal ID、Modal內容、callback及彈出前移除其他Modal (可參考[DAY26]番外篇-用Javascript在SPA中實做Bootstrap Modal)。

先來看一下到目前階段的成果,執行npm run build運行webpack,打開瀏覽器到 localhost/#/contact,剛剛傳入的callback會在彈出視窗後console顯示:

到目前都沒問題! 接著下一步就是要在送出表單時彈出確認視窗,所以針對formCB這個callback function來撰寫:

src/pages/Contact.js

//彈出表單後的callback function
const formCB = () => {
  //form表單
  const form = document.querySelector('#formModal form')
 
  //綁定表單提交事件
  form.onsubmit = (event) => {
    //取消提交表單預設事件
    event.preventDefault()
    
  }
}

這裡對form綁定onsubmit事件,為了避免在彈出確認視窗時真的送出,以preventDefault來取消預設事件行為。

再來我們要處理第二個彈出的確認視窗,所以會再度用到Modal函式模組。跟上面第一個視窗的流程一樣,宣告並加入確認視窗的UI,以及按下確認的callback:

src/pages/Contact.js

//彈出表單後的callback function
const formCB = () => {
  //form表單
  const form = document.querySelector('#formModal form')
 
  //綁定表單提交事件
  form.onsubmit = (event) => {
    //取消提交表單預設事件
    event.preventDefault()
    //確認視窗內容
    const confirmModal = `
      <form>
        <div class="modal-header">
          <h5 class="modal-title">留言給river</h5>
        </div>
        <div class="modal-body">
          <h5>確定要提交嗎?</h5>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-secondary" data-dismiss="modal">否</button>
          <button type="button" class="btn btn-primary" id="confirm">是</button>
        </div>
      </form>
    `
 
    //完成提交表單的callback
    const confirmCB = () => {
      console.log('confirm callback')
    }
    //彈出確認視窗(不關閉前個視窗)
    Modal('confirmModal', confirmModal, confirmCB, false)
  }
}

這裡要注意的是,在呼叫第二個Modal時,要把最後一個參數值設定為false,這樣才不會把前一個表單視窗給移除。

然後繼續完成在confirmCB這個回呼函式,我們對確認按鈕綁定onclick事件,當按下確認後才會正式送出,並關掉所有視窗:

src/pages/Contact.js

//完成提交表單的callback
const confirmCB = () => {
  //confirm按鈕
  const confirm = document.querySelector('#confirmModal #confirm')
 
  confirm.onclick = () => {
    console.log('完成提交!')
    //關閉所有modal
    $('[data-modal="true"]').modal('hide')
  }
}

彈出視窗的程序都處理完成了,來看看測試看看結果如何。

STEP3:測試結果

非常好,彈出視窗的流程如同前面規劃,當按下確認後會看到console有完成的紀錄! 不過仔細一看,好像哪裡怪怪的R…

為Modal處理圖層顯示順序(z-index)

雖然經過我們一番努力之下程式都正確運行,但仔細一看會發現,彈出第二個確認視窗時,背景的圖層順序怪怪的。記得前一篇實做提到Bootstrap Modal的特性嗎?Modal彈出時會在DOM裡的body結尾前插入modal與modal-backdrop這兩個DIV元件,第一個彈出的表單在DOM順序為於第二個彈出的確認視窗之上,確認視窗會疊在上面無誤,但Bootstrap對modal預設的z-index是1050,modal-backdrop則是1040,所以視窗永遠在那層背景之上,這樣感受不到層次。

那麼該怎麼解呢? 我們要回去前一天實做的Modal模組做點小補強,自動判斷彈出視窗的z-index圖層順序,並設定在元件的inline style樣式(請看註解新增部份):

src/components/Modal.js

import $ from 'jquery'
 
export const Modal = (id, content, callback, removeModal = true) => {
  //1.預設處理畫面中原有modal
  if (removeModal) {
    if (document.querySelectorAll('[data-modal="true"]').length) {
      document
        .querySelectorAll('[data-modal="true"]')
        .forEach((v) => v.remove())
    }
  }
 
  //新增部份:判斷畫面中是否有modal
  let zIndexLevel =
      document.querySelectorAll('[data-modal="true"]').length * 10,
    zIndexModal = 1050,
    zIndexBackdrop = 1040
 
  //2.定義新創建的modal物件
  const modal = document.createElement('div')
  //賦予modal ID
  modal.id = id
  //賦予modal class
  modal.className = 'modal modal-alert fade'
  modal.setAttribute('data-modal', 'true')
  //設定modal是否可被使用者取消
  modal.setAttribute('data-backdrop', 'static')
 
  //新增部份:處理modal本身的z-index
  modal.style.zIndex = zIndexModal + zIndexLevel
 
  modal.innerHTML = `
    <div class="modal-dialog">
      <div class="modal-content">
        ${content}
      </div>
    </div>
  `
 
  //3.新增至DOM
  document.querySelector('body').appendChild(modal)
 
  //4.執行跳出
  $(`#${id}`).modal('show')
 
  //新增部份:處理Backdrop z-index
  document.querySelector('.modal-backdrop:last-child').style.zIndex =
    zIndexBackdrop + zIndexLevel
 
  //5.callback after showing modal
  $(`#${id}`).on('shown.bs.modal', function (e) {
    callback ? callback() : null
  })
  //callback after modal is hidden
  $(`#${id}`).on('hidden.bs.modal', function (e) {
    document.querySelector(`#${id}`).remove()
  })
}

補強之後可以看到執行Modal函式模組時,會自動對modal與modal-backdrop的DIV加入inline style,處理Modal的z-index能夠讓圖層的順序正常顯示,實現了多個視窗彈出效果,以上是今天的實做內容。

/images/emoticon/emoticon08.gif


上一篇
[DAY26]番外篇-用Javascript在SPA中實做Bootstrap Modal
下一篇
[DAY28]番外篇-使用fetch發送請求
系列文
不用前端框架 手把手打造基礎SPA網站30

尚未有邦友留言

立即登入留言