舊寫法
舊的 JQuery Dialog。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 function changePasswordDialog ( ) { var htmlbody = "<input type.. id='input'>" ; htmlbody += "......" ; $("#memberdialog" ).html (htmlbody); dialog = $("#memberdialog" ).dialog ({ title : "XXX" , autoOpen : true , height : 400 , width : 520 , modal : true , closeOnEscape : true , buttons : { 修改: changePassword, 取消: function ( ) { $("#memberdialog" ).html ("" ); dialog.dialog ("close" ); }, }, open : function (event, ui ) { $("#" ).val ("" ); ........... }, close : function ( ) { $("#memberdialog" ).html ("" ); }, }); }
按「修改」後,會執行以下這段 changePassword()
函數。
1 2 3 4 5 6 7 8 9 10 function changePassword ( ) { ............. $.ajax ({ ........... success : function (rt ) { ................ $("#memberdialog" ).dialog ("close" ); }, }); }
新寫法
用 Vanilla JS 取代 JQuery 的 overlay 以及 scrollable 元件
用 ES6 模版字串符重構原本用 +
串的 JS 程式碼
用 ES6 解構賦值來取代原本的 rt[0].mid
這種寫法
用 TailwindCSS 取代原本的 inline-style
以及原始凌亂的 CSS class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 function changePasswordDialog ( ) { var htmlContent = ` <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"> <div class="sm:flex sm:items-start"> <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"> <h3 class="text-xl leading-6 font-medium text-gray-900" id="modal-headline">修改會員資料</h3> <div class="mt-2"> <div class="mt-2">請輸入舊密碼:<input type="text" id="oldPassword" name="oldPassword" class="border-2 border-slate-500 rounded-md"></div> <div class="mt-2">請輸入新密碼:<input type="text" id="newPassword" name="newPassword" class="border-2 border-slate-500 rounded-md"></div> <div class="mt-2">請再輸入一次新密碼:<input type="text" id="newPassword2" name="newPassword2" class="border-2 border-slate-500 rounded-md"></div> </div> </div> </div> </div>` ; CustomDialog .open ({ htmlContent : htmlContent, buttonsHTML : ` <button type="button" class="confirm-button inline-flex justify-center w-full rounded-md border border-transparent px-4 py-2 mx-2 bg-indigo-600 font-bold text-white shadow-sm hover:bg-indigo-500 focus:outline-none transition ease-in-out duration-150 sm:text-sm sm:leading-5"> 查詢 </button> <button type="button" class="cancel-button inline-flex justify-center w-full rounded-md border border-gray-300 px-4 py-2 mx-2 bg-white font-bold text-gray-700 shadow-sm hover:text-gray-500 focus:outline-none transition ease-in-out duration-150 sm:text-sm sm:leading-5"> 取消 </button> ` , onConfirm : function ( ) { changePassword (); }, onCancel : function ( ) { }, }); }
我也順便將整段程式碼重構
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 function changePassword ( ) { var oldPassword = $("#oldPassword" ).val (); var newPassword = $("#newPassword" ).val (); var newPassword2 = $("#newPassword2" ).val (); if (newPassword != newPassword2) { alert ("新的密碼,兩次輸入不相同" ); $("#oldPassword" ).focus (); return ; } if (newPassword.length < 4 ) { alert ("請輸入4個字元以上的新密碼!" ); $("#oldPassword" ).focus (); return ; } $.ajax ({ type : "post" , url : "/member/func" , dataType : "json" , data : { op : "changePassword" , oldPassword : oldPassword, newPassword : newPassword, }, complete : function ( ) {}, success : function (rt ) { if (rt["level" ] == 1 ) { appendMessage (rt["msg" ], true ); } else { appendMessage (rt["msg" ], false ); } }, }); }
將 Dialog Component 封裝成 IIFE
關於 IIFE 的基礎介紹可以參考此文章:JavaScript IIFE 筆記:使用立即呼叫函示改善程式碼結構
完整程式碼如下,但這不會是最終的版本,因為到時會依照後續遇到的各種不同情況來對這個 Dialog Component 程式碼進行微調及擴充。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 const CustomDialog = (function ( ) { var dialogElement; function createDialogElement (htmlContent, buttonsHTML ) { var dialogHTML = ` <div class="fixed z-10 inset-0 overflow-y-auto flex items-center justify-center"> <div class="inline-block align-middle bg-white rounded-lg text-left overflow-hidden shadow-2xl transform transition-all" role="dialog" aria-modal="true" aria-labelledby="modal-headline"> ${htmlContent} <div class="bg-gray-50 px-4 py-3 flex flex-row-reverse"> ${buttonsHTML} </div> </div> </div> ` ; var div = document .createElement ("div" ); div.innerHTML = dialogHTML; return div; } function openDialog (config ) { dialogElement = createDialogElement (config.htmlContent , config.buttonsHTML ); document .body .appendChild (dialogElement); const confirmButton = dialogElement.querySelector (".confirm-button" ); const cancelButton = dialogElement.querySelector (".cancel-button" ); confirmButton.addEventListener ("click" , function ( ) { config.onConfirm && config.onConfirm (); closeDialog (); }); cancelButton.addEventListener ("click" , function ( ) { config.onCancel && config.onCancel (); closeDialog (); }); document .addEventListener ("keydown" , function (event ) { if (event.key === "Escape" ) { closeDialog (); } }); } function closeDialog ( ) { if (dialogElement) { dialogElement.remove (); dialogElement = null ; } } return { open : openDialog, }; })();
為何要封裝成 IIFE?
一開始這段程式初期是直接寫在 Login screen
的 <script>
底下,但經過審思後發現此專案在非常多的頁面以及功能區都會用到 Dialog,因此若不將 Dialog 的程式碼模組化的話,會造成程式碼出現大量重複區塊
,導致不易維護。
程式碼說明
先大致講解一下程式碼的主要結構和功能:
var dialogElement;
:用於存儲當前打開的對話框元素,這個變數在 openDialog 函數中被賦值,並在 closeDialog 函數中被使用和重置。
在 closeDialog 函數中,dialogElement 用於移除當前的對話框元素並將其設置為 null:
1 2 3 4 if (dialogElement) { dialogElement.remove (); dialogElement = null ; }
這樣做的目的是當對話框被關閉時,將其從 DOM 中移除並釋放相關資源。將 dialogElement 設置為 null 是一種清理動作,以避免不必要的記憶體佔用。
function createDialogElement(htmlContent, buttonsHTML)
:私有函數,用於創建對話框的 HTML 元素。它接收兩個參數:htmlContent
和 buttonsHTML
,分別表示對話框的內容和按鈕的 HTML。
function openDialog(config)
:私有函數,用於打開對話框,此函數會創建對話框的 HTML 元素,並綁定按鈕和鍵盤事件。。它接收一個配置對象 config,該對象包含以下屬性:
htmlContent
: 對話框的 HTML 內容。
buttonsHTML
: 對話框的按鈕 HTML。
onConfirm
: 按下確認按鈕時的回調函數。
onValidate
: 按下驗證按鈕時的回調函數。
onCancel
: 按下取消按鈕時的回調函數。
function closeDialog()
:私有函數,用於關閉和移除對話框。
最後,IIFE 返回一個物件,該物件包含一個公開的 open 方法。這意味著,你可以使用 CustomDialog.open(config) 來打開一個自定義的對話框。closeDialog 函數在這個例子中是私有的,外部無法直接調用,只能通過在 openDialog 函數中綁定的事件來間接調用。
1 2 3 4 5 return { open : openDialog, };
這種寫法典型的使用場景是創建一個模組,並將一些功能封裝起來,只暴露出需要被外部調用的接口,
Dialog 功能擴充
增加掌控何時該關閉 Dialog 的功能
在一開始的 Dialog 程式能看到在我們自己寫的這個 CustomDialog
Component 中就很單純的判斷當 User 不管是按下確認、驗證按鈕後都會直接調用 closeDialog()
函數來關閉 Dialog。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 .... confirmButton.addEventListener ("click" , function ( ) { config.onConfirm && config.onConfirm (); closeDialog (); }); .... function closeDialog ( ) { if (dialogElement) { dialogElement.remove (); dialogElement = null ; } }
現在問題來了,當 User 填寫 Dialog 資料完畢,程式將資料送去後端經過檢查機制、判斷式、驗證完後,回傳的 response
未必每次都只有「成功」,也需考慮到可能 User 填寫的手機號碼、手機條碼…等等資料格式不正確,這時除了回傳錯誤訊息外,還有最重要、攸關使用者介面體驗的一點就是:若送出的資料格式有誤或是有其他狀況,則 Dialog 應保持開啟狀態讓使用者能不必在去按一下觸發 openDialog 的程式 。
那接下來直接看看怎麼修改程式碼:
為了讓我們在調用 Dialog 這個 IIFE 時,能夠掌控何時該關閉 Dialog,可以通過在 CustomDialog
的內部向 onConfirm
回調函數傳遞 closeDialog
函數作為參數來實現
1 2 3 4 5 6 ...... confirmButton.addEventListener ("click" , function ( ) { config.onConfirm && config.onConfirm (closeDialog); }); .....
接下來就能在外部的 onConfirm
回調函數中根據邏輯判斷式來決定是否調用 closeDialog
函數
1 2 3 4 5 6 7 8 CustomDialog .open ({ htmlContent : htmlContent, buttonsHTML : buttonsHTML, onConfirm : function (closeDialog ) { updateMemberData (closeDialog); }, onCancel : function ( ) {}, });
接下來就能在這邊根據後端回傳的 response
來決定是否調用 closeDialog
函數來關閉 Dialog。
1 2 3 4 5 6 7 8 9 10 11 12 13 function updateMemberData (closeDialog ) { success : function (rt ) { if (rt["level" ] == 1 ) { appendMessage (rt["msg" ], true ); closeDialog (); } else { appendMessage (rt["msg" ], false ); } }, }
擴充不同的按鈕功能
由於當初自己客製化這個 Dialog 沒有料想到為了應映不同的功能,在不同頁面的 Dialog 都各會有各自的按鈕功能&數量。
例如:發送手機簡訊驗證碼的 Dialog 有「發送驗證碼」、「取消」、「驗證」
三個按鈕,而重置密碼的 Dialog 則只有「重置」、「取消」
兩個按鈕。
因此為了避免發生 addEventListener reading null
的錯誤,我們必須在 CustomDialog.open
中加上判斷式,來判斷當前頁面的 Dialog 是否有此按鈕,若有則加上監聽事件,若無則不加 (代表此 Dialog 沒有用到這個功能)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ................ const confirmButton = dialogElement.querySelector (".confirm-button" );const cancelButton = dialogElement.querySelector (".cancel-button" );const validateButton = dialogElement.querySelector (".validate-button" );confirmButton && confirmButton.addEventListener ("click" , function ( ) { config.onConfirm && config.onConfirm (closeDialog); }); validateButton && validateButton.addEventListener ("click" , function ( ) { config.onValidate && config.onValidate (closeDialog); }); cancelButton && cancelButton.addEventListener ("click" , function ( ) { config.onCancel && config.onCancel (); closeDialog (); }); .............
能看到多增加了 validateButton
這個變數及與其關聯的事件處理器(addEventListener
)。
但並不是所有 Dialog 都會用到此功能,因此 validateButton
常常會是 null
的狀態,所以才要在每個 xxxButton.addEventListener
加上 &&
就可以避免在任何情況出現 addEventListener reading null
錯誤。也能不斷持續擴充 Dialog 的按鈕功能。
BUG 修正
由於剛剛將 JQuery 的 Dialog 改成自己客製化的 Dialog 組件了,
現在其中一個汰換程式的「忘記密碼的 Dialog」 內容已經從原本的 HTML 改成 JS 動態產生,不在是像此舊寫法 forgetpwform
一樣透過 $("#memberdialog").html(htmlbody);
來產生 Dialog 的內容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 function forgetpwform ( ) { var htmlbody = '請輸入您註冊時的個人資料(至少輸入二項資料,聯絡電話必填)<br><br>' ; htmlbody += '<table border=0 width=100% id=forgetpwtable>' ; ............. htmlbody += '<tr><td>電 話 必 填:</td><td><input type=text name=usermobile></td><td>E-mail:</td><td><input type=text name=useremail></td></tr>' ; .............. $("#memberdialog" ).html (htmlbody); dialog = $("#memberdialog" ).dialog ({ title : "查詢會員帳號" , autoOpen : true , height : 360 , width : 600 , modal : true , closeOnEscape : true , buttons : { "查詢" : resetPassword, "取消" : function ( ) { $("#memberdialog" ).html ("" ); dialog.dialog ("close" ); }, "清空" : function ( ) { $('input[name=username]' ).val ('' ); .............. } }, open : function (event, ui ) {}, close : function ( ) { $("#memberdialog" ).html ("" ); } }); };
而是改為用 htmlContent 來產生 Dialog 的內容,並且將原本的 forgetpwform()
改成 openresetPassworddDialog()
來呼叫自己寫的客製化 Dialog Component
並傳入createDialogHTML
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 function createDialogHTML ( ) { ............... var htmlContent = ` <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"> .................. <p>請輸入您註冊時的個人資料(至少輸入二項資料,聯絡電話必填)</p> <div class="mt-2">E-mail:<input type="text" name="useremail" class="border-2 border-slate-500 rounded-md"></div> ............ </div>` ; return htmlContent; } function openresetPassworddDialog ( ) { CustomDialog .open ({ htmlContent : createDialogHTML (), buttonsHTML : ` ......... ` , onConfirm : function ( ) { resetPassword (); }, onCancel : function ( ) { } }); } function resetPassword (resettag ) { if (resettag == 1 ) var resetnow = 1 ; else resetnow = 0 ; $.ajax ({ type : 'POST' , url : '/member/func' , dataType : 'JSON' , data : { ....... username : $('input[name=username]' ).val (), PID : $('input[name=userpid]' ).val (), usermobile : $('input[name=usermobile]' ).val (), .......... resetnow : resetnow }, complete : function ( ) {}, success : function (rt ) { var count = rt.count ; var check = rt.check ; var mem_LogID = rt.mem_LogID ; if (mem_LogID) { if (confirm ("您的登入帳號為" + mem_LogID + "\n是否要立即重置密碼?" )) { resetPassword (1 ); } } else { if (count == 0 || count == 1 ) alert ("資料不正確或查無資料...\n可能是您剛剛資料填寫錯誤,請詳細填寫資料後,再試一次" ); ........ else if (count == -2 ) { alert (check); document .getElementById ('memberdialog' ).style .display = 'none' ; } } } }); };
進而導致了表單的所有 data.val()
在 confirm 這邊再次重新調用 resetPassword()
時,由於剛剛動態產生的 Dialog 已經被關閉,所以此時若在 success
中再次 console.log
剛剛 createDialogHTML
表單提交的值的話會發現是 undefined
。
1 2 3 4 if (confirm ("您的登入帳號為" + mem_LogID + "\n是否要立即重置密碼?" )) { resetPassword (1 ); console .log ("Success後的DATA:" + usermobile); }
簡而言之,此問題的發生的原因 = DOM的變化
,resetPassword
函數遞迴地調用自己。這本身並不總是一個問題,但在這種情況下,它與 DOM 的變化相結合,導致無法在第二次調用時獲取輸入值。
解決方案
為了防止 DOM 發生變化導致動態產生的表單被關閉而讓 resetPassword()
無法取得表單的值,我們可以將 resetPassword()
的 AJAX
請求邏輯移動到另一個內部函數 executeSearch
中。
將 AJAX 請求邏輯移到新的內部函數
如下方程式碼所示,將 resetPassword()
的 AJAX
請求邏輯移動到另一個內部函數 executeSearch()
中,第一次調用時將表單的 input 值儲存在變數中。
再第一次調用 executeSearch
函數時獲取表單的值,這樣即使 DOM 在第二次調用時發生變化,我們仍然可以擁有用戶的 input 值。而不會變成 undefined
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 function resetPassword ( ) { var username = $('input[name="username"]' ).val (); var usermobile = $('input[name="usermobile"]' ).val (); ............... console .log ("Success前的data:" +usermobile); function executeSearch (resetnow ) { $.ajax ({ type : 'POST' , url : '/member/func' , dataType : 'JSON' , data : { query_op : 'user_info_check' , username : username, PID : $('input[name=userpid]' ).val (), usermobile : usermobile, ........ resetnow : resetnow }, complete : function ( ) {}, success : function (rt ) { console .log ("rt" + rt); ......... if (mem_LogID) { if (confirm ("您的登入帳號為" + mem_LogID + "\n是否要立即重置密碼?" )) { executeSearch (1 ); console .log ("Success後的DATA:" +usermobile); } } else { .................... } } }); } executeSearch (0 ); };
使用閉包來存儲值
通過在一個外部函數resetPassword
中使用一個內部函數 executeSearch
,我們可以利用 JavaScript 的閉包來存儲值。這樣即使在外部函數執行完畢後,內部函數仍然可以訪問這些值。透過這樣利用內部函數將輸入值作為參數傳遞,就能避免遞迴調用 resetPassword
導致的 DOM 變化而無法取得表單的值。
可以看到此時的 console.log
在 AJAX
傳送給後端程式碼執行回傳後,也已經可以正常取得表單的值了。
若您覺得這篇文章對您有幫助,歡迎分享出去讓更多人看到⊂◉‿◉つ~
留言版