Try   HackMD

F_Vue3筆記

intro to vue3

  • git clone下來,在本地端建立分支並連線作者的遠端分支
git checkout -b L2 origin/L2-start
  • vscode安裝extension es6-string-html

creating the vue app

課程用的是CDN,官網有提供

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的結構:

<template>
    <div>
        <!--HTML-->
    </div>
</template>

<script>
    // JavaScript
</script>

<style>
    /*CSS*/
</style>

一個 .vue 檔中:

  • <template> 只能有一個
  • <script> 只能有一個 (不包含<script setup>)
  • <script setup> 只能有一個
  • <style> 可以多個

參考資料


影片提到:

  • {{}}:moustache syntax
    • 裡面可寫 JS 表達式
    • 可寫字串
    • 可寫三元運算
<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

  1. v-bind 的縮寫為 :
<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"
<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
<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
<p v-show="false">In Stock</p>

<!-- 實際渲染是隱藏它 -->
<p style="display:none">In Stock</p>

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Day 8: 認識 Vue directive 和 v-if v.s v-show
vue doc - template syntax
vue doc - Built-in Directives
vue doc - Conditional Rendering

v-for

可接受的value類型有:數字、字串、陣列(包含Map/Set)、物件

數字

<div id="app">
 <ul>
     <li v-for="number in 5">{{number}}</li>
 </ul>
</div>

從1開始,數字須為正整數

image

字串

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

image

陣列

<ul>
  <li v-for="item in variants" :key="item.id">{{item.color}}</li>
</ul>

可以使用解構

<ul>
 <li v-for="({id,color},index) in variants" :key="id">{{index}} : {{color}}</li>
</ul>
// 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" },
          ],
        }
    }
})

物件

可以拿到物件的 valuekeyindex

  <ul>
    <li v-for="(value,key,index) in customersData">{{key}}: {{value}}</li>
  </ul>

key attribute

  • vue用來識別和追蹤元素,不會顯示在html上
  • 建議綁定key屬性,並使用唯一的ID,讓vue能夠正確追蹤

補充

v-ifv-for不能放在同一個元素上,v-if的優先權高於v-for,會報錯

<li v-for="todo in todos" v-if="!todo.isComplete">
  {{ todo.name }}
</li>

Day 9: v-for 與他的坑 feat. key & v-if
Vue doc- special attribute

v-on

  1. 縮寫為@
v-on:click="handler" 

// 縮寫
@click="handler"

click:監聽的事件
handler:觸發的處理器,有 inline handlermethod handlerCalling Methods in Inline Handlers

<-- inline handler 直接寫表達式-->
<div>
    <button @click="count+=1">
    <p>{{count}}</p>
</div>

<-- method handler -->
<div>
    <button @click="addToCart">
    <p>{{count}}</p>
</div>
const count = ref(0);

function addToCart(){
    count.value += 1;
}
  1. Event Modifiers 事件修飾符
  • .stop
  • .prevent
  • .self
  • .capture
  • .once
  • .passive

範例:

<form @submit.prevent = "onSubmit"></form>
  1. Key Modifiers 按鍵修飾符
<!-- 只有在 key 為 enter 時調用 submit -->
<input @keyup.enter="submit" />

Vue Doc:Event Handling

class & style bindings

<!-- css name為駝峰 -->
<div :style="{backgroundColor:blue}">
    blue
</div>

<!-- css name為烤肉串,須多加引號才不會報錯 -->
<div :style="{'background-color':blue}">
    blue
</div>

渲染出來長這樣

<div style="background-color:blue">
    blue
</div>

如果style比較多,可以包裝成物件傳入:

<div :style="styles"></div>
const styles = ref({fontSize:"18px",fontWeight:"bold"})

當out of stock時,add button按鈕要套反灰設計&不能增加cart數量:

<!-- 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>
.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. 不會直接修改任何數據,只是讀取並計算
// 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,
    };
  },
});
// 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 (自訂屬性將資料傳遞給元件)

範例:

// 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>內容

// PropsComponent.vue
<script setup>
const props = defineProps(['newTitle']) // 接收父元件上的newTitle
</script>

<template>
 <p>{{newTitle}}</p>
</template>

如果沒有使用<script setup>,寫法改成這樣:

<script>
export default {
  props: ['newTitle'],
  setup(props) {
    console.log(props.title)
  }
}
</script>

vue doc-Components Basics

communicating Events

emitting Events : tell "parent" when events happens,意思是子元件執行父元件的方法用 emit

子元件的按鈕點擊時,觸發父元件的事件處理:

// index.html
<product-display
    :premium="premium"
    @add-to-cart="updateCart"
  ></product-display>
// main.js

const app = Vue.createApp({
  data() {
    return {
      cart: [],
      premium: true,
    };
  },
  methods: {
    updateCart(id) {
      this.cart.push(id);
    },
  },
});
// 子元件 ProductDetails.js
methods: {
    addToCart() {
      this.$emit("add-to-cart", this.variants[this.selectedVariant].id);
    },
}

Forms & v-model

payload : 具有意義、有效的資料
v-model : 雙向綁定,可添加modifier修飾符

  • lazy : 每次change事件後才更新數據,預設的話是每次input事件就更新數據
<input v-model.lazy="message" />
  • number : 輸入自動轉換為數字
<input v-model.number="age" />

如果 <input type="number">v-model 會自動轉數字,不用 .number

  • trim : 自動去除輸入內容中兩端的空格
<input v-model.trim="message" />

vue doc:Form Input Bindings

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

<style scoped></style>

scoped : applies styles to component only 僅將樣式套用至元件

定義一個vue元件,並指定元件的名稱:

// 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>
// 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

vue router essential

SPA (Single-Page Application): 整個應用程式所有內容都在一個頁面中呈現(one index.html page)

// 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-linkrouter-view

<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官網

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

lifecycle hooks

元件具有生命週期,並在生命週期中呼叫不同的鉤子(hook)或方法
axios 是基於 promise 且非同步

如果每個元件都有axios instance,API 程式碼會遍佈整個應用程式,這樣會很混亂且難debug

所以,在應用程式內有一個專門的位置,來處理 api 行為,在 src 建立一個 service folder ,並建立一個 new file 為 EventService.js,把 axios 包裝在這裡,要使用再各別 import 進去

// 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 ❓

// 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 } } "

// 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)

// 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

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

    image

  • 完成後會出現一個 dist 資料夾,裡面有 html、css、js

Render 是自動部署靜態網站,不需要把 dist/ push 到 GitHub,須確定src/package.json 等原始碼 push 上去

image

遇到 deploy 報錯,原因是 node 版本的 openSSL 跟 webpack 不相容,但改了 node 版本為v16還是一樣

解法:在package.json加入 engines 欄位,Render就會跑正確版本

"engines": {
  "node": "16"
}

重新部署成功了!

image

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

image

解決方法:

  1. 先到 Render 的 redirects/rewrites 設定

    image

  2. 再到 router 加上 not found的路由component

// 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的一部分:
// 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 寫法

// 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 傳遞出去,元件再接收
  • 元件更容易測試與重用
  1. configure a component from the router
    使用Props Object Mode
// 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>
  1. transform query parameters
    使用Props function Mode,函式可以做更複雜的轉換
    網址為 http://localhost:8080/?e=true
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

例如有一個網址:/events?_limit=2&_page=3
_limit:每頁多少項目
_page:正在哪一頁

  1. 修改 EventService API 並帶入 perPage 和 page 參數
getEvents(perPage, page) {
    return apiClient.get(`/events?_limit=${perPage}&_page=${page}`)
  },
  1. 在 router 使用 props function mode 設置當前頁
    假如 query parameter 存在且是整數就取得,否則預設為1
  {
    path: '/',
    name: 'EventList',
    component: EventList,
    props: route => ({ page: parseInt(route.query.page) || 1 })
  },

parseInt(route.query.page):因為 route.query.page 拿到是字串,須轉換成數字
1:表示預設為第一頁

用函式寫法是因為「要動態計算傳資料給props」。

  1. 在 EventList.vue 呼叫 api 的地方修改參數
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'])原本是寫成

props: {
  page: {
    type: Any
  }
}

props 是 reactive proxy,而computed會把props.page變成 ref

也就是 props 有包含 page 屬性,就可以 props.page 取值,因為需要響應式變數來追蹤變化,透過 computed 包住props.page,才會自動更新。

  1. 在EventList.vue增加分頁連結
<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更新

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 :

onMounted(() => {
  watch(page,(newPage) => {  
    events.value = null
    EventService.getEvents(2, newPage)
      .then(response => {
        events.value = response.data
      })
      .catch(error => {
        console.log(error)
      })
  }), { immediate: true })
})
  1. 計算頁數
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-countHTTP 回應標頭(header),常用在 分頁(pagination)API,也就是資料庫中符合條件的資料總筆數

從資料庫拿到總筆數後,就能計算總共會有幾頁:

const hasNextPage = computed(() => {
  let totalPages = Math.ceil(totalEvents.value / 2)
  return page.value < totalPages // 代表不是最後一頁
})

最後排版一下prev page和 next page

image

nested routes

Lazy Loading Routes

// 原本 import UserDetails from './views/UserDetails'
// 改成
const UserDetails = () => import('./views/UserDetails.vue')

Vue doc: Lazy Loading Routes

建立一個 layout 元件給event底下所有的子元件,layout包含navigation和fetch events,這樣就不用每個子元件都寫。

{
    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 參數

// 把 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

Redirect & Alias

舊的 route 重新導向至新的 route

  1. 如何改網址從 /about/about-us?
  • 方法一:使用 redirect
 {
    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搜尋就不建議
  {
    path: '/about-us',
    name: 'About',
    component: () => import('@/views/About.vue')
    alias: '/about'
  },

可以把 alias 想成這個路由有小名叫做 /about

  1. 如何改網址從 /event/:id/events/:id?
 {
    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 物件:

image

要改的路由本身底下如有嵌套路由的話:

{
    path: '/event/:afterEvent(.*)', // afterEvent自定義參數
    redirect: to => {
      return { path: '/events/' + to.params.afterEvent }
    }
},

.*: 正則表達式寫法,表示任何內容、任何長度(包括空字串)
也就是 :afterEvent(.*)匹配 /event/ 後面「所有的東西」

vue doc:路徑參數正則表達式
Vue doc: 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

用官網的範例

// 字串路徑
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
router.push('/post/123')
  • router.replace:導入後不希望返回導航
    範例:使用者登入後自動跳轉到dashboard,不希望使用者按返回回到登入頁。
    瀏覽器歷史紀錄:/dashboard(取代掉 /login
router.replace('dashboard')

也可以在 router.push 增加屬性 replace:true

router.push({ path: '/home', replace: true })   

// 等同於   
router.replace({ path: '/home' })

補充:
router.go(1):向前一條紀錄,等同router.forward()
router.go(-1):向後一條紀錄,等同router.back()
等同於瀏覽器的前進 / 返回按鈕!

vue doc: Programmatic Navigation
vue doc: Vue Router and the Composition API
MDN: History API

error handling and 404s

假如使用者輸入一個不存在的路由,設計頁面導向 NotFound.vue 元件。通常用來處理 404 頁面,匹配所有未被其他路由捕捉的網址

// router/index.js
  {
    path: '/:catchAll(.*)',
    name: 'NotFound',
    component: () => import('@/views/NotFound.vue')
  }

catchAll:是自訂參數,是習慣上的命名因為有語意,也可以取path: '/:notFound(.*)'之類

不論是頁面的 404 還是 event 的 404 ,都可以重用 NotFound.vue 元件!

  {
    path: '/404/:resource',
    name: '404Resource',
    component: () => import('@/views/NotFound.vue'),
    props: true
  }
// 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屬性

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

image

範例: call api 的 EventService.js 假設 baseUrl 寫錯

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 元件

<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

  {
    path: '/network-error',
    name: 'NetworkError',
    component: () => import('@/views/NetworkError.vue')
  }
// 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

// 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

// 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搭配一起使用。

// 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}

// 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 路徑

範例:

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 不用 returnbeforeRouteUpdate 就要?
因為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 路由元信息

lazy loading routes 路由懶加載

將路由對應的元件分割,當路由被訪問才加載。

{
    path: 'register',
    name: 'EventRegister',
    component: () => import('@/views/event/Register.vue') // 不載入程式碼除非有請求
  }

vue doc: lazy loading routes

scroll behavior 滾動行為

當進入頁面,瀏覽器預設行為會帶使用者到頁面最頂端。
例如:google 搜尋引擎,當滑到底部有分頁,點選第二頁時候,會到第二頁的頂端

但影片一開始的範例點選 next page,仍停留在底部,需要改善使用者介面,所以在 router 加上 scrollBehavior

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes,
    // 滾動到頂端
  scrollBehavior() {
    return { top: 0 }
  }
})

按返回前一頁,要帶回到原本頁面的原本的位置:

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

vue3 Forms

basic input

盡可能讓元件保持彈性和複用設計

<pre></pre>可以直接在模板中看資料

:face_holding_back_tears: 為什麼 input 沒有吃到樣式? :face_holding_back_tears:

image

因為 multi-root component (多個根元素) 無法自動繼承 attribute(例如 type="text"),需要手動把 $attrs 綁到指定的元素例如 <input>,這樣父層傳入的屬性才能正確應用。
如果 single-root component(一個根元素),Vue 會自動把這些 attribute 加到 root 上。

解法:手動注入 attribute ,綁定$attrs 物件

// 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>        
// SimpleForm.vue 
<BaseInput label="Title" v-model="event.title" type="text" />

image

BaseInput元件就能吃到父層給的type="text"

doc: $attrs

其他範例:

<BaseInput label="Name" v-model="name" type="text" maxlength="20" />

父層傳來但沒有對應 prop 的所有屬性,例如typemaxlength沒有在 defineProps 中定義,就會被自動轉為 $attrs
v-bind="$attrs" 就能讓它們應用到 <input>

@input="$emit('update:modelValue', $event.target.value)"
  1. $emit('update:modelValue', $event.target.value) 發送了一個事件:
  • 事件名稱:update:modelValue
  • 傳遞的值:$event.target.value(即使用者輸入的字串)
  1. $event.target.value 將input 元素的輸入事件的 value作為參數傳遞給父層
// SimpleForm.vue
  <BaseInput
    label="Title"
    type="text"
    :modelValue="event.title"
    @update:modelValue="($event) => (event.title = $event)"
  />

所以 @update:modelValue$event 接收到子元件的 value,並賦值並更新 event.title

<BaseInput
  :modelValue="name"
  @update:modelValue="($event) => name = $event"
// $event參數 就是 $event.target.value傳來的值
/>

補充:Component v-model

<BaseInput label="Title" v-model="event.title" type="text" />

等於下面寫法:

  <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(例如 idclassname 等)
  • 但又要控制自己的 onChange 行為
  • 如果你在 $attrs 裡有傳入 onChange,這邊就會覆蓋它
// SimpleForm.vue 
  <BaseSelect
    v-model="event.category"
    label="Select a category"
    :options="categories"
  />
// 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

// 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>
// SimpleForm.vue
<BaseCheckbox
  label="Live music"
  v-model="event.extras.music"
  type="checkbox"
/>

BaseRadio

<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>
// 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

// 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>
// SimpleForm.vue
  <div>
    <BaseRadioGroup
      :modelValue="event.pets"
      @update:modelValue="($event) => (event.pets = $event)"
      name="pets"
      :options="petOptions"
    />
  </div>

image
image

在排版上,如果 radio group 要水平排版,需要用 <span> 包 ; 如果要垂直排版,需要用 <div> 包。這時候可以使用 <component> 動態渲染其他組件HTML 元素

<component :is />

:is 屬性:決定渲染 哪個元件 或 標籤

範例:

<component :is="href ? 'a' : 'span'"></component>

意思是 href 為 true,則 <component> 會變成 <a></a> ,反之 <component> 會變成 <span></span>

:star: 為什麼把 v-for:key 放在 <component> 上?
因為要把每個 radio 的外層包容器元素

// v-for寫在component上
<div>
  <BaseRadio />
</div>
<div>
  <BaseRadio />
</div>

// v-for寫在BaseRadio上
<BaseRadio />
<BaseRadio />

doc: 動態組件
doc: <component>
doc: 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

到開發工具的network頁面可以觀察 fetch/XHR,Header可以看到為成功請求

image

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

image

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

image

basic a11y for our component

<fieldset>將一組相關的表單欄位包在一起的容器,螢幕閱讀器會解釋欄位間的關係。

<legend><legend> 必須放在 <fieldset> 裡面,它提供這一組欄位的說明標題。這段文字會被螢幕閱讀器唸出來。

placeholder不能取代說明的標籤(label)

// 最快的解決方式,確保input可以連接到label
<label>
    Title
    <input>
</label>

通用唯一辨識碼(Universally Unique Identifier,縮寫:UUID

aria-describedby:提供補充說明,不取代label

aria-live:某段內容變動時,自動唸出來通知使用者。例如:送出成功、3秒後自動跳轉等

aria-live="polite" // 有空再唸(不打斷使用者)
aria-live="assertive"  // 立即打斷並唸出內容

aria-invalid:這個表單欄位目前是無效的(驗證失敗),但不會執行表單驗證!

<input type="email" id="email" aria-invalid="true">

submit button不要用disable,因為輔助科技不會取得到回饋而被忽略

MDN: accessibility( a11y 無障礙 )

validating vue3 Forms

setting up

安裝vee-validate

npm i vee-validate --save

useField()

<script setup>
import { useField } from 'vee-validate';

const { value, errorMessage } = useField('name', inputValue => !!inputValue);
</script>

因為 useField() 是回傳一個物件,可以用解構的方式取得物件裡的屬性,能使用的屬性很多,詳細看 vee-validate:useField

第一個屬性 value:是Vue ref,代表欄位的值
第二個屬性 errorMessage:是一個 ref,當驗證失敗時,它會包含錯誤訊息,否則為空字串

image

image

:star: !regex.test(String(value).toLowerCase()為什麼轉成小寫?
Email 使用者名稱(@ 前):技術上是區分大小寫的。
Email 網域名稱(@ 後):一律 不區分大小寫
開發上會統一轉小寫做驗證,如果要做更嚴格的驗證,就不須加上轉小寫。

validating a form level 表單層級驗證

useForm()

  • 是初始化「整份表單狀態」的函式
  • validation schema(驗證規則):一個統一的結構(通常是一個物件)來定義整個表單的驗證規則。因為用useField()只能一個一個欄位設定驗證,而 schema 則可以一次定義表單中每個欄位的驗證規則。

範例:

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 屬性名稱要一致!其他也是以此類推

image

return true 代表驗證通過

vee-validate:useForm
vee-validate:Typed Schemas

submitting bigger forms

password: value => {
    const requiredMessage = 'This field is required'
    if (value === undefined || value === null) return requiredMessage // 驗證失敗,顯示錯誤訊息
    if (!String(value).length) return requiredMessage // 驗證失敗,顯示錯誤訊息

    return true // 驗證成功
}

處理各種「空」的情況:
因為 .length 無法用在 nullundefined,所以先判斷 value === undefined || value === null 是為了避免錯誤發生

:star: useForm({ validationSchema }) validationSchema 本身設定就是個物件,為什麼外層還要有{}?
因為 useForm 本身會包含多個屬性選項,validationSchema 只是其中一個,如下方設計:

useForm({
  initialValues: { ... },
  validationSchema: { ... },
  validateOnBlur: true,
  ...其他選項
});

:star: undefined是什麼?

const { value: pets, errorMessage: petsError } = useField("pets", undefined, {
  initialValue: 1,
});

因為 useField 第一個參數是欄位
第二個參數 validateundefined意思是 不需要驗證規則
第三個參數是可選擇性
如果省略第二個參數直接寫{ initialValue: 1 },會誤以為是第二個參數而報錯。

Composable API - handleSubmit()

handleSubmit: (cb: SubmissionHandler) => (evt?: Event) => Promise<void>

處理表單送出

  • callback是「驗證成功」後要做的事情
  • 這個callback 回傳的是一個 promise

Composable API - errors

把欄位名稱對應的的錯誤訊息包成一個物件

const { errors } = useForm();

例如:
category 和 title 是必填,當沒填寫就送出表單,跳出錯誤訊息

image

image

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

image

:100: 改寫成這樣就可以了:100:

// 不要用 v-model

  <BaseInput
    :modelValue="values.title"
    @update:modelValue="(value) => setFieldValue('title', value)"
    label="Title"
    :error="errors.title"
    type="text"
  />
// 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

範例:

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: 保留確定會用到的程式

// 原本寫法,引入所有 yup 的 package
// 會影響打包程式的大小
import * as yup from "yup" 

// 修改,只留下有使用到的
import { object, stirng, number, boolean } from "yup" 

github文件 - yup

lazy validation

Composable API - handleChange

原本 input 事件會每次輸入或刪除就會觸發,如果想要讓使用者體驗變好

  1. 將 v-model 改成 屬性是 modelValue 和 change事件
  2. 提取 useField 的 handleChange,更新欄位值、驗證欄位
<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 方法來設定

const { setFiedValue } = useForm({
  validationSchema: validations,
});

const handleChange = (event) => {
  setFiedValue("email", event.target.value);
};

// useField就不用提取handleChange