###### tags: `JavaScript` `API` `DOM`
# [week 7] DOM 介面、瀏覽器事件處理
> 本篇為 [[FE102] 前端必備:JavaScript](https://lidemy.com/p/fe102-javascript) 這門課程的學習筆記。如有錯誤歡迎指正。
```
學習目標:
P1 你知道 JavaScript 跑在網頁上跟跑在 Node.js 上差在哪裡
P1 你知道 DOM 是什麼
P1 你知道如何用 JavaScript 操控 DOM 物件
P1 你知道如何幫一個按鈕加上 event listener
```
---
當我們運用 JavaScript 在網頁進行操作時,主要可分為下列三大面向:
1. 介面(Interface):如何改變介面
2. 事件(Event):如何監聽事件並做出反應
3. 資料(Data):如何和伺服器交換資料
> 以下主要探討「改變介面」與「事件監聽」的部分。
## JavaScript 與瀏覽器的溝通
那麼,該把 `<script>` 標籤放在哪呢?首先要瞭解,我們可以放在 HTML 檔案的任何位置,通常會有兩種回答:
1. 放在 `<head>` ... `</head>` 之間
2. 放在 `</body>` 之前
由於瀏覽器是由上而下渲染,相較之下以第二種方法較佳。但更好的做法,是另外新增 js 檔,以外連檔的形式執行 JS,更有助於管理與維護。
```htmlmixed=
<head>
<title>This is title.</title>
<script src="./main.js"></script>
</head>
```
### 執行環境
我們可以在 Node.js 或瀏覽器執行 JS。由於兩者是不同的執行環境,在語法支援度也有些微差異。
> 例如:
> 1. require() 引入模組的語法,只能在 Node.js上執行
> 2. alert() 叫出提示窗,只能在瀏覽器上執行
### DOM 是什麼?
DOM,全名是 Document Object Model(文件物件模型)。
是由「瀏覽器」提供用來和「程式語言」溝通的橋樑。最常被用在網頁與 JavaScript 的溝通。
DOM 類似於把 HTML 的文件(Document)轉成物件(Object)。JavaScript 即可透過 DOM 提供的 API 來存取並操作 HTML。

### 如何選取 DOM 元素
document 是瀏覽器提供的特殊物件,內部放著許多 function。而我們可以透過「物件」的方式去呼叫想要的元素:
- 根據 Tag 名稱選取:`document.getElementsByTagName()`
- 根據 Class 名稱選取:`document.getElementsByClassName()`
- 根據 ID 名稱選取:`document.getElementById()`
- 選取 CSS 選擇器:`document.querySelector()`
以 `document.getElementsByTagName('div')`,選取所有 tag 名稱是 div 元素為範例:
```htmlmixed=
<body>
<div>
This is tagName.
<div class="box">This is className.</div>
<div id="name">This is idName.</div>
</div>
<script>
const elements = document.getElementsByTagName('div')
console.log(elements)
</script>
</body>
```
在瀏覽器運行結果,會得到類陣列如下:

若改選取 `elements[1]`,即可選取類陣列中的元素:

若根據 Class 和 ID 名稱選取,也是類似用法:
- `document.getElementsByClassName('box')`:括弧內不需加 `.` 即可表示 class

- `document.getElementById('name')`:注意 ID 只會有一個

相較於前面三種選取方式,又以 querySelector 更方便且直覺。就像寫 CSS 選擇器來選取元素:
- 選取標籤:`const element = document.querySelector('div')`
- 選取 class:`const element = document.querySelector('.box')`
- 選取 id:`const element = document.querySelector('#name')`
但 `querySelector()` 只會選取第一個匹配的元素,舉例如下:
```htmlmixed=
<body>
<a href="#">1st</a>
<div>
<a href="#">2rd</a>
</div>
<script>
const element = document.querySelector('a')
console.log(element)
</script>
</body>
```
結果只會選取第一個 `<a>`:

若想選取到所有匹配到的元素,可使用 `querySelectorAll()`:
```htmlmixed=
const element = document.querySelectorAll('a')
```
同樣能夠得到類陣列:

### 改變元素的 CSS:`.style`
- 直接寫上行內樣式
```htmlmixed=
const element = document.querySelector('.box')
element.style.background = 'red';
element.style.paddingTop = '10px'; // 駝峰式
element.style["padding-top"] = '10px'; // 字串
```
- 但這種寫法其實不易修改,因此並不推薦。通常會寫好另外的 class,藉由「改變元素的 class」來套用該狀態,也就是下面要介紹的方法。
### 改變元素的 Class:`.classList`
- `.classList.add()`:增加
- `.classList.remove()`:移除
- `.classList.toggle()`:開關(有就刪、沒有就加)
- `.classList.contains()`:判斷是否包含該 class
```htmlmixed=
<style>
.active {
background: red;
}
</style>
element.classList.add('active')
<!-- 將元素套用 active 這個 class 的樣式 -->
```
### 改變元素的內容
- `.innerText`:只抓取標籤內的文字。最常用來改變文字內容。
```htmlmixed=
<div>
<div class="box">This is <span>class</span>.</div>
<div id="name">This is id.</div>
</div>
<script>
const element = document.querySelector('.box')
console.log(element.innerText)
</script>
<!-- 印出:This is class. -->
```
- `.innerHTML`:抓取標籤內的所有內容,包含 HTML 標籤。
```htmlmixed=
<div>
<div class="box">This is <span>class</span>.</div>
<div id="name">This is id.</div>
</div>
<script>
const element = document.querySelector('.box')
console.log(element.innerHTML)
</script>
<!-- 印出:This is <span>class</span>. -->
```
- `.outerHTML`:抓取整個元素,可用來重新設置元素本身。較少使用。
```htmlmixed=
<div class="block">
<div class="box">This is <span>class</span>.</div>
<div id="name">This is id.</div>
</div>
<script>
const element = document.querySelector('.box')
console.log(element.outerHTML)
</script>
<!-- 印出:<div class="box">This is <span>class</span>.</div> -->
```
### 刪除與插入元素:removechild 與 appendChild
- `.removechild()`:移除子元素
```htmlmixed=
<div class="block">
<div class="box">This is <span>class</span>.</div>
<a>This is a.</a>
</div>
<script>
const element = document.querySelector('.block')
element.removeChild(document.querySelector('a'))
</script>
<!-- 成功刪除在 block 元素底下,標籤為 a 的元素 -->
```
- `.appendChild()`:插入元素在最後方
1. 插入元素之前,要先建立元素或文字:
- `const newElement = document.createElement('h2')`
- `const newTextElement = document.createTextNode('Hello World')`
2. 增加節點:
- `element.appendChild(newElement)`
- `element.appendChild(newTextElement)`
參考資料:
1. [前端基礎JavaScript篇:JavaScript 與瀏覽器的溝通| by Hugh's Program learning](https://medium.com/@hugh_Program_learning_diary_Js/%E5%89%8D%E7%AB%AF%E5%9F%BA%E7%A4%8E-javascript%E7%AF%87-javascript-%E8%88%87%E7%80%8F%E8%A6%BD%E5%99%A8%E7%9A%84%E6%BA%9D%E9%80%9A-e00ff05d0fdc)
2. [JavaScript入門系列:BOM和DOM筆記| 快樂學程式](https://www.happycoding.today/posts/43)
3. [HTML DOM 元素](https://www.runoob.com/jsref/dom-obj-document.html)
---
## JavaScript 網頁事件處理
JavaScript 是以事件驅動(Event-driven)的程式語言。
也就是說,當 JS 被瀏覽器載入後並不會馬上執行,而是透過使用者操作來觸發事件,才會啟動對應程式。例如:滑鼠點擊、鍵盤輸入等。
> 可參考:[HTML DOM Event 對象列表](https://www.w3schools.com/jsref/dom_obj_event.asp)
### 監聽事件 `addEventListener()`
在處理事件時,通常會需要指派監聽者(Event listeners)來監聽事件觸發。
也就是監測 DOM 中的某一元素,當使用者觸發某事件時,就會執行後續動作,語法如下:
```javascript=
element.addEventListener('event', function, useCapture)
```
### 參數值說明
#### 1. event 事件
- 必須
- 指定事件類型
- 常見的事件類別:(詳見 [MDN](https://developer.mozilla.org/zh-CN/docs/Web/Events))
- 滑鼠相關:click、mousedown、mouseenter、mouseleave
- 鍵盤相關:keydown、keypress、keyup
- 瀏覽器相關:scroll、resize
#### 2. function 功能
- 必須
- 指定事件觸發時執行的函式
而 function 的部分有下列兩種寫法,以最常見的 click 做舉例:
- 直接將函式作為參數帶入:回呼函式(callback function)
```javascript=
const element = document.querySelector('.box')
// 先註冊一個事件
element.addEventListener('click', onClick())
// 等事件被觸發才執行 callback function
function onClick(){
alert('click!')
}
```
- 匿名函式(anonymous)
```javascript=
const element = document.querySelector('.box')
element.addEventListener('click', function(){
alert('click!')
})
```
#### 3. useCapture
- 可選
- 布林值,指定事件在捕獲或冒泡階段執行
1. true:補獲
2. false:冒泡(預設值)
參考資料:
1. [重新認識 JavaScript: Day 14 事件機制的原理](https://ithelp.ithome.com.tw/articles/10191970)
2. [HTML DOM addEventListener()方法](https://www.runoob.com/jsref/met-element-addeventlistener.html)
### 事件資訊 `event(e)`
一個事件的發生會包含各種資訊。event 資訊會放在 callback function 的第一個參數。通常取名 `event` 或簡寫 `e`,可以當作是一個「物件」,裡面放著關於此事件的參數值。
以 click 事件為例:
```javascript=
const element = document.querySelector('.box')
element.addEventListener('click', function(e) {
console.log(e)
})
```
點擊元素後會出現下列資訊:

我們可以用「物件」的方式來取得需要的元素,以常用的事件資訊為例。
### 常用的事件資訊
#### click 事件:點擊 DOM 元素時觸發
- e.target:點擊到的元素
- e.screenX:滑鼠離視窗左邊的距離
- e.screenY:滑鼠離視窗上邊的距離
#### keydown 事件:按下鍵盤時觸發
- e.key:按鍵號碼
#### submit 事件:提交表單前觸發,通常用來驗證表單內容
首先建立一個簡易表單:
```htmlmixed=
<form class="login-form">
<div class="account">
Account: <input name="account" type="text">
</div>
<div class="password">
Password: <input name="password" type="password">
</div>
<div class="password2">
Password again: <input name="password2" type="password">
</div>
<div class="submit">
<input name="submit" type="submit" value="Submit">
</div>
</form>
```

點選表單中的 submit 按鈕後,會以預設方法 `GET` 送出資料,也就是把參數帶入原網址送出。而 submit 事件是在表單送出前觸發,通常用來驗證表單內容。
以點選 submit 後彈出視窗為例:
```javascript=
const element = document.querySelector('.login-form')
element.addEventListener('submit', function () {
alert("成功")
})
```

### 阻止預設行為 `e.preventDefault()`
用來阻止瀏覽器上特定元素的預設行為。以下為常見使用方式:
- `<form>` 的 submit 事件:阻止送出表單
- `<a>` 的 click 事件:阻止轉址
- `<input>` 的 keypress 事件:阻止輸入按鍵
以上述例子來說,當 Password 跟 Password again 輸入的值不同時,就可使用 `e.preventDefault()` 來阻止表單送出:
```javascript=
<script>
const element = document.querySelector('.login-form');
element.addEventListener('submit', function(e) {
const pw1 = document.querySelector('input[name="password1"]');
const pw2 = document.querySelector('input[name="password2"]');
// 若密碼不同,就不送出表單
if (pw1.value !== pw2.value) {
alert('密碼不同!')
e.preventDefault();
}
});
</script>
```

參考資料:
1. [[第七週] DOM - 操作 DOM 介面、事件監聽 - Yakim](https://yakimhsu.com/project/project_w7_DOM.html)
2. [[第八週]DOM — 瀏覽器事件處理 - Miahsu](https://medium.com/@miahsuwork/%E7%AC%AC%E5%85%AB%E9%80%B1-dom-%E7%80%8F%E8%A6%BD%E5%99%A8%E4%BA%8B%E4%BB%B6%E8%99%95%E7%90%86-fb8f6178178a)