表單驗證：中文姓名、身分證、手機條碼、生日、信箱、手機、選擇縣市
Tags: javascript, html, beginners, tutorial
Published: 2022-06-27 08:35:30

## 本篇大綱

- [本篇要解決的問題](https://www.letswrite.tw/form-validation/#%e6%9c%ac%e7%af%87%e8%a6%81%e8%a7%a3%e6%b1%ba%e7%9a%84%e5%95%8f%e9%a1%8c)
- [中文姓名](https://www.letswrite.tw/form-validation/#%e4%b8%ad%e6%96%87%e5%a7%93%e5%90%8d)
- [身分證字號、手機條碼](https://www.letswrite.tw/form-validation/#%e8%ba%ab%e5%88%86%e8%ad%89%e5%ad%97%e8%99%9f%e3%80%81%e6%89%8b%e6%a9%9f%e6%a2%9d%e7%a2%bc)
 - [身分證首字、手機條碼自動大寫](https://www.letswrite.tw/form-validation/#%e8%ba%ab%e5%88%86%e8%ad%89%e9%a6%96%e5%ad%97%e3%80%81%e6%89%8b%e6%a9%9f%e6%a2%9d%e7%a2%bc%e8%87%aa%e5%8b%95%e5%a4%a7%e5%af%ab)
- [生日 / 出生年月日](https://www.letswrite.tw/form-validation/#%e7%94%9f%e6%97%a5-%e5%87%ba%e7%94%9f%e5%b9%b4%e6%9c%88%e6%97%a5)
- [E-mail 電子信箱](https://www.letswrite.tw/form-validation/#e-mail-%e9%9b%bb%e5%ad%90%e4%bf%a1%e7%ae%b1)
- [手機](https://www.letswrite.tw/form-validation/#%e6%89%8b%e6%a9%9f)
 - [手機限輸入 10 碼](https://www.letswrite.tw/form-validation/#%e6%89%8b%e6%a9%9f%e9%99%90%e8%bc%b8%e5%85%a5-10-%e7%a2%bc)
- [選擇縣市](https://www.letswrite.tw/form-validation/#%e9%81%b8%e6%93%87%e7%b8%a3%e5%b8%82)
- [範例及原始碼](https://www.letswrite.tw/form-validation/#%e7%af%84%e4%be%8b%e5%8f%8a%e5%8e%9f%e5%a7%8b%e7%a2%bc)

---

## 本篇要解決的問題

網站上蠻常會有表單功能的，像是聯絡我們、購物車，就會需要前端工程師先將欄位驗證好，當資料、格式都正確後，再調用 API 把表單送到後端去做處理，這樣子的流程會是比較好的 UX。最近的案子剛好要驗證很多的表單欄位，就順便整理成這篇，之後要用就可以 ~~複製貼上~~ 回頭查找。

除了表單驗證，本篇的 Demo 也會使用一些套件，讓出來的資料會是正確的格式，就不會寫驗證了，因為格式已經是對的。

本篇除了自己手寫的原生 JS，也會使用套件，都會附上套件的連結。

本篇最後完成的 Demo：[https://letswritetw.github.io/letswrite-form-validation/](https://letswritetw.github.io/letswrite-form-validation/)

Demo 部份為了讓頁面好看，有用 [daisyUI](https://daisyui.com/) 這套 Tailwind CSS 的 framework。

---

## 中文姓名

這算是比較特別的需求，要求說姓名欄位的部份只能填寫中文名字，不能填寫英文。

August 在寫的時候也增加了限制：不能填寫數字、不能填寫特殊符號。

但…特殊符號有點麻煩，因為中文還有些人會打全形，因此過濾的部份就是濾掉幾個常用的符號。

```html
<!-- HTML -->
<input id="js_name" type="text">

<!-- JS -->
<script>
  function checkChName(el) {
    el.addEventListener('input', e => {
      setTimeout(() => {
        return e.target.value = e.target.value.replace(/[a-zA-Z0-9]|[\w\s]|[!#$€£%&'"`()*+\-.\/:：;；…,，。「」【】=<>?@{}\\\^|\[\]]/g, '')
      }, 0)
    }, false);
  }
  const jsName = document.getElementById('js_name');
  checkChName(jsName);
</script>
```

簡單來說，就是 `id` 為 `js_name` 的 `input`，在值有改變時，會把輸入的值濾掉英文、數字、指定的符號後，再回傳成值。

因為是寫原生的，所以寫的落落長，如果能用 Vue.js，直接寫進 `watch` 裡可以更簡短。

---

## 身分證字號、手機條碼

這部份就直接用套件了，上網查了一下身份證字號的規則，覺得自己寫好麻煩，既然有佛心工程師寫了套件，就直接使用囉~

使用的套件是：[taiwan-id-validator](https://www.npmjs.com/package/taiwan-id-validator)

它總共可以驗證以下資料：

- 台灣身分證字號驗證
- 舊版臺灣地區無戶籍國民、外國人、大陸地區人民及香港或澳門居民之專屬代號
- 新版臺灣地區無戶籍國民、外國人、大陸地區人民及香港或澳門居民之專屬代號
- 公司統一編號驗證 (2021/10/28 新增新版統一編號檢查)
- 自然人憑證編號驗證
- 電子發票手機條碼驗證
- 電子發票捐贈碼驗證

是不是超多，本篇僅示範身分證字號、電子發票手機條碼這二個。

**HTML**

```html
<!-- 身份證字號 -->
<input id="js_id" class="uppercase" type="text">

<!-- 手機條碼 -->
<input id="js_phoneId" class="uppercase" type="text">
```

**JS**

```jsx
import {
  isNationalIdentificationNumberValid, // 身分證字號
  isEInvoiceCellPhoneBarcodeValid, // 手機條碼
} from 'taiwan-id-validator';

// 驗證身分證字號
const jsId = document.getElementById('js_id');
let value = jsId.value;
if(isNationalIdentificationNumberValid(value)) {
  // 格式正確時
} else {
  // 格式錯誤時
}

// 驗證手機條碼
const jsPhoneId = document.getElementById('js_phoneId');
let value = jsPhoneId.value;
if(isEInvoiceCellPhoneBarcodeValid(value)) {
	// 格式正確時
} else {
  // 格式錯誤時
}
```

### 身分證首字、手機條碼自動大寫

五歲能抬頭的朋友一定有發現到，上面 August 寫的 HTML 程式碼，在 `input` 上多加了 `uppercase` 這個 class。

身分證字號的第一個英文字母、手機條碼的英文字母，都是大寫，如果要使用者多按一個按鍵去切換大小寫，是不太好的 UX，因為每多一次點擊，就會多流失一點使用者的填單率。

所以更好的 UX 是不論使用者輸入大寫或小寫，我們都直接透過程式讓它自動轉為大寫。

方式很簡單。

1. 在頁面 HTML 的部份，我們用 `.uppercase` 讓使用者看到的結果直接都是大寫
2. 驗證資料時，用 JS 把使用者輸入的值轉成大寫後再去驗證
3. 送資料時，也是透過 JS 把使用者輸入的值轉成大寫後再送出

所以上面的範例程式碼就可以改成：

**HTML**

```html
<!-- 身份證字號 -->
<input id="js_id" class="uppercase" type="text">

<!-- 手機條碼 -->
<input id="js_phoneId" class="uppercase" type="text">
```

**CSS**

```css
.uppercase {
	text-transform: uppercase;
}
```

**JS**

```jsx
import {
  isNationalIdentificationNumberValid, // 身分證字號
  isEInvoiceCellPhoneBarcodeValid, // 手機條碼
} from 'taiwan-id-validator';

// 驗證身分證字號
const jsId = document.getElementById('js_id');
let value = jsId.value.toLocaleUpperCase();
if(isNationalIdentificationNumberValid(value)) {
  // 格式正確時
} else {
  // 格式錯誤時
}

// 驗證手機條碼
const jsPhoneId = document.getElementById('js_phoneId');
let value = jsPhoneId.value.toLocaleUpperCase();
if(isEInvoiceCellPhoneBarcodeValid(value)) {
	// 格式正確時
} else {
  // 格式錯誤時
}
```

---

## 生日 / 出生年月日

選日期的部份，August 也是用套件，用過的有二套。

如果不在意頁面多引用 jQuery，推薦這套：[fDatepicker](https://github.com/liedekef/fdatepicker)。

他可以讓使用者先從「年」開始選，選完後再選「月」，之後再選「日」，操作上比較直覺。

如果不想用 jQuery，想用原生，那可以用本篇 Demo 使用的這套：[Vanilla JS Datepicker](https://github.com/mymth/vanillajs-datepicker)。

August 覺得這套操作體驗上沒有 fDatepicker 來的好，但它的優點就是原生。

如果有用過更好的日期套件要推薦的話，歡迎留言~

關於出生年月日的欄位，為了要統一輸出的格式，因此是用套件來產生選單讓使用者選取。

但這樣還不夠，還需要一道工，就是生日的 `input` 不能讓使用者自行填寫。

**HTML**

```html
<input id="js_birthday" type="text" readonly inputmode="none">
```

`readonly`，就是讓這個欄位只能讀不能寫。

`inputmode="none"`，就是在手機時不讓小鍵盤跳出來。

至於為什麼這邊 August 是推薦 `readonly` 而不是 `disabled`？

在工程師永遠的好朋友 [stackoverflow](https://stackoverflow.com/questions/7730695/whats-the-difference-between-disabled-disabled-and-readonly-readonly-for-ht) 上有提到，我們按 **tab** 鍵切換選定的元素時，`readonly` 可以被 focus，而 `disabled` 則無法，為了讓桌機時操作順暢，這邊選用的是 `readonly`。

**JS**

```jsx
import { Datepicker } from 'vanillajs-datepicker'

// 引用中文
import zhTW from 'vanillajs-datepicker/locales/zh-TW'
Object.assign(Datepicker.locales, zhTW);

const birthday = document.getElementById('js_birthday');
const datepicker = new Datepicker(birthday, {
  format: 'yyyy-mm-dd',
  language: 'zh-TW'
});
```

`format` 的部份可以改成後端需要的格式。

---

## E-mail 電子信箱

驗證 email 比較單純，就是驗證輸入的格式正不正確。

**HTML**

```html
<input id="js_email" type="email" inputmode="email">
```

`inputmode="email"`，是讓手機時跳出的小鍵盤，是專門輸入 email 用的，iPhone 上看會像這樣：


![手機鍵盤輸入 email 形式](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b8auubaqqmlk8enmmjl3.jpg)

**JS**

```jsx
const jsEmail = document.getElementById('js_email');
const rex = /^\w+((-\w+)|(\.\w+))*\@[A-Za-z0-9]+((\.|-)[A-Za-z0-9]+)*\.[A-Za-z]+$/;
if(jsEmail.value.search(rex) === -1) {
  // 錯誤
}
else {
  // 正確
}
```

---

## 手機

驗證手機也很單純，就是看格式正不正確。

**HTML**

```html
<input id="js_phone" type="tel" inputmode="tel">
```

`inputmode="tel"`，是讓手機時跳出的小鍵盤，是專門輸入電話號碼用的，iPhone 上看會像這樣：

![手機鍵盤輸入電話號碼形式](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b6srmpku4e1kssjofjx1.jpg)

**JS**

```jsx
const jsPhone = document.getElementById('js_phone');

const rex = /^(09)[0-9]{8}$/;
if(jsPhone.value.search(rex) === -1) {
  // 錯誤
}
else {
  j// 正確
}
```

### 手機限輸入 10 碼

手機號碼總共 10 碼，我們自己在網購時都知道，有時就是會手抖或手殘不小心多按一碼造成錯誤。

因此 August 在製作表單時，也會把手機這欄加入只能填寫 10 碼的 JS：

```jsx
const jsPhone = document.getElementById('js_phone');
jsPhone.addEventListener('input', e => {
  if(e.target.value.length > 10) {
    e.target.value = e.target.value.slice(0, 10);
  }
});
```

---

## 選擇縣市

通常，如果讓使用者填寫地址時，縣市的部份是自己打字，就會看到什麼叫世界末日，光一個台北市，就會看到「台北、台北市、臺北、臺北市、北市」。

如果還要使用者自己去找郵遞區號，就會更慘，不是憑記憶填不然就是乾脆放棄不填了。

幸好，工程師界裡總有希望世界和平的大神，選擇縣市的部份推薦套件：[jQuery-TWzipcode](https://github.com/essoduke/jquery-twzipcode)****。****

雖然名稱上有可以再戰十年的「jQuery」，但不用擔心，它有出原生 JS 版的。

進到套件的 GitHub，會看見 JS 檔案有三個：


![jQuery-TWzipcode 的 GitHub](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h5lss08ma8tky8i2h2k6.png)

- [jquery.twzipcode.js](https://github.com/essoduke/jQuery-TWzipcode/blob/master/jquery.twzipcode.js)：jQuery + 未壓縮版
- [jquery.twzipcode.min.js](https://github.com/essoduke/jQuery-TWzipcode/blob/master/jquery.twzipcode.min.js)：jQuery + 壓縮版
- [twzipcode.js](https://github.com/essoduke/jQuery-TWzipcode/blob/master/twzipcode.js)：原生 JS + 未壓縮版

文件的部份，jQuery 版本的就看 GitHub 上 [README.md](https://github.com/essoduke/jQuery-TWzipcode/blob/master/README.md) 的說明。原生的就看上面提供的 [Demo](https://code.essoduke.org/twzipcode/)。

因為原生的部份不是寫成 export，所以引用時就在頁面上直接引用。

**HTML**

```html
<div class="twzipcode">
	
	<!-- 選擇縣市 -->
	<div data-role="county">
	
	<!-- 選擇鄉鎮市區 -->
	<div data-role="district">
	
	<!-- 郵遞區號 -->
	<div data-role="zipcode">

</div>

<script src="/path/twzipcode.js"></script>
```

**JS**

```jsx
const twzipcode = new TWzipcode(".twzipcode", {
  zipcode: {
    readonly: true
  }
});
```

郵遞區號因為套件會自動產生，因此建議是加上 `readonly: true` 讓使用者無法自行輸入。

---

## 範例及原始碼

本篇的範例及原始碼有放在 GitHub 上，取用前麻煩分享本篇，或是按個星星，你的小小動作對本站都是大大的鼓勵。

Demo：[https://letswritetw.github.io/letswrite-form-validation/](https://letswritetw.github.io/letswrite-form-validation/)

原始碼：[https://github.com/letswritetw/letswrite-form-validation](https://github.com/letswritetw/letswrite-form-validation)