昨天看了《Pro Javascript Techniques》中的用js改進表單的那章,自己把代碼敲了一遍,用firebug調試了半天,發現了書中的一些小問題,我會在下面複習的過程中提出,歡迎大家留言討論 : )
在表單方面,驗證使用者輸入是否合法是個重頭,我就直接拿書中的例子展開吧,先給出語義化的XHTML
<body><form action="" method="post"><fieldset class="login"><legend>Login Information</legend><label for="username" class="hover">Username</label><input type="text" id="username" class="required text"/><br/><label for="password" class="hover">Password</label> <input type="password" id="password" class="required text"/></fieldset><fieldset><legend>Personal Information</legend><label for="name">Name</label><input type="text" id="name" class="required text"/><br/><label for="email">Email</label><input type="text" id="email" class="required email text"/><br/><label for="date">Date</label><input type="text" id="date" class="required date text"/><br/><label for="url">Website</label><input type="text" id="url" class="url text" value="http://"/><br/><label for="phone">Phone</label><input type="text" id="phone" class="phone text"/><br/><label for="age">Over 13?</label><input type="checkbox" id="age" name="age" value="yes"/><br/><input type="submit" value="Submit Form" class="submit"/></fieldset></form></body>
在表單內,所有<input>元素都精細地分好類(比如,type為text的元素其class也為text,必填項的class為required為js的運用做個鉤子),並與正確的label一起被包含在合適的fieldset內。並配上css使得更美觀一些:
<style type="text/css">form {font-family:Arial;font-size:14px;width:300px;}fieldset {border:1px solid #ccc;margin-bottom:10px;}fieldset.login input {width:125px;}legend {font-weight:bold;font-size:1.1em;}label {display:block;width:60px;text-align:right;float:left;padding-right:10px;margin:5px 0px;}input {margin:5px 0;}input.text {padding:0 0 0 3px;width:172px;}input.submit {margin:15px 0 0 70px;}</style>
希望對js感興趣並且學習不久的園友能夠自己親自敲下代碼,體驗一下。
出來的效果就像這樣:
給出了樣式原型後,作者寫了一些驗證函式做引導,介紹些Regex匹配的用法。我們跳過這些直接進入重點,先來看js驗證的規則集合,可以注意到所有測試都分別需要通用的名稱和語義化的錯誤資訊。下面給出本例中使用的驗證規則集合:
var errMsg = {required:{ msg:'This field is required.', test:function(obj) { return trim(obj.value).length > 0 || trim(obj.value) != obj.defaultValue; }},email: { msg:'Not a valid email address.', test:function(obj) {return trim(obj.value).length <= 0 || /^[a-z0-9_+.-]+\@([a-z0-9-]+\.)+[a-z0-9]{2,4}$/i.test(obj.value); }},phone: { msg:'Not a valid phone number.', test:function(obj) { var m = /(\d{3}).*(\d{3}).*(\d{4})/.exec( obj.value ); if(m) obj.value = "(" + m[1] + ") " + m[2] + "-" + m[3]; return trim(obj.value).length <= 0 || m; }},date: { msg:'Not a valid date.', test:function(obj) { return trim(obj.value).length <= 0 || /^\d{2}\/\d{2}\/\d{2,4}$/.test(obj.value); } },url: { msg:'Not a valid URL.', test:function(obj) { return trim(obj.value).length <= 0 || obj.value == 'http://' || /^https?:\/\/([a-z0-9-]+\.)+[a-z0-9]{2,4}.*$/.test(obj.value); }}}
這裡我已經對驗證規則做了修改,原文中對於必填項(required)的驗證:return obj.value.length > 0這個子條件有問題,如果只是輸入了一連串空格而無實質性的內容,難道也讓它驗證通過嗎?很明顯不行,所以用trim函數來去掉空格在來判斷才正確。
function trim(value) {return value.replace(/(^\s*)|(\s*$)/g,'');}
這個函數用replace方法通過匹配Regex去掉字串首尾空格,很常用。對於下面一些其他欄位的判斷如email,date等的判斷也是這個問題,這裡有個細節,像email這種需要特定格式驗證的欄位,得先看看使用者輸入了東西沒,如果沒有那也沒必要判斷,所以用trim(obj.value).length <= 0如果為true那麼這個||運算就到此為止了,說明使用者沒輸入什麼東西,也就沒必要判斷格式了,反之如果為false,就說明有內容了,並且需要判斷下。
規則給出來了,接著我們該用這些規則判斷,然後給出必要的提示資訊。先介紹個知識點:所有<form>元素(在DOM中)都有一個被稱為elements的屬性,這個屬性是包含表單所有欄位的數組,使用這個數組就可以輕鬆遍曆所有可能的欄位,並檢查錯誤。在我debug的過程中,發現不光使用者輸入相關的<input>元素在elements中,兩個<fieldset>元素也在其中...下面給出具體的驗證相關的函數。
//驗證表單所有欄位的函數//form是個表單元素的引用function validateForm(form) { var valid = true; //遍曆表單的所有欄位元素 //form.elements是表單所有欄位的一個數組 for(var i = 0; i < form.elements.length; i++) { //先隱藏任何錯誤資訊,以防不意的顯示 hideErrors(from.elements[i]); //檢查欄位是否包含正確的內容 //註:原書中條件上加了個取反運算子'!',看了下面的validateField就知道這裡有問題 if ( validateField(form.elements[i]) ) valid = false; } return valid; }//驗證單個欄位的內容function validateField(elem) { var errors = []; //遍曆所有可能的驗證技術 for(var name in errMsg ) { //查看欄位是否有錯誤類型指定的class var re = new RegExp('(^|\\s)' + name + '(\\s|$)'); //檢查元素是否帶有該class並把它傳遞給驗證函式 if( re.test(elem.className) && !errMsg[name].test(elem) ) //如果沒有通過驗證,把錯誤資訊增加到列表中 errors.push( errMsg[name].msg ); } //如果存在錯誤資訊,則顯示出來 if(errors.length) showErrors( elem, errors ); return errors.length > 0; //>0說明有錯誤資訊} /*顯示和隱藏相應欄位的錯誤資訊*///隱藏當前正顯示的任何錯位資訊function hideErrors(elem) { //擷取當前欄位的下一個元素 var next = elem.nextSibling; //如果下一個元素是ul並有class為errors if(next && next.nodeName == 'UL' && next.className == 'errors') //刪掉它(這是我們'隱藏'的含義) elem.parentNode.removeChild(next);}//顯示表單內特定欄位的錯誤資訊function showErrors(elem, errors) { //擷取當前欄位的下一個元素 var next = elem.nextSibling; //如果該欄位不是我們指定的包含錯誤的容器 if( next && ( next.nodeName != 'UL' || next.className != 'errors' ) ) { //我們得產生一個 next = document.createElement('ul'); next.className = 'errors'; //並在DOM中把它插入到恰當的地方 //從HTML中看,這裡的elem.nextSibling指向的是'<br/>', //而<ul>本身預設的display為block elem.parentNode.insertBefore( next, elem.nextSibling ); } //現在有了一個包含錯誤的容器引用,我們可以遍曆所有的錯誤資訊了 for(var i = 0; i < errors.length; i++) { var li = document.createElement('li'); li.innerHTML = errors[i]; //並插入到DOM中 next.appendChild( li ); }}
在配上錯誤資訊的css樣式:
ul.errors { list-style:none; background:#ffcece; padding:3px; margin:3px 0 3px 70px; font-size:0.9em; width:165px;}
先看下示範效果:
不算很漂亮,但也還算不錯...接下來我們就著手啟動驗證,對於何時驗證,作者進行了一些討論,如在提交時進行統一驗證,或是填寫時單獨的對每個元素進行驗證。我先給出作者的代碼:
/*自己寫的事件輔助函數*/function addEvent(obj, event, eventHandler) { if(obj.addEventListener) { obj.addEventListener(event, eventHandler, false); } else if ( obj.attachEvent ) { obj.attachEvent(event, eventHandler); }}/*前面文章中提到過的stopDefault函數*/function stopDefault( e ) { if(e && e.preventDefault) e.preventDefault(); else window.event.returnValue = false; return false;}/** * 在表單提交時進行驗證 *///在表單提交的時候運行表單驗證的函數function watchForm( form ) { //監聽表單的提交事件 addEvent( form, 'submit', function() { return validateForm( form ); });}addEvent(window, 'load', function() { var form = document.getElementsByTagName('form')[0]; watchForm( form );});/** * 在欄位改變時驗證 */function watchFields( form ) { //遍曆表單內的所有欄位 for(var i = 0 ; i < form.elements.length; i++) { //並綁定'change'事件處理函數(它監聽input元素的失焦) addEvent(form.elements[i], 'change', function() { //一旦失去焦點,重驗證該欄位 return validateField (this); }); }} addEvent(window, 'load', function() { var form = document.getElementsByTagName('form')[0]; //監聽表單所有欄位變化 watchFields( form );});
我一開始按照書上的代碼進行測試,發現一些問題,比如用'change'來監聽的話,我先讓一個input.required獲得焦點,然後我再讓它失去焦點,就沒有任何即時的反饋,因為input中的內容沒有改變,還有一個問題,如果我email輸錯一次,它會給出一個錯誤資訊,我再次輸錯,它就給出了兩條重複的錯誤資訊...更嚴重的是即使有錯誤,'submit'的綁定函數返回了false還是提交了,這裡我自己也有點疑惑,後來用了上面的stopDefault函數,就有想要的效果了,個人認為應該把單個input元素的即時驗證和表單提交整體驗證結合起來,下面給出我修改後的代碼:
/*修改後的watchFields*/function watchFields(form) { for(var i = 0; i < form.elements.length; i++) { addEvent(form.elements[i], 'blur', function() { //先去掉原先的錯誤資訊,防止出現重複錯誤資訊 hideErrors(this); //add by myself return validateField(this); }); }}/*修改後的watchForm*/function watchForm(form) { addEvent(form, 'submit', function(e) { //如果驗證沒有通過,阻止提交事件 if ( !validateForm( form ) ) stopDefault(e); });}addEvent(window, 'load', function() { var form = document.getElementsByTagName('form')[0]; //監聽每個input元素的blur事件 watchFields(form); //監聽form的提交事件 watchForm(form);});
ok,寫到這裡算是把問題解決完了,完整執行個體下載, 如果有什麼問題歡迎大家討論: )