# 小專案一: Form Validator | 登入頁面的製作
###### tags: `JS小專案`

## 學習重點_CSS
[參考資料_JavaScript初學:DOM常用屬性與方法](https://medium.com/change-or-die/javascript%E5%88%9D%E5%AD%B8-dom%E5%B8%B8%E7%94%A8%E5%B1%AC%E6%80%A7%E8%88%87%E6%96%B9%E6%B3%95-ef851afdb65a)
### 1. 如何連接CSS和Javascript語法
一般來說都會寫在head裡面,但也有撰寫在body內的
```htmlembedded=
<link rel="stylesheet" href="./style.css">
<script src="script.js" defer></script>
```
以下是寫在head裡面的特性:
1. 可以把jscode跟其他文字分開
2. 如果都是使用外部套件與資源,寫上head會優先讀取這些資源
3. 同樣的script如果都被不同的pages所使用,寫在header看起來會更簡潔

### 2. 撰寫與規劃表單
以下是常用的表單的基本結構?
container->form->form-control->label->input->small
```htmlembedded=
<div class="container">
<!-- 先設計一個container 把表單的內容都包起來 -->
<form id="form" class="form">
<!-- 使用一個表單,並賦予類別,讓欄位和文字都被包在其中 -->
<h2>Register With Us</h2>
<!-- 主題文字 -->
<div class="form-control">
<!-- 表單控制項 -->
<label for="username">Username</label>
<!-- 使用label來定義欄位標題 -->
<input type="text" id="username" placeholder="Enter username">
<!-- 使用input來定義輸入文字的欄位 -->
<small>Error message</small>
<!-- 萬一出現例外狀況的警告文字 -->
</div>
<button>Submit</button>
<!-- 提交按鈕 -->
</form>
</div>
```
### 3. 根元素的運用
根元素在CSS裡面就是宣告`全域變數`的動作
只要制定了根元素,我們就可以在任何地方都使用這個變數
```css=
:root {
--success-color: #2ecc71;
--error-color: #e74c3c;
}
```
例如:
```css=
.form-control.success input {
border-color: var(--success-color);
/* 如果填寫正確,則邊框效果為綠色 */
}
.form-control.error input {
border-color: var(--error-color);
/* 如果填寫錯誤,邊框效果為紅色 */
}
```
## 學習重點_Javacript
### 1. 如何把表單欄位與JavaScript連動
這個範例中用到大量的`document.getElementById`,用途在於會回傳所指定id的第一個物件
**原文: Document.getElementById(elementId: string):**
HTMLElement, Returns a reference to the first object with the specified value of the ID attribute.
```javascript=
const form = document.getElementById('form');
const username = document.getElementById('username');
const email = document.getElementById('email');
const password = document.getElementById('password');
const password2 = document.getElementById('password2');
```
### 2. 如何讓出現例外狀況時,表單欄位下面有錯誤訊息(紅字出現)
```javascript=
function showError(input, message){
// 設定一個showError的functin,內有兩個引數
const formControl = input.parentElement;
// parentElement可以回傳HTML的元素
// 把formControl設定為input的父節點
formControl.className = 'form-control error';
// 將'form-control error'賦給formControl所回傳的Class元素
// 把form-control error設定為formControl的class
const small = formControl.querySelector('small');
// querySelector可以抓到特定id的HTNL Node節點
// 把HTML 名為small節點的內容賦值給變數small
small.innerText = message;
// Node.innerText 是一個代表節點及其後代之「已渲染」(rendered)文字內容的屬性。
// 獲取除了HTML Tag外所包含的內容,此時的innerText取決於function後面輸入的引數
}
```
### 3. 如何用if else來讓特定的例外狀況觸發特定文字
這邊我們先用檢閱email是否正確,來小小的偷個懶
[參考資料](https://stackoverflow.com/questions/46155/how-to-validate-an-email-address-in-javascript)
首先,把stackflow抓下來的驗證email文字貼在function內,並且賦值給re
只要輸入的文字經過驗證後,結果為true,則可導入輸入成功的狀態,反之則會顯示email is not valid
```javascript=
function checkEmail(input) {
const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
// 把Stackflow內的資源抓下來修改
if(re.test(input.value.trim())) {
showSuccess(input);
// test可以把正規表達式與指定的字符串抓來檢查是否匹配
// 如果把輸入的值拿來與上方驗證,若是為true,代表匹配email的格式
// 則會放入showSuccess的內容中
} else {
showError(input, 'Email is not valid');
// 反之,到showError的部分,並且輸入'Email is not valid'的引數'
}
}
```
### 4. 如果格式輸入正確,如何導入成功的結果呢?
方才說到,如果輸入格式正確,就會導到另一個結果
這時再針對格式正確的狀態做一個function,讓結果可以正確被導引
```javascript=
function showSuccess(input){
const formControl = input.parentElement;
// 把form-control success賦值給formControl的class
formControl.className = 'form-control success';
}
```
### 5. 如果沒填東西怎辦?
一樣,我們需要特別設計一個function來去驗證填寫文字的狀態
不過這次我們需要運用迴圈來處理
```javascript=
function checkRequired(inputArr) {
// 運用迴圈,讓每個input都被驗證
inputArr.forEach(function(input){
if(input.value.trim() === '') {
// 如果input進去的值經過移除空白仍是處於空值
showError(input, `${getFieldName(input)} is required`);
// 則會出現xxx is required的字樣
} else {
showSuccess(input);
}
});
}
```
### 6. 密碼不一致...
如果使用者前後輸入的密碼不一致,我們也可以使用不等於(`!==`)來處理
```javascript=
function checkPasswordsMatch(input1, input2) {
if(input1.value !== input2.value) {
showError(input2, 'Password do not match');
}
}
```
## 1. Project HTML
### 步驟1 設定:
先建立一個index.html,輸入!跑出HTML後,把title改成Validate

### 步驟2 新增container:
建立一個container,並且加入一個form的區塊以及title
```htmlembedded=
<div class="container">
<form id="form" class="form">
<h2>Register With Us</h2>
</form>
</div>
</body>
```

### 步驟3 設定表單欄位:
使用Div把各個欄位給設定好,包含username、email、password、confirm password等四個部分
(小訣竅:按壓`shift+滑鼠`可以選擇同樣字元的單字,再用`ctrl+D+方向鍵`來選擇想要改變的單字)
```htmlembedded=
<div class="form-control">
<label for="username">Username</label>
<input type="text" id="username" placeholder="Enter username">
<small>Error message</small>
</div>
<div class="form-control">
<label for="email">Email</label>
<input type="text" id="email" placeholder="Enter email">
<small>Error message</small>
</div>
<div class="form-control">
<label for="password">Password</label>
<input type="password" id="password" placeholder="Enter password">
<small>Error message</small>
</div>
<div class="form-control">
<label for="password2">Confirm Password</label>
<input type="password" id="password2" placeholder="Enter password again">
<small>Error message</small>
</div>
```
### 步驟4 新增表單按鈕
新增提交按鈕
```htmlembedded=
<button>Submit</button>
```

此時的成品應該會長這樣子

## 2. Project CSS
### 步驟5 前往 **Google Fonts** 鑲嵌字型在網頁上
[Google Fonts 網址](https://fonts.google.com/)

創建一個style.css,直接把複製的字體貼上去
```htmlembedded=
@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');
```

### 步驟6 設定CSS Link
在原有的HTML中的head區塊,放入CSS的link
```htmlembedded=
<link rel="stylesheet" href="./style.css">
```

### 步驟7 CSS內容的初始設定
使用下列的語法,這個元素的內距和邊框將不會增加元素本身的寬度。就不用自己去運算出元素的寬度。
```css=
* {
box-sizing:border-box;
}
```
### 步驟8 設定Body內的CSS的內容
[參考資料:align-items](https://w3c.hexschool.com/flexbox/87d66dc4)
[MDN Align-items](https://developer.mozilla.org/zh-CN/docs/Web/CSS/align-items)
[MDN Justify-contents](https://developer.mozilla.org/zh-CN/docs/Web/CSS/justify-content)
[min-height說明](https://www.wibibi.com/info.php?tid=CSS_min-height_%E5%B1%AC%E6%80%A7)
[CSS margin](https://www.wibibi.com/info.php?tid=110)
```css=
body {
background-color: #f9fafb;
/* 設定背景顏色 */
font-family: 'Open Sans',sans-serif;
/* 設定字體 */
display:flex;
/* 使用flexb讓資料自動排列 */
align-items: center;
/* 對Y軸中央 */
justify-content: center;
/* 在X軸中居中排列 */
min-height: 100vh;
/* 設定網頁元素的最低高度限制 */
margin: 0;
/* 外邊界距離,俗稱外距 */
}
```

### 步驟9 設定container內的CSS
```Example
.container{
/* 用container設定表單整體範圍 */
background-color: #fff;
/* 設定背景顏色 */
border-radius: 5px;
/* 設定邊界圓角 */
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
/* 設定周邊陰影 */
width: 400px;
/* 設定寬度 */
}
```

### 步驟10 用form設定表單文字的排列
```css=
.form{
padding: 30px 40px;
/* 調整上下內距 */
}
```
### 步驟11 調整表單的標題文字
```css=
h2 {
text-align: center;
/* 讓文字置中 */
margin: 0 0 20px;
/* 上0px 左、右0px 下20px */
}
```
### 步驟12 使用表單控制向來調整表單
[參考資料: 甚麼是form-control?](https://matthung0807.blogspot.com/2019/08/html-form-control.html)
``` css=
.form-control{
margin-bottom: 10px;
/* 外距調整為10px */
padding-bottom: 20px;
/* 內距調整為20px */
position: relative;
/* 會使其元素「相對地」調整其原本該出現的所在位置 */
}
```

### 步驟13 使用form-control來調整label
```css=
.form-control label{
color: #777;
/* Label的文字呈現為灰色 */
display: block;
/* 要求元素以區塊方式呈現 */
margin-bottom: 5px;
/* 底部外距為5px */
}
```

### 步驟14 使用form-control來調整input
```css=
.form-control input {
border: 2px solid #f0f0f0;
/* 調整inputbox的邊界為粗框設計 */
border-radius: 4px;
/* 讓邊界產生部分圓角 */
display: block;
/* 使用區塊型的呈現方式 */
width: 100%;
/* 自然地在規定的範圍內延展呈現 */
padding: 10px;
/* 內距調整為10px(文字填寫內容與輸入空格的間距) */
font-size: 14px;
/* 文字大小為14px */
}
```

### 步驟15 使用form-control input:focus 來設計邊框效果
```css=
.form-control input:focus {
outline: 0;
/* 去掉原有邊框設計 */
border-color: #777;
/* 選取邊框效果改為灰色 */
}
```

### 步驟16 設計填寫正確與錯誤的邊框效果
```css=
.form-control.success input {
border-color: #2ecc71;
/* 如果填寫正確,則邊框效果為綠色 */
}
.form-control.error input {
border-color: #e74c3c;
/* 如果填寫錯誤,邊框效果為紅色 */
}
```
示範一下效果,後續可以透過Javascript來呈現
```htmlembedded=
<div class="form-control success">
<label for="username">Username</label>
<input type="text" id="username" placeholder="Enter username">
<small>Error message</small>
</div>
<div class="form-control error">
<label for="email">Email</label>
<input type="text" id="email" placeholder="Enter email">
<small>Error message</small>
</div>
```

設定錯誤訊息的字體顏色
```javascript=
.form-control small{
color: #e74c3c;
}
```
後續可以建立一個根目錄選取器,來建立變數,並且後續可以套用
[參考資料 :root 根目錄選取器 - 叫你阿爸出來講](https://ithelp.ithome.com.tw/articles/10228111)
```javascript=
:root {
--success-color: #2ecc71;
--error-color: #e74c3c;
}
```
套用結果如下:
```javascript=
.form-control.success input {
border-color: var(--success-color);
/* 如果填寫正確,則邊框效果為綠色 */
}
.form-control.error input {
border-color: var(--error-color);
/* 如果填寫錯誤,邊框效果為紅色 */
}
.form-control small{
color:var(--error-color);
/* 填寫錯誤出現的訊息顏色 */
}
```

### 步驟17 進一步設定錯誤訊息
```javascript=
.form-control small{
color:var(--error-color);
/* 填寫錯誤出現的訊息顏色 */
position: absolute;
/* 修改位置呈現為絕對位置 */
bottom: 0;
/* 向下貼齊,與空格保持部分距離 */
left: 0;
/* 置左呈現 */
visibility: hidden;
/* 預設為隱藏 */
}
```

預設一個機制可以讓error message顯現出來
```javascript=
.form-control.error small {
visibility: visible;
}
```
### 步驟18 設定按鈕
```javascript=
.form button {
cursor: pointer;
/* 會出現手指的符號 */
background-color: #3498db;
/* 背景顏色設定 */
border: 2px solid #3498db;
/* 邊界顏色與背景色相同 */
border-radius: 4px;
/* 部分圓角 */
color: #fff;
/* 字體顏色為白色 */
display: block;
/* 顯示方式為區塊型呈現 */
font-size: 16px;
/* 字體大小為16px */
padding:10px;
/* 內距為10px */
margin-top: 20px;
/* 與上方空格距離為20px */
width: 100%;
/* 按鈕左右延展 */
}
```

## 3. Adding Simple Validation
### 步驟19 針對表單內各個代表InputBox的元素物件進行賦值
一開始先創建一個js的檔案,命名為script
```javascript=
// 首先 指定表單的項目為變數來賦值
// returns an Element object representing the element whose id property matches the specified string.
// 回傳特定id所代表的元素物件
const form = document.getElementById('form');
//回傳div內id為form的相關資訊
const username = document.getElementById('username');
//回傳id為username的相關資訊
const email = document.getElementById('email');
//回傳id為email的相關資訊
const password = document.getElementById('password');
//回傳id為password的相關資訊
const password2 = document.getElementById('password2');
//回傳id為password2的相關資訊
```
**注意**
JS Code的擺放位置
1. 放在Body的尾端
```javascript=
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Validate</title>
<link rel="stylesheet" href="./style.css">
// 記得要加 defer
<script src="script.js" defer></script>
</head>
```
2. 放在Head裡面
```javascript=
<body>
<script src="script.js"></script>
</body>
```
### 步驟20 設定EventListener(submit鍵按下去可能出現的結果)
```javascript=
// Event listeners
form.addEventListener('submit',function(e) {
e.preventDefault();
// 讓所key in的文字可以持續顯示在console內
if (username.value === ''){
showError(username, 'Username is required');
} else {
showSuccess(username);
}
// 設定按下submit後,可能產生的結果
});
```
#### 甚麼是addEventListener
透過function來向目標物件提供event
```javascript=
element.addEventListener(event, function, useCapture)
```
[參考資料](https://www.runoob.com/jsref/met-element-addeventlistener.html)
#### 甚麼是preventDefault
作用是停止事件的默認動作,我們會希望透過程式的判斷,希望他暫停執行預設動作,在程式中停止,就可以加入event.preventDefault()
### 步驟21 設定錯誤訊息呈現方式
```javascript=
// Show input error message
function showError(input, message){
const formControl = input.parentElement;
formControl.className = 'form-control error';
const small = formControl.querySelector('small');
//querySelector可以抓到特定id
small.innerText = message;
}
```
不讓錯誤訊息只是error message,透過querySelector的方式可以抓到特定的文字

### 步驟22 設定填寫正確的欄位框色
```javascript=
// Show success outline
function showSuccess(input){
const formControl = input.parentElement;
formControl.className = 'form-control success';
}
```

### 步驟23 把每個欄位的錯誤訊息補充上去
```javascript=
// Event listeners
form.addEventListener('submit', function(e) {
e.preventDefault();
//如果事件可以取消就取消,但不會影響事件的傳遞
// 作用: 讓所key in的文字可以持續顯示在console內
if (username.value === ''){
showError(username, 'Username is required');
} else {
showSuccess(username);
}
if (email.value === ''){
showError(email, 'Email is required');
} else {
showSuccess(email);
}
if (password.value === ''){
showError(password, 'Password is required');
} else {
showSuccess(password);
}
if (password2.value === ''){
showError(password2, 'Password 2 is required');
} else {
showSuccess(password2);
}
```
### 步驟24 如何驗證欄位信箱是否正確(validate email)
[資料來源 stack Overflow](https://stackoverflow.com/questions/46155/how-to-validate-an-email-address-in-javascript)
上stack overflow找尋相關的資源並且將其放入function,當填入email時就會產生驗證作用
```javascript=
//Check email is valid
function isValidEmail(email) {
const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(String(email).toLowerCase());
}
```
並且修改產生錯誤訊息的條件
```javascript=
if (email.value === ''){
showError(email, 'Email is required');
} else if(!isValidEmail(email.value)) {
showError(email, 'Email is not valid');
} else {
showSuccess(email);
}
```

### 步驟25 用checkRequired替代逐欄設定錯誤訊息
```javascript=
// Check required fields
function checkRequired(inputArr) {
inputArr.forEach(function(input){
if(input.value.trim() === '') {
showError(input, 'is required');
} else {
showSuccess(input);
}
});
}
// Event listener裡面有關欄位錯誤描述的內容全部註解,用矩陣來代替
// Event listeners
form.addEventListener('submit', function(e) {
e.preventDefault();
checkRequired([username, email, password, password2]);
});
```

### 步驟26 讓欄位的id與錯誤訊息結合
使用${}與反引號的方式,讓物件與文字訊息結合
```javascript=
// Check required fields
function checkRequired(inputArr) {
inputArr.forEach(function(input){
if(input.value.trim() === '') {
showError(input, `${input.id} is required`);
} else {
showSuccess(input);
}
});
}
```

### 步驟27 將錯誤訊息的第一個字大寫
首先,把input.id的部分改為獲取getFieldName function的值
```javascript=
// Check required fields
function checkRequired(inputArr) {
inputArr.forEach(function(input){
if(input.value.trim() === '') {
showError(input, `${getFieldName(input)} is required`);
} else {
showSuccess(input);
}
});
}
```
新增function,來獲取每個欄位的id並且使用char()和slice()來改寫回傳的數值
```javascript=
// Get fieldname
function getFieldName(input){
return input.id.charAt(0).toUpperCase() + input.id.slice(1);
}
```

### 步驟28 新增密碼長度驗證
在原有的eventlistner內新增checkLength的功能
```
// Event listeners
form.addEventListener('submit', function(e) {
e.preventDefault();
//如果事件可以取消就取消,但不會影響事件的傳遞
checkRequired([username, email, password, password2]);
checkLength(username, 3, 15);
checkLength(password, 6, 25);
});
```
定義checkLength的功能
```javascript=
// Check input length
function checkLength(input, min, max) {
if(input.value.length < min) {
showError(input,`${getFieldName(input)} must be at least ${min} characters` );
} else if(input.value.length > max) {
showError(input, `${getFieldName(input)}must be less than ${max} characters`);
} else {
showSuccess(input);
}
}
```

### 步驟28 豐富email驗證功能
修改email的錯誤訊息與驗證內容
```javascript=
function checkEmail(input) {
const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
if(re.test(input.value.trim())) {
showSuccess(input);
} else {
showError(input, 'Email is not valid');
}
}
```
新增驗證項目
```javascript=
checkRequired([username, email, password, password2]);
checkLength(username, 3, 15);
checkLength(password, 6, 25);
checkEmail(email);
});
```
### 步驟29 新增驗證兩組密碼是否相符的功能
新增驗證的function
```javascript=
// Check password match
function checkPasswordsMatch(input1, input2) {
if(input1.value !== input2.value) {
showError(input2, 'Password do not match');
}
}
```
把欲驗證的欄位放入陣列內
```javascript=
// Event listeners
form.addEventListener('submit', function(e) {
e.preventDefault();
//如果事件可以取消就取消,但不會影響事件的傳遞
checkRequired([username, email, password, password2]);
checkLength(username, 3, 15);
checkLength(password, 6, 25);
checkEmail(email);
checkPasswordsMatch(password, password2);
});
```
