# F_Vue3筆記
[TOC]
## intro to vue3
- git clone下來,在本地端建立分支並連線作者的遠端分支
```
git checkout -b L2 origin/L2-start
```
- vscode安裝extension `es6-string-html`
## creating the vue app
課程用的是CDN,[官網](https://vuejs.org/guide/quick-start.html#using-vue-from-cdn)有提供
```javascript!
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
```
- 每個 `Vue application instance` 都是經由 `createApp` 函式建立
- 傳入給 `createAPP` 的 APP 這個物件其實是一個 `root component` 根元件
- `root component`根元件可以包含其他元件作為他的子元件,也就是他是最頂層的元件
- 例如:APP.vue檔可以匯入NavBar.vue、Footer.vue等,而NavBar.vue 也可以再匯入 Form.vue等,形成元件樹
### SFC
全名Single-File Components,就是專案看到的`.vue檔`,每個vue檔就是完整的元件,裡面包含三個區塊:
- 邏輯`(JavaScript/<script>)`
- 模板`(html/<template>)`
- 樣式`(css/<style>)`
SFC的結構:
```javascript!
<template>
<div>
<!--HTML-->
</div>
</template>
<script>
// JavaScript
</script>
<style>
/*CSS*/
</style>
```
一個 `.vue` 檔中:
- `<template>` 只能有一個
- `<script>` 只能有一個 (不包含`<script setup>`)
- `<script setup>` 只能有一個
- `<style>` 可以多個
[參考資料](https://ithelp.ithome.com.tw/articles/10295418)
---
影片提到:
- `{{}}`:moustache syntax
- 裡面可寫 JS 表達式
- 可寫字串
- 可寫三元運算
```javascript!
<div id="app">
<h1>{{ product }}</h1>
<p>{{ firstName + lastName }}</p>
<span>{{ clicked ? true : false }}</span>
</div>
```
reactivity system 響應式系統
## vue directives
### v-bind
1. 用來綁定html屬性,例如:class、alt、src等
> 因為 `{{}}` 無法使用在html的屬性,所以才有v-bind
2. v-bind 的縮寫為 `:`
```html!
<div id="app">
<div>
<a v-bind:href="url">Google</a>
<!-- 縮寫寫法 -->
<a :href="url">Google</a>
</div>
</div>
```
### v-if 、v-show
- v-if
- v-show
- 實際渲染後tag還存在,只是style="display:none"
```html
<p v-show="false">In Stock</p>
<!-- 實際渲染樣子 -->
<p style="display:none">In Stock</p>
```
### v-if 、v-show
- v-if
- 根據綁定條件決定是否渲染或是銷毀
- v-else-if、v-else為可選,根據需求使用,但不能單獨使用,必須跟在 v-if 後面
- 會重新渲染,每次渲染就發一次API
```html!
<p v-if="inventory >10">In Stock</p>
<p v-else-if="inventory<10 && inventory>0">almost sold out</p>
<p v-else>out of Stock</p>
```
- v-show
- 透過css控制顯示。如判斷為true,是display;判斷為false,則display:none
- 實際渲染後tag還存在,只是隱藏起來
- 不會重新渲染,只有第一次掛載發API
```html
<p v-show="false">In Stock</p>
<!-- 實際渲染是隱藏它 -->
<p style="display:none">In Stock</p>
```

[Day 8: 認識 Vue directive 和 v-if v.s v-show](https://ithelp.ithome.com.tw/articles/10297034)
[vue doc - template syntax](https://vuejs.org/guide/essentials/template-syntax.html#template-syntax)
[vue doc - Built-in Directives](https://vuejs.org/api/built-in-directives.html#built-in-directives)
[vue doc - Conditional Rendering](https://vuejs.org/guide/essentials/conditional.html)
### v-for
可接受的value類型有:數字、字串、陣列(包含Map/Set)、物件
#### 數字
```html!
<div id="app">
<ul>
<li v-for="number in 5">{{number}}</li>
</ul>
</div>
```
從1開始,數字須為正整數

#### 字串
```html!
<div id="app">
<ul>
<li v-for="char in myName">{{char}}</li>
</ul>
</div>
```
```javascript!
const app = Vue.createApp({
data(){
return {
myName:"Fang"
}
}
})
```

#### 陣列
```html
<ul>
<li v-for="item in variants" :key="item.id">{{item.color}}</li>
</ul>
```
可以使用解構
```html
<ul>
<li v-for="({id,color},index) in variants" :key="id">{{index}} : {{color}}</li>
</ul>
```
```javascript!
// main.js
const app = Vue.createApp({
data() {
return {
product: 'Socks',
image: './assets/images/socks_blue.jpg',
inStock: true,
details: ['50% cotton', '30% wool', '20% polyester'],
variants: [
{ id: 1234, color: "green" },
{ id: 1235, color: "blue" },
],
}
}
})
```
#### 物件
可以拿到物件的 `value` 、 `key` 、 `index`
```html
<ul>
<li v-for="(value,key,index) in customersData">{{key}}: {{value}}</li>
</ul>
```
#### key attribute
- vue用來識別和追蹤元素,不會顯示在html上
- 建議綁定key屬性,並==使用唯一的ID==,讓vue能夠正確追蹤
#### 補充
`v-if`和`v-for`不能放在同一個元素上,`v-if`的優先權高於`v-for`,會報錯
```html!
<li v-for="todo in todos" v-if="!todo.isComplete">
{{ todo.name }}
</li>
```
[Day 9: v-for 與他的坑 feat. key & v-if](https://ithelp.ithome.com.tw/articles/10297907)
[Vue doc- special attribute](https://vuejs.org/api/built-in-special-attributes.html#key)
### v-on
1. 縮寫為`@`
```javascript!
v-on:click="handler"
// 縮寫
@click="handler"
```
click:監聽的事件
handler:觸發的處理器,有 `inline handler` 、`method handler`和 `Calling Methods in Inline Handlers`
```html!
<-- inline handler 直接寫表達式-->
<div>
<button @click="count+=1">
<p>{{count}}</p>
</div>
<-- method handler -->
<div>
<button @click="addToCart">
<p>{{count}}</p>
</div>
```
```javascript!
const count = ref(0);
function addToCart(){
count.value += 1;
}
```
2. Event Modifiers 事件修飾符
- `.stop`
- `.prevent`
- `.self`
- `.capture`
- `.once`
- `.passive`
範例:
```javascript!
<form @submit.prevent = "onSubmit"></form>
```
3. Key Modifiers 按鍵修飾符
```html!
<!-- 只有在 key 為 enter 時調用 submit -->
<input @keyup.enter="submit" />
```
[Vue Doc:Event Handling](https://vuejs.org/guide/essentials/event-handling.html)
### class & style bindings
```html
<!-- css name為駝峰 -->
<div :style="{backgroundColor:blue}">
blue
</div>
<!-- css name為烤肉串,須多加引號才不會報錯 -->
<div :style="{'background-color':blue}">
blue
</div>
```
渲染出來長這樣
```html
<div style="background-color:blue">
blue
</div>
```
如果style比較多,可以包裝成物件傳入:
```html
<div :style="styles"></div>
```
```javascript!
const styles = ref({fontSize:"18px",fontWeight:"bold"})
```
當out of stock時,add button按鈕要套反灰設計&不能增加cart數量:
```html
<!-- inStock為false -->
<!-- :disabled="!inStock" 意思disabled屬性為true-->
<button
class="button"
:class="{disabledButton:!inStock}"
:class="[{[disabledButton]:!inStock}]" // 陣列裡用物件語法
:class="[!inStock ? disabledButton:'']" // 三元寫法
@click="addToCart"
:disabled="!inStock"
>
Add to Cart
</button>
```
```css!
.disabledButton {
background-color: #d8d8d8;
cursor: not-allowed;
}
```
可以綁定陣列渲染多個class
> Chris:vue 是如何做到抽換 class? 要知道這件事
### computed properties
1. 依賴的響應式資料變動時,重新計算結果並回傳
2. 分成 唯讀 和 可讀可寫 情境:
- `computed(() => someValue)` 傳入 getter ,計算新值
- `computed({ get, set })` 要手動賦值就要傳入 getter 和 setter
3. 會緩存資料
4. 適合單純加工資料
補充:getter function
1. 不會帶入參數
2. 唯一目的是返回計算後的值
3. 不會直接修改任何數據,只是讀取並計算
```javascript!
// composition api
const { createApp, ref, reactive } = Vue;
const app = createApp({
setup() {
const cart = ref(0);
const product = ref("Socks");
const brand = ref("Vue Mastery");
const image = ref("./assets/images/socks_blue.jpg");
const inStock = ref(true);
const details = ref(["50% cotton", "30% wool", "20% polyester"]);
const variants = ref([
{ id: 2234, color: "green", image: "./assets/images/socks_green.jpg" },
{ id: 2235, color: "blue", image: "./assets/images/socks_blue.jpg" },
]);
const styles = ref({
fontSize: "18px",
fontWeight: "bold",
});
function addToCart() {
cart.value += 1;
}
function updateImage(variantImage) {
image.value = variantImage;
}
return {
cart,
product,
brand,
image,
inStock,
details,
variants,
styles,
addToCart,
updateImage,
};
},
});
```
```javascript!
// option api
const app = Vue.createApp({
data() {
return {
cart: 0,
product: "Socks",
brand: "Vue Mastery",
selected: 0,
details: ["50% cotton", "30% wool", "20% polyester"],
variants: [
{
id: 2234,
color: "green",
image: "./assets/images/socks_green.jpg",
quantity: 50,
},
{
id: 2235,
color: "blue",
image: "./assets/images/socks_blue.jpg",
quantity: 0,
},
],
onSale: true,
};
},
methods: {
addToCart() {
this.cart += 1;
},
updateVariant(index) {
this.selected = index;
},
},
computed: {
title() {
return this.brand + " " + this.product;
},
image() {
return this.variants[this.selected].image;
},
inStock() {
return this.variants[this.selected].quantity;
},
sale() {
if (this.onSale) {
return this.brand + " " + this.product + "is onsale.";
}
return "";
},
},
});
```
### components & Props
encapsulate:封裝
元件有各自的獨立作用域,無法拿取外部的資料,如要使用到外部資料,用`props`來解決。
`props` : a custom attribute for passing data into a component (自訂屬性將資料傳遞給元件)
範例:
```javascript!
// App.vue
<script setup>
import { ref } from 'vue'
import PropsComponent from './PropsComponent.vue'
const posts = ref([
{ id: 1, title: 'My journey with Vue' },
{ id: 2, title: 'Blogging with Vue' },
{ id: 3, title: 'Why Vue is so fun' }
])
</script>
<template>
<PropsComponent
v-for = "post in posts"
:key = "post.id"
:newTitle = "post.title" // 用newTitle來當作props名稱
/>
</template>
```
在父元件上引入子元件`PropsComponent.vue`,並用v-for渲染子元件的`<p>`內容
```javascript!
// PropsComponent.vue
<script setup>
const props = defineProps(['newTitle']) // 接收父元件上的newTitle
</script>
<template>
<p>{{newTitle}}</p>
</template>
```
如果沒有使用`<script setup>`,寫法改成這樣:
```javascript!
<script>
export default {
props: ['newTitle'],
setup(props) {
console.log(props.title)
}
}
</script>
```
[vue doc-Components Basics](https://vuejs.org/guide/essentials/component-basics.html)
### communicating Events
`emitting Events` : tell "parent" when events happens,意思是子元件執行父元件的方法用 `emit`
子元件的按鈕點擊時,觸發父元件的事件處理:
```html
// index.html
<product-display
:premium="premium"
@add-to-cart="updateCart"
></product-display>
```
```javascript!
// main.js
const app = Vue.createApp({
data() {
return {
cart: [],
premium: true,
};
},
methods: {
updateCart(id) {
this.cart.push(id);
},
},
});
```
```javascript!
// 子元件 ProductDetails.js
methods: {
addToCart() {
this.$emit("add-to-cart", this.variants[this.selectedVariant].id);
},
}
```
### Forms & v-model
`payload` : 具有意義、有效的資料
`v-model` : 雙向綁定,可添加modifier修飾符
- `lazy` : 每次`change`事件後才更新數據,預設的話是每次`input`事件就更新數據
```javascript!
<input v-model.lazy="message" />
```
- `number` : 輸入自動轉換為數字
```javascript!
<input v-model.number="age" />
```
如果 `<input type="number">`,`v-model` 會自動轉數字,不用 `.number`
- `trim` : 自動去除輸入內容中兩端的空格
```javascript!
<input v-model.trim="message" />
```
[vue doc:Form Input Bindings](https://vuejs.org/guide/essentials/forms.html)
### vue cli
`cli` : command line interface
Hot Module Replacement (HMR) 熱更新
> exchanges, adds, or removes modules while an application is running, without a full reload.
> 當應用程式運行時,替換、新增或移除模組無須重整加載頁面。
### single file components
```javascript!
<style scoped></style>
```
`scoped` : applies styles to component only 僅將樣式套用至元件
定義一個vue元件,並指定元件的名稱:
```javascript!
// vue3 option api寫法
// name 用來定義 vue 元件名稱
// components 屬性來手動註冊元件
<script>
export default {
name: "ParentComponent",
components: {
EventCard
}
};
</script>
// vue 3 Composition API
// 通常不需要手動定義 name
// 用import元件方式,不需要手動註冊
<script setup>
import EventCard from "./EventCard.vue";
</script>
<template>
<EventCard />
</template>
```
```javascript!
// EventCard.vue
<script setup>
const props = defineProps({
event: {
type: Object,
required: true,
},
})
const { event } = props // 解構
</script>
<template>
<div class="event-card">
<span>@{{ event.time }} on {{ event.date }}</span>
<h4>{{ event.title }}</h4>
</div>
</template>
```
[vue doc: Props](https://cn.vuejs.org/guide/components/props)
### vue router essential
`SPA (Single-Page Application)`: 整個應用程式所有內容都在一個頁面中呈現(one `index.html` page)
```javascript!
// router/index.js
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView,
},
{
path: '/about',
name: 'about',
component: () => import('../views/AboutView.vue'),
},
],
})
```
`path`: url
`name`: name of route
`component`: which view component to render
Vue Router 提供的內建元件:`router-link`、`router-view`
```javascript!
<template>
<header>
<img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" />
<div class="wrapper">
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
</nav>
</div>
</header>
<RouterView />
</template>
```
[vue router官網](https://router.vuejs.org/zh/guide/)
#### server-side routing 伺服器端路由
- 過去使用這種方式
- 瀏覽器向 server 發 URL 請求,server 回傳 page 給瀏覽器
- 如果使用者點擊該頁面的連結,瀏覽器又會向 server 發URL 請求。**每次載入頁面瀏覽器都會刷新**
#### client-side routing 客戶端路由
- 瀏覽器向 server 發 URL 請求,server 傳回該頁面 (index.html)
- vue 藉由 vue router 幫忙,就**無需重新加載頁面**即可呈現
- `<router-view>` 就像是一個placeholder,會被route的元件程式碼取代
- 應用程式首次載入時,會載入完整應用程式的程式碼
### API calls with Axios
如不要寫死程式碼,透過api call 來 fetch 外部資料
- 瀏覽器向 server 發 events 請求,server 傳回 `JSON`
mock database: [My JSON Server](https://my-json-server.typicode.com/)
#### lifecycle hooks
元件具有生命週期,並在生命週期中呼叫不同的鉤子(hook)或方法
`axios` 是基於 promise 且非同步
如果每個元件都有axios instance,API 程式碼會遍佈整個應用程式,這樣會很混亂且難debug
所以,在應用程式內有一個專門的位置,來處理 api 行為,在 src 建立一個 service folder ,並建立一個 new file 為 EventService.js,把 axios 包裝在這裡,要使用再各別 import 進去
```javascript!
// EventService.js
import axios from 'axios'
const apiClient = axios.create({
baseURL: 'https://my-json-server.typicode.com/Code-Pop/Real-World_Vue-3',
withCredentials: false,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
})
export default {
getEvents() {
return apiClient.get('/events') // 添加到 baseURL 上
}
}
```
### Dynamic Routing
將該 event 的 ID 新增至此URL的末尾
`https://my-json-server.typicode.com/Code-Pop/Real-World_Vue-3/events/123`
如何在 router 的 parameters 新增 event id ❓
```javascript!
// EventCard.vue
<template>
<router-link
class="event-link"
:to="{ name: 'EventDetails', params: { id: event.id } }"
>
<div class="event-card">
<span>{{ $route.params.id }}</span>
<span>@{{ event.time }} on {{ event.date }}</span>
<h4>{{ event.title }}</h4>
</div>
</router-link>
</template>
```
`to="/event/123"` 原本route.path寫法會hard code
`:to`: 用v-bind綁定物件,並裡面改成放物件 `:to="{ name:'EventDetails', params: { id: event.id } } "`
```javascript!
// router/index.js
const routes = [
{
path: '/event/:id', // dynamic segment
name: 'EventDetails',
props: true, // 預設為false
component: EventDetails
},
]
```
path 屬性把動態字段以「冒號」開始,改成`:id`
`:id` :可以是 username 或是 user id 來進行更新
`props: true` : 將 `route.params` 自動作為 props 傳進元件(例如 EventDetails.vue)
```javascript!
// EventDetails.vue
export default {
props: ['id'], // 直接拿到 route 中的 id
created() {
EventService.getEvent(this.id)
// this.id 是從 props 來的,不用再從 $route.param.id 拿
}
}
```
`props: ['id']` 是 option api 寫法,改成composition api寫法是用`defineProps`
```javascript!
const props = defineProps(['id'])
```
### Deploying with Render 使用渲染進行部署
部署前需要考慮的事情:
- find a web hosting service 找網路託管服務
- get SSL for a secure domain 取得安全域的SSL
- build the site locally 在本地建立網站
- drop those files into the server 將文件放入伺服器
- ensure its being served correctly 確保其正確服務
如何部署:
- package.json中可以看到有一個script為build,指令下`npm run build`

- 完成後會出現一個 dist 資料夾,裡面有 html、css、js
Render 是自動部署靜態網站,不需要把 `dist/` push 到 GitHub,須確定`src/`、`package.json` 等原始碼 push 上去

遇到 deploy 報錯,原因是 node 版本的 openSSL 跟 webpack 不相容,但改了 node 版本為v16還是一樣
解法:在package.json加入 engines 欄位,Render就會跑正確版本
```
"engines": {
"node": "16"
}
```
重新部署成功了!

影片說到,當點EventList的時候,把網址複製再次貼上會出現not found (如圖)

解決方法:
1. 先到 Render 的 redirects/rewrites 設定

2. 再到 router 加上 not found的`路由` 和 `component`
```javascript!
// index.js
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: NotFound
}
```
`VueX`: storage center for data throughtout your app
## Vue Router
### receiving URL parameters
1. 讀取URL的查詢參數(query parameters):
例如:網址為 `http://localhost:8080/events?page=4`
要取得 query parameters,搭配錢字號 `$route.query.page`,會得到 `4` 的結果
2. 讓 parameter 成為URL的一部分:
```javascript!
// src/router/index.js
// 冒號後為 route parameter
const routes = [
...
{ path:'/events/:page', component:Events }
]
// 元件
<template>
<h1>you are on page{{ $route.params.page }}</h1>
</template>
```
用`$route.params.page`取得值,但這樣寫法會`高度耦合(hightly coupled)` 元件,也就是元件直接依賴 vue router 的 $route 物件。
:star: pure 寫法
```javascript!
// src/router/index.js
const routes = [
...
{ path:'/events/:page', component:Events, props:true }
]
// 元件
<script>
export default{
props:['page']
}
/* composition api寫法
const props = defineProps({
page: String
})
*/
</script>
<template>
<h1>you are on page{{ page }}</h1>
</template>
```
- 在 route 用 props 傳遞出去,元件再接收
- 元件更容易測試與重用
3. configure a component from the router
使用`Props Object Mode`
```javascript!
// src/router/index.js
const routes = [
{
path: '/',
name: 'Home',
component: Home,
props: {
showExtra: true
}
}
]
// src/views/Home.vue
<template>
<div class="home">
<h1>This is a home page</h1>
<div v-if="showExtra"></div>
</div>
</template>
<script>
export default {
props: ['showExtra']
}
</script>
```
4. transform query parameters
使用`Props function Mode`,函式可以做更複雜的轉換
網址為 `http://localhost:8080/?e=true`
```javascript!
const routes = [
{
path: '/',
name: 'Home',
component: Home,
props: (route)=>({showExtra:route.query.e})
}
]
```
Vue Router 會將 URL 查詢字串中的 `e` 參數,轉成一個叫 `showExtra` 的 prop 傳給 `Home` 元件。
這樣做讓元件不需要知道路由的細節,只關心它收到的 `showExtra`,這樣元件的職責就更單一、也更方便重複使用。
### buliding pagination 建立分頁
[mock JSON server](https://my-json-server.typicode.com/Code-Pop/Touring-Vue-Router)
例如有一個網址:`/events?_limit=2&_page=3`
`_limit`:每頁多少項目
`_page`:正在哪一頁
1. 修改 EventService API 並帶入 perPage 和 page 參數
```javascript!
getEvents(perPage, page) {
return apiClient.get(`/events?_limit=${perPage}&_page=${page}`)
},
```
2. 在 router 使用 props function mode 設置當前頁
假如 query parameter 存在且是整數就取得,否則預設為`1`
```javascript!
{
path: '/',
name: 'EventList',
component: EventList,
props: route => ({ page: parseInt(route.query.page) || 1 })
},
```
`parseInt(route.query.page)`:因為 route.query.page 拿到是字串,須轉換成數字
`1`:表示預設為第一頁
> 用函式寫法是因為「要動態計算傳資料給props」。
3. 在 EventList.vue 呼叫 api 的地方修改參數
```javascript!
const props = defineProps(['page'])
const page = computed(() => props.page)
onMounted(() => {
// 設定每頁2個item和當前頁的參數
EventService.getEvents(2, page.value)
.then(response => {
events.value = response.data
})
.catch(error => {
console.log(error)
})
})
```
為什麼`props.page`要 computed :question:
因為 `defineProps(['page'])`原本是寫成
```javascript!
props: {
page: {
type: Any
}
}
```
`props` 是 reactive proxy,而computed會把`props.page`變成 `ref`
也就是 props 有包含 page 屬性,就可以 `props.page` 取值,因為需要響應式變數來追蹤變化,透過 computed 包住`props.page`,才會自動更新。
4. 在EventList.vue增加分頁連結
```javascript!
<template>
<h1>Events for Good</h1>
<div class="events">
<EventCard v-for="event in events" :key="event.id" :event="event" />
<router-link
:to="{ name: 'EventList', query: { page: page - 1 } }"
rel="prev"
v-if="page != 1"
>
>Prev Page
</router-link>
</div>
</template>
```
- `:to="{ name: 'EventList', query: { page: page - 1 } }` :導向 EventList ,並在網址加上 `?page=...` 的參數,query參數會出現在`?`的後面
網址長的樣子:
```
http://localhost:8080/?page=3
```
- `rel='prev'`:為了SEO
:star: 補充:params參數 和 query參數 使用時機
```
inginging
```
但因為 onMounted 生命週期僅在初始載入時調用,而不是在元件重用時調用,所以要透過 `watchEffect` 來監聽page更新
```javascript!
const events = ref(null)
onMounted(() => {
watchEffect(() => {
// 清除頁面上的事件,以便使用者知道 API 已被調用
events.value = null
EventService.getEvents(2, page.value)
.then(response => {
events.value = response.data
})
.catch(error => {
console.log(error)
})
})
})
```
watchEffect:
- 第一次執行會自動跑一次(在上面程式碼會在 mounted 時跑一次,之後 page.value 改變也會再跑)
- 寫法比較簡潔,不需要明確指定響應式資料來源
watch:
- 要手動指定監聽的變數
- 不會自動在一開始執行,除非加上 `{ immediate: true }`
上面的程式碼改寫成 watch :
```javascript!
onMounted(() => {
watch(page,(newPage) => {
events.value = null
EventService.getEvents(2, newPage)
.then(response => {
events.value = response.data
})
.catch(error => {
console.log(error)
})
}), { immediate: true })
})
```
5. 計算頁數
```javascript!
const totalEvents = ref(0)
EventService.getEvents(2, page.value)
.then(response => {
events.value = response.data
totalEvents.value = response.headers['x-total-count']
})
.catch(error => {
console.log(error)
})
```
`x-total-count`: **HTTP 回應標頭(header)**,常用在 **分頁(pagination)API**,也就是資料庫中符合條件的**資料總筆數**。
從資料庫拿到總筆數後,就能計算總共會有幾頁:
```javascript!
const hasNextPage = computed(() => {
let totalPages = Math.ceil(totalEvents.value / 2)
return page.value < totalPages // 代表不是最後一頁
})
```
最後排版一下prev page和 next page

### nested routes
Lazy Loading Routes
```javascript!
// 原本 import UserDetails from './views/UserDetails'
// 改成
const UserDetails = () => import('./views/UserDetails.vue')
```
[Vue doc: Lazy Loading Routes](https://router.vuejs.org/guide/advanced/lazy-loading.html#%E8%B7%AF%E7%94%B1%E6%87%92%E5%8A%A0%E8%BD%BD)
建立一個 layout 元件給event底下所有的子元件,layout包含navigation和fetch events,這樣就不用每個子元件都寫。
```javascript!
{
path: '/event/:id',
name: 'EventLayout',
props: true,
component: () => import('../views/event/Layout.vue'),
children: [
{
path: '',
name: 'EventDetails',
component: () => import('../views/event/Details.vue')
}
]
},
```
嵌套路由:在父層寫 `children`,==子層的path為空==,表示載入父層的根路徑`/event/:id`
:question: 如果沒有發送 :id ,它將尋找並使用存在的 :id 參數
```javascript!
// 把 params:{id} 刪掉,因為layout的route本身有:id,子層route的id就會來自父層
<template>
<div v-if="event">
<h1>{{ event.title }}</h1>
<div id="nav">
<router-link :to="{ name: 'EventDetails' }">Details</router-link>
|
<router-link :to="{ name: 'EventRegister' }">Register</router-link>
|
<router-link :to="{ name: 'EventEdit' }">Edit</router-link>
</div>
<router-view :event="event" />
</div>
</template>
```
[vue doc:Nested Routes](https://router.vuejs.org/guide/essentials/nested-routes.html)
### Redirect & Alias
舊的 route 重新導向至新的 route
1. 如何改網址從 `/about` 到 `/about-us`?
- 方法一:使用 `redirect`
```javascript!
{
path: '/about-us', // 將about改成about-us
name: 'About',
component: () => import('@/views/About.vue')
},
{
path: '/about',
redirect: { name: 'About' }
// 使用者輸入about會重新導向到name為About的網址,也就是about-us
}
```
`redirect`: 將路由導向另一個指定的路由
可以有三種寫法:
1. 直接寫字串:`redirect: '/home'`
2. 寫成物件:`redirect: { path: '/home', query: { foo: 'bar' } }`
3. 寫成函式:`redirect: (to) => {...}`
- 方法二:使用 `alias` 別名,但如果在意SEO搜尋就不建議
```javascript!
{
path: '/about-us',
name: 'About',
component: () => import('@/views/About.vue')
alias: '/about'
},
```
可以把 alias 想成這個路由有小名叫做 `/about`
2. 如何改網址從 `/event/:id` 到 `/events/:id`?
```javascript!
{
path: '/event/:id',
redirect: () => {
return {
name: 'EventDetails'
}
}
},
// {
// path: '/event/:id',
// redirect: to => {
// return {
// name: 'EventDetails',
// params:{ id:to.params.id }
// }
// }
// },
```
`to`參數: vue router 的 route 物件,包含使用者要前往的路由。透過to參數就能找到動態參數id。
瀏覽器的Vue可以查看 route 物件:

要改的路由本身底下如有嵌套路由的話:
```javascript!
{
path: '/event/:afterEvent(.*)', // afterEvent自定義參數
redirect: to => {
return { path: '/events/' + to.params.afterEvent }
}
},
```
`.*`: 正則表達式寫法,表示任何內容、任何長度(包括空字串)
也就是 `:afterEvent(.*)` 會**匹配 `/event/` 後面「所有的東西」**
[vue doc:路徑參數正則表達式](https://router.vuejs.org/zh/guide/essentials/dynamic-matching.html#%E6%8D%95%E8%8E%B7%E6%89%80%E6%9C%89%E8%B7%AF%E7%94%B1%E6%88%96-404-Not-found-%E8%B7%AF%E7%94%B1)
[Vue doc: Redirect and Alias](https://router.vuejs.org/guide/essentials/redirect-and-alias.html#Redirect-and-Alias)
### programmatic navigation
> This is the method called internally when you click a `<router-link>`, so clicking `<router-link :to="...">` is the equivalent of calling `router.push(...)`.
> 當點擊 router-link,內部會調用 router.push()
#### router.push
用官網的範例
```javascript!
// 字串路徑
router.push('/users/eduardo')
// 路徑的物件
router.push({ path: '/users/eduardo' })
// 用name,並帶上參數
router.push({ name: 'user', params: { username: 'eduardo' } })
// 帶查詢參數,结果 /register?plan=private
router.push({ path: '/register', query: { plan: 'private' } })
// 带 hash,结果 /about#team
// hash用來跳到頁面中某個元素的位置
router.push({ path: '/about', hash: '#team' })
```
#### router.replace
和 `router.push` 一樣都是導向到另一個路由,差別在瀏覽器歷史紀錄history。
- `router.push`:正常頁面導航
範例:使用者從首頁點擊一篇文章
瀏覽器歷史紀錄:`home`、`/post/123`
```javascript!
router.push('/post/123')
```
- `router.replace`:導入後不希望返回導航
範例:使用者登入後自動跳轉到dashboard,不希望使用者按返回回到登入頁。
瀏覽器歷史紀錄:`/dashboard`(取代掉 `/login`)
```javascript!
router.replace('dashboard')
```
也可以在 `router.push` 增加屬性 `replace:true`
```javascript!
router.push({ path: '/home', replace: true })
// 等同於
router.replace({ path: '/home' })
```
補充:
`router.go(1)`:向前一條紀錄,等同router.forward()
`router.go(-1)`:向後一條紀錄,等同router.back()
等同於瀏覽器的前進 / 返回按鈕!
[vue doc: Programmatic Navigation](https://router.vuejs.org/guide/essentials/navigation.html#%E7%BC%96%E7%A8%8B%E5%BC%8F%E5%AF%BC%E8%88%AA)
[vue doc: Vue Router and the Composition API](https://router.vuejs.org/guide/advanced/composition-api.html)
[MDN: History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API)
### error handling and 404s
假如使用者輸入一個不存在的路由,設計頁面導向 NotFound.vue 元件。通常用來處理 404 頁面,**匹配所有未被其他路由捕捉的網址**
```javascript!
// router/index.js
{
path: '/:catchAll(.*)',
name: 'NotFound',
component: () => import('@/views/NotFound.vue')
}
```
`catchAll`:是自訂參數,是習慣上的命名因為有語意,也可以取`path: '/:notFound(.*)'`之類
不論是頁面的 404 還是 event 的 404 ,都可以重用 NotFound.vue 元件!
```javascript!
{
path: '/404/:resource',
name: '404Resource',
component: () => import('@/views/NotFound.vue'),
props: true
}
```
```javascript!
// NotFound.vue
// 這裡的resource可以是page或是event,props預設為page
<template>
<h1>Oops!</h1>
<h3>The {{ resource }} you are looking for is not here.</h3>
<router-link :to="{ name: 'EventList' }">Back to the home page</router-link>
</template>
<script setup>
import { defineProps } from 'vue'
defineProps({
resource: {
type: String,
required: true,
default: 'page'
}
})
</script>
```
在 NotFound.vue 加上 defineProps: resource屬性
```javascript!
onMounted(() => {
EventService.getEvent(props.id)
.then(response => {
event.value = response.data
})
.catch(error => {
console.log(error)
router.push({ name: '404Resource', params: { resource: 'event' } })
})
})
```
打 api 時,如果沒有這個 event,則跑 catch 區塊,router.push 到 404, params 直接設定為 `event`
網址為 `http://localhost:8080/404/event`

範例: call api 的 EventService.js 假設 baseUrl 寫錯
```javascript!
const apiClient = axios.create({
baseURL: 'https://my-json-server.typicode.com/Code-Pops/Touring-Vue-Router', // 測試網址錯誤:pop後面多加s
withCredentials: false,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
})
```
建立一個 NetworkError 元件
```javascript!
<template>
<div class="networkError">
<h1>Uh-Oh!</h1>
<h3>
It looks like you're experiencing some network issues, please take a
breath and <a href="#" @click="router.go(-1)">click here</a> to try again.
</h3>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()
</script>
```
建立 NetworkError 的 router
```javascript!
{
path: '/network-error',
name: 'NetworkError',
component: () => import('@/views/NetworkError.vue')
}
```
```javascript!
// Layout.vue
onMounted(() => {
EventService.getEvent(props.id)
.then(response => {
event.value = response.data
})
.catch(error => {
if (error.response && error.response.status == 404) {
router.push({ name: '404Resource', params: { resource: 'event' } })
} else {
router.push({ name: 'NetworkError' })
}
})
})
```
在 catch 寫 if 判斷,假入 event 不存在,載入 404 ;否則 network error
```javascript!
// EventList.vue
onMounted(() => {
watchEffect(() => {
events.value = null
EventService.getEvents(2, page.value)
.then(response => {
events.value = response.data
totalEvents.value = response.headers['x-total-count']
})
.catch(() => {
router.push({ name: 'NetworkError' })
})
})
})
```
因為 EventList 也有call api,所以如有錯,則導入到network error
### flash message
全域儲存機制
創建一個 reactive 全域物件`GStore`
```javascript!
// main.js
import { createApp, reactive } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
const GStore = reactive({ flashMessage: '' })
createApp(App)
.use(store)
.use(router)
.provide('GStore', GStore)
.mount('#app')
```
`provide`: 提供給後代元件注入值,通常會跟`inject`搭配一起使用。
```javascript!
// register.vue
<script setup>
import { defineProps, inject } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const props = defineProps(['event'])
const GStore = inject('GStore')
function register() {
GStore.flashMessage = `You are successfully registered for ${props.event.title}`
setTimeout(() => {
GStore.flashMessage = ''
}, 3000)
router.push({ name: 'EventDetails' })
}
</script>
```
在register.vue 的register函式 `注入(inject)` GStore
當點擊 register 按鈕後 flash message 本來為空字串被改成`You are successfully registered for ${props.event.title}`
```javascript!
// App.vue
<div id="flashMessage" v-if="GStore.flashMessage">
{{ GStore.flashMessage }}
</div>
```
最後在 App.vue 的 template 中加入 GStore.flashMessage 的文字判斷和樣式。
### in component route guards (組件內的全域路由守衛)
`onBeforeRouteUpdate`:單一元件內,同一元件路由參數變動時執行。例如:`/user/1` ➜ `/user/2`
`onBeforeRouteLeave`:單一元件內,要離開元件時(可攔下)
`to`:即將導航到的路由
`from`:當前導航正要離開的路由
`next`:vue 2需要用,但vue 3可用回傳值來取代next。沒有回傳值或是回傳true,導航就繼續,如果有回傳false,導航就停止
有以下寫法:
- `return true` 繼續導航
- `return false` 取消導航
- `return '/'` 重新導向 / path
- `return {name:'event-list'}` 重新導向 event-list 路徑
範例:
```javascript!
const unsavedChanges = ref(true)
onBeforeRouteLeave((to, from, next) => {
if (unsavedChanges.value) {
const alert = window.confirm(
'Do you really want to leave? You have unsaved changed!'
)
if (!alert) {
return false
}
}
})
```
### Global and Per-Route Guards
TODO:需要把progress bar在每個頁面呈現
`beforeEach 路由守衛`:**全域**,每次導航前會先攔截
`beforeResolve`
`afterEach`:導航完成之後調用,所以不能取消導航
加上 `return` 原因是讓 vue router 知道載入頁面之前等待 API 呼叫。
為什麼 `beforeRouteEnter` 不用 `return` 但 `beforeRouteUpdate` 就要?
因為`next`會告訴vue router等到API呼叫回傳後,才能進行路由
#### vue router 呼叫的順序
1. router.beforeEach()
2. beforeEnter()
3. beforeRouteEnter()
4. router.beforeResolve()
5. router.afterEach()
6. 最後執行vue元件的生命週期方法
### route meta field
meta 藉由子路由取得繼承,也就是父層路由有 meta field ,那子路由自動繼承 meta field 。
例如:父層是管理者頁面,設定 `meta: {requireAuth:true}`,其子路由也都會一同繼承 `meta: {requireAuth:true}`
[vue doc:route meta field 路由元信息](https://router.vuejs.org/guide/advanced/meta.html#Route-Meta-Fields)
### lazy loading routes 路由懶加載
將路由對應的元件分割,當路由被訪問才加載。
```javascript!
{
path: 'register',
name: 'EventRegister',
component: () => import('@/views/event/Register.vue') // 不載入程式碼除非有請求
}
```
[vue doc: lazy loading routes](https://router.vuejs.org/guide/advanced/lazy-loading.html#Lazy-Loading-Routes)
### scroll behavior 滾動行為
當進入頁面,瀏覽器預設行為會帶使用者到頁面最頂端。
例如:google 搜尋引擎,當滑到底部有分頁,點選第二頁時候,會到第二頁的頂端
但影片一開始的範例點選 next page,仍停留在底部,需要改善使用者介面,所以在 router 加上 `scrollBehavior` :
```javascript!
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
// 滾動到頂端
scrollBehavior() {
return { top: 0 }
}
})
```
按返回前一頁,要帶回到原本頁面的原本的位置:
```javascript!
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { top: 0 }
}
}
})
```
[vue doc:scroll behavior](https://router.vuejs.org/guide/advanced/scroll-behavior.html#Scroll-Behavior)
## vue3 Forms
### basic input
盡可能讓元件保持彈性和複用設計
`<pre></pre>`可以直接在模板中看資料
:face_holding_back_tears: 為什麼 input 沒有吃到樣式? :face_holding_back_tears:

因為 multi-root component (多個根元素) 無法自動繼承 attribute(例如 `type="text"`),需要手動把 `$attrs` 綁到指定的元素例如 `<input>`,這樣父層傳入的屬性才能正確應用。
如果 single-root component(一個根元素),Vue 會自動把這些 attribute 加到 root 上。
解法:手動注入 attribute ,==綁定$attrs 物件==
```javascript!
// BaseInput.vue
<template>
// label 和 input 這樣是兩個root
<label>{{ label }}</label>
<input
v-bind="$attrs" // 在這裡加上$attrs
:placeholder="label"
:value="modelValue"
class="field"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
/* -------------------------*/
<template>
// div這樣是一個root
<div>
</div>
</template>
```
```javascript!
// SimpleForm.vue
<BaseInput label="Title" v-model="event.title" type="text" />
```

BaseInput元件就能吃到父層給的type="text"
[doc: $attrs](https://cn.vuejs.org/api/component-instance#attrs)
其他範例:
```javascript!
<BaseInput label="Name" v-model="name" type="text" maxlength="20" />
```
父層傳來但沒有對應 prop 的所有屬性,例如`type` 和 `maxlength`沒有在 `defineProps` 中定義,就會被自動轉為 `$attrs`
用 `v-bind="$attrs"` 就能讓它們應用到 `<input>`。
```javascript!
@input="$emit('update:modelValue', $event.target.value)"
```
1. `$emit('update:modelValue', $event.target.value)` 發送了一個事件:
- 事件名稱:`update:modelValue`
- 傳遞的值:`$event.target.value`(即使用者輸入的字串)
2. `$event.target.value` 將input 元素的輸入事件的 `value`,**作為參數傳遞給父層**。
```javascript!
// SimpleForm.vue
<BaseInput
label="Title"
type="text"
:modelValue="event.title"
@update:modelValue="($event) => (event.title = $event)"
/>
```
所以 `@update:modelValue` 的 `$event` 接收到子元件的 `value`,並賦值並更新 `event.title`
```javascript!
<BaseInput
:modelValue="name"
@update:modelValue="($event) => name = $event"
// $event參數 就是 $event.target.value傳來的值
/>
```
#### 補充:Component v-model
```javascript!
<BaseInput label="Title" v-model="event.title" type="text" />
```
等於下面寫法:
```javascript!
<BaseInput
label="Title"
type="text"
:modelValue="event.title"
@update:modelValue="($event) => (event.title = $event)"
/>
```
- 使用 `v-model` 綁定的時候,其實背後就是在監聽 `update:modelValue` 這個事件
- `modelValue` 是 Vue 3 的 `v-model` 預設接收 prop
### basic select
在vue 3,如果沒有使用`@`這個語法,事件將以關鍵字 `on` 作為前綴
例如:`@change ➜ onChange`、`@input ➜ onInput`
看不懂意思 :boom: :boom: :angry: :angry: 為什麼 onChange 是寫在 v-bind 裡面?
> GPT解釋這樣做的目的是:
> - 保留並傳遞外部父元件可能傳進來的 `$attrs`(例如 `id`、`class`、`name` 等)
> - 但又要**控制自己的 `onChange` 行為**
> - 如果你在 `$attrs` 裡有傳入 `onChange`,這邊就會**覆蓋它**
```javascript!
// SimpleForm.vue
<BaseSelect
v-model="event.category"
label="Select a category"
:options="categories"
/>
```
```javascript!
// BaseSelect.vue
<template>
<label v-if="label">{{ label }}</label>
<select
:value="modelValue"
v-bind="{
...$attrs,
onChange: ($event) => {
$emit('update:modelValue', $event.target.value);
},
}"
class="field"
>
<option
v-for="option in options"
:value="option"
:key="option"
:selected="option === modelValue"
>
{{ option }}
</option>
</select>
</template>
<script>
export default {
props: {
label: {
type: String,
default: "",
},
modelValue: {
type: [String, Number],
default: "",
},
options: {
type: Array,
required: true,
},
},
};
</script>
```
### auto importing components
### base checkbox
```javascript!
// BaseCheckbox.vue
<template>
<input
type="checkbox"
:checked="modelValue"
@change="$emit('update:modelValue', $event.target.checked)"
class="field"
/>
<label>{{ label }}</label>
</template>
<script>
export default {
props: {
label: {
type: String,
default: "",
},
modelValue: {
type: Boolean,
default: false,
},
},
};
</script>
```
```javascript!
// SimpleForm.vue
<BaseCheckbox
label="Live music"
v-model="event.extras.music"
type="checkbox"
/>
```
### BaseRadio
```javascript!
<template>
<input
type="radio"
:checked="modelValue === value" <!-- 是否勾選 -->
:value="value" <!-- radio 的值 -->
v-bind="$attrs"
@change="onChange"
/>
<label>{{ label }}</label>
</template>
<script setup>
const { label, modelValue, value } = defineProps({
label: {
type: String,
default: "",
},
modelValue: {
type: [String, Number],
default: "",
},
value: {
type: [String, Number],
required: true,
},
});
const emit = defineEmits(["update:modelValue"]);
function onChange() {
emit("update:modelValue", value);
}
</script>
```
```javascript!
// SimpleForm.vue
<div>
<BaseRadio v-model="event.pets" :value="1" name="pets" label="Yes" />
</div>
<div>
<BaseRadio v-model="event.pets" :value="0" name="pets" label="No" />
</div>
```
### BaseRadioGroup
```javascript!
// BaseRadioGroup.vue
<template>
<BaseRadio
v-for="option in options"
:key="option.value"
:value="option.value"
:label="option.label"
:name="name"
:modelValue="modelValue"
@update:modelValue="$emit('update:modelValue', $event)"
/>
</template>
<script setup>
import BaseRadio from "./BaseRadio.vue";
const { options, name , modelValue } = defineProps({
options: {
type: Array,
required: true,
},
name: {
type: String,
required: true,
},
modelValue: {
type: [String, Number],
required: true,
},
});
</script>
```
```javascript!
// SimpleForm.vue
<div>
<BaseRadioGroup
:modelValue="event.pets"
@update:modelValue="($event) => (event.pets = $event)"
name="pets"
:options="petOptions"
/>
</div>
```


在排版上,如果 radio group 要水平排版,需要用 `<span>` 包 ; 如果要垂直排版,需要用 `<div>` 包。這時候可以使用 `<component>` **動態渲染其他組件** 或 **HTML 元素**。
```
<component :is />
```
`:is` 屬性:決定渲染 哪個元件 或 標籤
範例:
```javascript!
<component :is="href ? 'a' : 'span'"></component>
```
意思是 href 為 true,則 `<component>` 會變成 `<a></a>` ,反之 `<component>` 會變成 `<span></span>`
:star: 為什麼把 `v-for` 和 `:key` 放在 `<component>` 上?
因為要把每個 radio 的外層包容器元素
```javascript!
// v-for寫在component上
<div>
<BaseRadio />
</div>
<div>
<BaseRadio />
</div>
// v-for寫在BaseRadio上
<BaseRadio />
<BaseRadio />
```
[doc: 動態組件](https://cn.vuejs.org/guide/essentials/component-basics.html#dynamic-components)
[doc: <component>](https://cn.vuejs.org/api/built-in-special-elements.html#component)
[doc: is](https://cn.vuejs.org/api/built-in-special-attributes.html#is)
### submittimg forms
XMLHTTPRequests(XHR)
> `XMLHttpRequest` (XHR) objects are used to interact with servers. You can retrieve data from a URL without having to do a full page refresh. This enables a Web page to update just part of a page without disrupting what the user is doing.
>
> JavaScript 提供的一個內建物件,**主要用來向伺服器發送請求、接收資料**,而不需要重新載入整個頁面。
- 表單預設行為:瀏覽器會重載入新頁面
- `submit.prevent` 這個修飾符會呼叫`e.preventDefault()`
- 當點擊按鈕標籤有`type="submit"` ,`<form></form>`標籤會發送sumbit事件
安裝axios
```
npm i axios
```
發出特定的請求,例如:POST
```
post(URL , payload 物件)
```
- 另外建議表單在前端做預先驗證或客戶端驗證 和 顯示加載 loading spinner
[JSON server - vue form](https://my-json-server.typicode.com/Code-Pop/Vue-3-Forms)
到開發工具的`network`頁面可以觀察 fetch/XHR,Header可以看到為成功請求

Response則可以觀察伺服器回傳回來的資料

如果是在console.log頁面,則可以看到完整的回傳資料

### basic a11y for our component
`<fieldset>`:**將一組相關的表單欄位包在一起**的容器,螢幕閱讀器會解釋欄位間的關係。
`<legend>`:`<legend>` 必須放在 `<fieldset>` 裡面,它提供這一組欄位的**說明標題**。這段文字會被螢幕閱讀器唸出來。
placeholder不能取代說明的標籤(label)
```javascript!
// 最快的解決方式,確保input可以連接到label
<label>
Title
<input>
</label>
```
通用唯一辨識碼(Universally Unique Identifier,縮寫:_UUID_)
`aria-describedby`:提供補充說明,不取代label
`aria-live`:某段內容變動時,自動唸出來通知使用者。例如:送出成功、3秒後自動跳轉等
```javascript!
aria-live="polite" // 有空再唸(不打斷使用者)
aria-live="assertive" // 立即打斷並唸出內容
```
`aria-invalid`:這個表單欄位目前是無效的(驗證失敗),但不會執行表單驗證!
```javascript!
<input type="email" id="email" aria-invalid="true">
```
submit button不要用disable,因為輔助科技不會取得到回饋而被忽略
[MDN: accessibility( a11y 無障礙 )](https://developer.mozilla.org/en-US/docs/Web/Accessibility)
## validating vue3 Forms
### setting up
安裝vee-validate
```
npm i vee-validate --save
```
#### useField()
```javascript!
<script setup>
import { useField } from 'vee-validate';
const { value, errorMessage } = useField('name', inputValue => !!inputValue);
</script>
```
因為 useField() 是回傳一個物件,可以用解構的方式取得物件裡的屬性,能使用的屬性很多,詳細看 [vee-validate:useField](https://vee-validate.logaretm.com/v4/api/use-field/)。
第一個屬性 `value`:是Vue ref,代表欄位的值
第二個屬性 `errorMessage`:是一個 `ref`,當驗證失敗時,它會包含錯誤訊息,否則為空字串


:star: `!regex.test(String(value).toLowerCase()`為什麼轉成小寫?
Email 使用者名稱(@ 前):技術上是區分大小寫的。
Email 網域名稱(@ 後):一律 **不區分大小寫**。
開發上會統一轉小寫做驗證,如果要做更嚴格的驗證,就不須加上轉小寫。
### validating a form level 表單層級驗證
#### useForm()
- 是初始化「整份表單狀態」的函式
- `validation schema(驗證規則)`:一個統一的結構(通常是一個物件)來定義整個表單的驗證規則。因為用`useField()`只能一個一個欄位設定驗證,而 schema 則可以一次定義表單中每個欄位的驗證規則。
範例:
```javascript!
validationSchema: yup.object({
email: yup.string().email().required(),
password: yup.string().min(6).required()
})
```
以這個例子 validation schema 驗證規則為
- email 是 email 格式、必填
- password 至少 6 個字、必填
需要注意的地方:
`useField("email")`和 validations schema 的 `email 屬性`名稱要一致!其他也是以此類推

return true 代表驗證通過
[vee-validate:useForm](https://vee-validate.logaretm.com/v4/api/use-form/)
[vee-validate:Typed Schemas](https://vee-validate.logaretm.com/v4/guide/composition-api/typed-schema#yup)
### submitting bigger forms
```javascript!
password: value => {
const requiredMessage = 'This field is required'
if (value === undefined || value === null) return requiredMessage // 驗證失敗,顯示錯誤訊息
if (!String(value).length) return requiredMessage // 驗證失敗,顯示錯誤訊息
return true // 驗證成功
}
```
處理各種「空」的情況:
因為 `.length` 無法用在 `null` 或 `undefined`,所以先判斷 `value === undefined || value === null` 是為了**避免錯誤發生**
:star: `useForm({ validationSchema })` validationSchema 本身設定就是個物件,為什麼外層還要有`{}`?
因為 useForm 本身會包含多個屬性選項,validationSchema 只是其中一個,如下方設計:
```javascript!
useForm({
initialValues: { ... },
validationSchema: { ... },
validateOnBlur: true,
...其他選項
});
```
:star: undefined是什麼?
```javascript!
const { value: pets, errorMessage: petsError } = useField("pets", undefined, {
initialValue: 1,
});
```
因為 useField 第一個參數是欄位
第二個參數 `validate`,`undefined`意思是 不需要驗證規則
第三個參數是可選擇性
如果省略第二個參數直接寫`{ initialValue: 1 }`,會誤以為是第二個參數而報錯。
#### Composable API - handleSubmit()
```!
handleSubmit: (cb: SubmissionHandler) => (evt?: Event) => Promise<void>
```
處理表單送出
- callback是「驗證成功」後要做的事情
- 這個callback 回傳的是一個 promise
#### Composable API - errors
把欄位名稱對應的的錯誤訊息包成一個物件
```javascript!
const { errors } = useForm();
```
例如:
category 和 title 是必填,當沒填寫就送出表單,跳出錯誤訊息


原想改寫把 useField 的 value 改成從 useForm 解構取得 valuse 來綁定`BaseSelect`等元件的v-model,結果失敗 :cry:
查詢發現 `useForm().values` 是 ==readonly的 reactive proxy==,也就是只能讀,不能雙向綁定(v-model不行,會出現 readonly 的錯誤)

:100: 改寫成這樣就可以了:100:
```javascript!
// 不要用 v-model
<BaseInput
:modelValue="values.title"
@update:modelValue="(value) => setFieldValue('title', value)"
label="Title"
:error="errors.title"
type="text"
/>
```
```javascript!
// ComponentsForm.vue
<script setup>
import { ref } from "vue";
import { useField, useForm } from "vee-validate";
const categories = ref([
"sustainability",
"nature",
"animal welfare",
"housing",
"education",
"food",
"community",
]);
const required = (value) => {
const requiredMessage = "This field is required";
if (value === undefined || value === null) return requiredMessage;
if (!String(value).length) return requiredMessage;
return true;
};
const minLength = (number, value) => {
if (String(value).length < number)
return "Please type at least " + number + " characters";
return true;
};
const anything = () => {
return true;
};
const validationSchema = {
category: required,
title: (value) => {
const req = required(value);
if (req !== true) return req;
const min = minLength(3, value);
if (min !== true) return min;
return true;
},
description: anything,
location: undefined,
pets: anything,
catering: anything,
music: anything,
};
const { handleSubmit, errors } = useForm({
validationSchema,
initialValues: {
pets: 0,
catering: false,
music: false,
},
});
const onSubmit = handleSubmit(
(values) => {
console.log("驗證通過", values);
},
(error) => {
console.log("驗證失敗", error);
},
);
const { value: category } = useField("category");
const { value: title } = useField("title");
const { value: description } = useField("description");
const { value: location } = useField("location");
const { value: pets } = useField("pets");
const { value: catering } = useField("catering");
const { value: music } = useField("music");
</script>
```
### using YUP for validations
vee-validate官網推薦使用第三方Yup函式庫,屬於 schema 驗證庫
基本安裝
```
npm install @vee-validate/yup
```
範例:
```javascript!
import * as yup from 'yup';
// 設定規則
const schema = yup.object({
// 欄位是一個字串、格式是 email、欄位必填
email: yup.string().email().required(),
// 欄位是一個字串、最少要 8 個字元、欄位必填
password: yup.string().min(8).required()
});
schema.validate({
email: 'user@example.com', // 使用者實際輸入
password: '12345678' // 使用者實際輸入
}).then(validData => {
console.log('驗證成功:', validData);
}).catch(err => {
console.log('驗證錯誤:', err.errors);
});
```
* `yup.object()`:建立整體資料的schema。範例中裡面包含email 和 password 兩個欄位,代表有各自的驗證條件。
* `yup.required()`:括號裡可以設置自訂訊息。例如:`yup.required("the title field is required")`
* Yup 的 `.validate()` 是一個回傳 Promise 的非同步函式。例如:email 要連到後端確認 email 是否被註冊過
**tree shaking: 保留確定會用到的程式**
```javascript!
// 原本寫法,引入所有 yup 的 package
// 會影響打包程式的大小
import * as yup from "yup"
// 修改,只留下有使用到的
import { object, stirng, number, boolean } from "yup"
```
[github文件 - yup](https://github.com/jquense/yup?tab=readme-ov-file#schemavalidatevalue-any-options-object-promiseinfertypeschema-validationerror)
### lazy validation
#### Composable API - handleChange
原本 input 事件會每次輸入或刪除就會觸發,如果想要讓使用者體驗變好
1. 將 v-model 改成 屬性是 modelValue 和 change事件
2. 提取 useField 的 `handleChange`,更新欄位值、驗證欄位
```javascript!
<template>
<form @submit.prevent="onSubmit">
<BaseInput
label="Email"
type="email"
:error="emailError"
:modelValue="email"
@change="handleChange"
/>
<form />
</template>
<script setup>
const {
value: email,
errorMessage: emailError,
handleChange,
} = useField("email");
</script>
```
那如果好幾個欄位都要使用`handleChange`,可以提取 useForm 的 `setFieldValue` 方法來設定
```javascript!
const { setFiedValue } = useForm({
validationSchema: validations,
});
const handleChange = (event) => {
setFiedValue("email", event.target.value);
};
// useField就不用提取handleChange
```