進階 API
進階章節說明
操作 DOM 元素技巧 refs
// HTML
<div id="app">
<h3>使用 ref 定義元素</h3>
<input type="text" ref="inputDom" />
<h3>使用 ref 取得元件內的資訊</h3>
<button type="button" @click="getComponentInfo">取得元件資訊</button>
<card ref="card"></card>
<h3>進階,使用 ref 搭配 Bootstrap</h3>
Bootstrap Modal:
<a href="https://getbootstrap.com/docs/5.0/components/modal/"
>https://getbootstrap.com/docs/5.0/components/modal/</a
>
<button @click="openModal">開啟 Bootstrap Modal</button>
<!-- 2. 定義 ref modal 屬性 -->
<div class="modal" tabindex="-1" ref="modal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Modal title</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<p>Modal body text goes here.</p>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
>
Close
</button>
<button type="button" class="btn btn-primary">Save changes</button>
</div>
</div>
</div>
</div>
</div>
// JS
<!-- 另外載入 Bootstrap CDN -->
<script>
const app = Vue.createApp({
data() {
return {
// 1. 先定義一個 bsModal 變數
bsModal: "",
};
},
methods: {
getComponentInfo() {
console.log(this.$refs.card);
this.$refs.card.title = "被更新的元件標題";
},
openModal() {
// 4. 加上 show() 把 bootstrap Modal 打開
this.bsModal.show();
},
},
mounted() {
console.log(this.$refs, this.$refs.inputDom);
this.$refs.inputDom.focus();
// 3. 使用 this.bsModal 建立一個 new bootstrap Modal 實體
// 3-1. this.refs 指到我們在畫面上這個的元素
// 3-2. new bootstrap.Modal 實體會存在 this.bsModal
// 3-3. this.bsModal 可以在不同作用域取用
this.bsModal = new bootstrap.Modal(this.$refs.modal);
},
});
app
.component("card", {
data() {
return {
title: "文件標題",
content: "文件內文",
};
},
template: `
<div class="card" style="width: 18rem;">
<div class="card-body">
<h5 class="card-title">{{ title }}</h5>
<p class="card-text">{{ content }}</p>
</div>
</div>
`,
})
.mount("#app");
</script>
自訂元件生成位置 teleport
// HTML
<div id="app">
<div id="target"></div>
<h3>Teleport 自訂義元件位置</h3>
結構:<code><teleport to="{ target }"></code>
<card></card>
<h3>使用限制(錯誤情境)</h3>
<card2></card2>
<h3>實用技巧(取代標題、多個)</h3>
<new-title></new-title>
</div>
// JS
<script>
const app = Vue.createApp({});
app.component("card", {
data() {
return {
title: "文件標題",
content: "文件內文",
toggle: false,
};
},
template: `
<div class="card" style="width: 18rem;">
<div class="card-body">
<h5 class="card-title">{{ title }}</h5>
<p class="card-text">{{ content }}</p>
<button type="button" @click="toggle = !toggle">切換元素顯示</button>
</div>
</div>
<teleport to="#target">
<div v-if="toggle" class="alert alert-danger">被招喚的元素</div>
</teleport>
`,
props: ["item"],
});
app.component("card2", {
template: `
<teleport to=".col-md-3">
<div class="alert alert-danger">被招喚的元素</div>
</teleport>
`,
});
app.component("new-title", {
template: `
<teleport to="title"> - 新增的標題片段</teleport>
<teleport to="h1"> - 新增的文字片段</teleport>
`,
});
app.mount("#app");
</script>
跨層級資料傳遞 provide
// HTML
<div id="app">
<h2>多層級資訊傳遞</h2>
<ul>
<li>在外層加入 provide</li>
<li>內層元件補上 inject</li>
</ul>
<card></card>
{{ user.name }}
<h3>注意事項:響應式</h3>
</div>
// JS
<script>
// 最內層
const userComponent = {
template: `
<div>
userComponent 元件:<br />
{{ user.name }}, <br />
{{ user.uuid }}
</div>
`,
// 2. 內層元件補上 inject
// 2-1. inject 是一個陣列
// 2-2. inject 內容加上 user 這個名稱
inject: ["user"],
created() {
console.log(this.user);
// 如果根元件沒有使用 function return 則不能使用響應式
this.user.name = "杰倫";
},
};
// 最外層
const app = Vue.createApp({
data() {
return {
user: {
name: "小明",
uuid: 78163,
},
};
},
// 1. 在外層加入 provide
// 1-1 函式、物件運作上有點不一樣
// provide: {
// user: {
// name: "小明",
// uuid: 78163,
// },
// },
// 注意事項: 響應式
// provide 物件改成函式
// 實戰建議直接使用函式的結構
provide() {
return {
user: this.user,
};
},
});
// 內層
app.component("card", {
data() {
return {
title: "文件標題",
content: "文件內文",
toggle: false,
};
},
components: {
userComponent,
},
inject: ["user"],
template: `
<div class="card" style="width: 18rem;">
<div className="card-header">card 元件</div>
<div class="card-body">
{{ user.name }}
<userComponent></userComponent>
</div>
</div>
`,
});
app.mount("#app");
</script>
元件直接加入 v-model
// HTML
<div id="app">
<h2>將元件內的值傳回 v-model(update:modelValue)</h2>
<p>
參考說明:<a
href="https://vue3js.cn/docs/zh/guide/component-custom-events.html#v-model-%E5%8F%82%E6%95%B0"
>https://vue3js.cn/docs/zh/guide/component-custom-events.html#v-model-%E5%8F%82%E6%95%B0</a
>
</p>
{{ name }}
<!-- 1. v-model:text="name" -->
<custom-input v-model:text="name"></custom-input>
<hr />
<h2>多個 v-model</h2>
{{ text }} {{ text2 }}
<!-- 1. v-model:t1="text"、v-model:t2="text2 -->
<custom-input2 v-model:t1="text" v-model:t2="text2"></custom-input2>
</div>
// JS
<script>
const app = Vue.createApp({
data() {
return {
name: "小明",
text: "這是文字片段 1",
text2: "這是文字片段 2",
};
},
});
// $emit('update:text', $event.target.value) 搭配 props,可以將更新後的值寫回 v-model 內
app.component("custom-input", {
props: ["text"],
// 2-1. :value="text"
// 2-2. @input="$emit('update:text', $event.target.value)"
template: `
<input type="text"
:value="text"
@input="$emit('update:text', $event.target.value)"
class="form-control">
`,
});
app.component("custom-input2", {
props: ["t1", "t2"],
// 2-1 :value="t1"、:value="t2"
// 2-2 @input="$emit('update:t1', $event.target.value)"
// @input="$emit('update:t2', $event.target.value)"
template: `
<input type="text"
:value="t1"
@input="$emit('update:t1', $event.target.value)"
class="form-control">
<input type="text"
:value="t2"
@input="$emit('update:t2', $event.target.value)"
class="form-control">
`,
});
app.mount("#app");
</script>
混合元件方法 mixins
// HTML
<div id="app">
<h2>單一混合、多個混合</h2>
<card></card>
<p>重點:</p>
<ul>
<li>可以重複混合</li>
<li>生命週期可以重複觸發</li>
<li>同名的變數、方法則會被後者覆蓋</li>
</ul>
</div>
// JS
<script>
const mixComponent1 = {
data() {
return {
name: "混合的元件",
};
},
created() {
console.log("混合的元件生命週期");
},
};
const mixComponent2 = {
data() {
return {
name: "混合的元件 2",
};
},
created() {
console.log("混合的元件生命週期 2");
},
};
const app = Vue.createApp({});
app.component("card", {
// 優先順序
// mixins 的優先順序是比較低的
// 元件本身的優先順序會是比較高的
data() {
return {
name: "card",
};
},
template: `
<div class="card">
<div class="card-body">{{ name }}</div>
</div>
`,
// 1-1. mixins 是一個陣列
// 1-2. 陣列內容把其他元件的內容混合進來
mixins: [mixComponent1, mixComponent2],
created() {
console.log("card 的元件生命週期");
},
});
app.mount("#app");
</script>
擴展元件方法 extend
// HTML
<div id="app">
<h2>單一擴展</h2>
<!-- <card></card> -->
<h3>權重</h3>
<card2></card2>
<h3>重點:</h3>
<ul>
<li>擴展為單一擴展</li>
<li>生命週期可以與 mixins 重複觸發</li>
<li>權重:元件屬性 > mixins > extend</li>
<li>同名的變數、方法則會依據權重決定</li>
</ul>
</div>
// JS
<script>
const extendComponent1 = {
data() {
return {
name: "擴展的元件",
};
},
created() {
console.log("擴展的元件生命週期");
},
};
const mixinComponent = {
data() {
return {
name: "混合的元件",
};
},
created() {
console.log("混合的元件生命週期");
},
};
const app = Vue.createApp({});
app.component("card", {
template: `
<div class="card">
<div class="card-body">{{ name }}</div>
</div>
`,
// extends 後面直接加入一個物件,這物件是元件的本身
extends: extendComponent1,
created() {
console.log("card 的元件生命週期");
},
});
app.component("card2", {
template: `
<div class="card">
<div class="card-body">{{ name }}</div>
</div>
`,
data() {
return {
name: "元件資料狀態",
};
},
mixins: [mixinComponent],
extends: extendComponent1,
created() {
console.log("card 的元件生命週期");
},
});
app.mount("#app");
</script>
自定義指令 directive
// HTML
<div id="app">
<h2>自訂義指令</h2>
<h3>用途</h3>
<ul>
<li>實戰中屬於進階功能,初學可先了解有此功能即可</li>
<li>可從延伸套件中看到相關的運用</li>
<li>多用於 HTML 上的便利操作,複雜功能還是會搭配元件</li>
</ul>
<h3>結構</h3>
<ul>
<li>v-{自訂義名稱}</li>
<li>
主要透過生命週期來觸發變動,可參考:<a
href="https://vue3js.cn/docs/zh/guide/custom-directive.html#%E5%8A%A8%E6%80%81%E6%8C%87%E4%BB%A4%E5%8F%82%E6%95%B0"
>https://vue3js.cn/docs/zh/guide/custom-directive.html#%E5%8A%A8%E6%80%81%E6%8C%87%E4%BB%A4%E5%8F%82%E6%95%B0</a
>
</li>
</ul>
<!-- 1. v-validator -->
<input type="email" v-model="text" v-validator="'form-control'" />
</div>
// JS
<script type="module">
const app = Vue.createApp({
data() {
return {
text: "",
};
},
});
// 註冊指令
app.directive("validator", {
// directive 生命週期
mounted(el, binding) {
el.focus();
// 將外部的值改為
console.log(binding);
el.className = binding.value;
},
updated: function (el, binding, vnode) {
// el 元素本體
// binding 綁定的資源狀態
// vnode 虛擬 DOM 節點
console.log("update", el, binding, vnode);
const className = binding.value;
// 尋找當前的 model 名稱(取得 key 值,並帶入第一個)
const currentModel = Object.keys(binding.instance)[0];
// console.log(currentModel);
// 從當前 Model 取值
const value = binding.instance[currentModel];
console.log(currentModel, value);
// Email validate
// 正規表達式
const re =
/^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i;
if (!re.test(value)) {
el.className = `${className} is-invalid`;
} else {
el.className = `${className} is-valid`;
}
},
});
app.mount("#app");
</script>
<script>
// ESM 版本的差異(需要 Webpack)
// import { Field, Form, ErrorMessage } from 'vee-validate';
//
// export default {
// components: {
// Field,
// Form,
// ErrorMessage,
// },
// methods: {
// isRequired(value) {
// if (value && value.trim()) {
// return true;
// }
//
// return 'This is required';
// },
// },
// };
</script>
擴充插件 plugins
// HTML
<div id="app">
<p>外部套件匯入方式</p>
<ul>
<li>載入方式:使用 CDN 或使用 npm</li>
<li>
運用方式:<a href="https://www.npmjs.com/package/vue-axios">app.use()</a>
或
<a
href="https://vee-validate.logaretm.com/v4/guide/components/validation#field-level-validation"
>元件的形式載入</a
>
啟用。(另有指令等各種 Vue 的語法形式)
</li>
</ul>
<h3>使用外部套件注意事項:</h3>
<ul>
<li>需多注意可搭配的版本號</li>
<li>更新頻率</li>
<li>使用人數</li>
</ul>
<h3>範例:載入 VeeValidate 驗證套件</h3>
<!-- 1-1. form 改成 v-form -->
<!-- 1-2. input 改成 v-field -->
<!-- 2. v-form 加上 v-slot -->
<v-form @submit="onSubmit" v-slot="{ errors }">
{{ errors }}
<!-- 3-1. v-field 會加上規則 -->
<!-- 3-4. name 也可以自定義名稱 -->
<v-field
name="欄位名稱"
type="text"
placeholder="Who are you"
:rules="isRequired"
></v-field>
<!-- 3-5. error-message -->
<!-- 3-6. 在 error-message 加上 name="欄位名稱" 對應到 v-field -->
<error-message name="欄位名稱">請填寫此欄位</error-message>
<button>Submit</button>
</v-form>
<p>比對與 ESM 版本上的差異</p>
</div>
// JS
<script src="https://cdnjs.cloudflare.com/ajax/libs/vee-validate/4.1.17/vee-validate.min.js"></script>
<script type="module">
console.log(VeeValidate);
// VeeValidate CDN 版本
const app = Vue.createApp({
components: {
// Components were renamed to avoid conflicts of HTML form element without a vue compiler
VForm: VeeValidate.Form,
VField: VeeValidate.Field,
ErrorMessage: VeeValidate.ErrorMessage,
},
methods: {
onSubmit(value) {
// 3-7. 在 console 加上數值 1
// 3-8. 在 onSubmit() 會自動帶上一個參數
// console.log(1);
console.log(value);
},
// 3-2. 規則會定義在方法裡面,參數會傳入一個值
isRequired(value) {
// 3-3. 驗證值有沒有填寫
if (!value) {
return "此欄是必填的";
}
return true;
},
},
});
app.mount("#app");
</script>
<script>
// ESM 版本的差異(需要 Webpack)
// import { Field, Form, ErrorMessage } from 'vee-validate';
//
// export default {
// components: {
// Field,
// Form,
// ErrorMessage,
// },
// methods: {
// isRequired(value) {
// if (value && value.trim()) {
// return true;
// }
//
// return 'This is required';
// },
// },
// };
</script>
表單驗證套件 vee-validation
如何為單一表單(input) 進行驗證
- 加入 VeeValidation 相關資源
- 註冊元件
- 註冊全域的表單驗證元件 (VForm, VField, ErrorMessage)
- 定義規則
- 加入多國語系
- 套用 v-form 並加入 v-slot
- 套用 v-field 及 error-message
- 加入自訂驗證、送出表單等行為
// HTML
<div id="app">
<h2>套用一個現成的流程</h2>
<p>
參考文件:<a href="https://hackmd.io/FFv0a5cBToOATP7uI5COMQ"
>https://hackmd.io/FFv0a5cBToOATP7uI5COMQ</a
>
</p>
<h3>範例:載入 VeeValidate 驗證套件</h3>
<!-- 5. 套用 v-form 並加入 v-slot -->
<v-form @submit="onSubmit" v-slot="{ errors }">
{{ errors }}
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<!-- 6. 套用 v-field 及 error-message -->
<!-- 6-2. 規則加入 rules 是對應到先前所載入的規則 -->
<!-- 6-3. 加上 input 樣式,使用 :class 方式 -->
<!-- 8-1. 可以直接補上 v-model 對應 user.email 進行驗證 -->
<v-field
id="email"
name="email"
type="email"
class="form-control"
rules="email|required"
v-model="user.email"
placeholder="請輸入 Email"
:class="{ 'is-invalid': errors['email'] }"
></v-field>
<!-- 6-1. error-message 必須對應到 v-field 的 name 欄位 -->
<error-message name="email" class="invalid-feedback"></error-message>
</div>
<div class="mb-3">
<label for="name" class="form-label">姓名</label>
<v-field
id="name"
name="姓名"
type="text"
class="form-control"
rules="required"
v-model="user.name"
placeholder="請輸入姓名"
:class="{ 'is-invalid': errors['姓名']}"
></v-field>
<error-message name="姓名" class="invalid-feedback"></error-message>
</div>
<div class="mb-3">
<label for="phone" class="form-label">電話</label>
<!-- 7. 加入自訂驗證、送出表單等行為… -->
<!-- 7-1. input 改成 v-field -->
<!-- 7-2 規則加上 rules,這裡使用的是 :rules="isPhone" -->
<!-- 綁定的是下方的方法 isPhone(value) -->
<v-field
id="phone"
name="電話"
type="text"
class="form-control"
:rules="isPhone"
v-model="user.phone"
placeholder="請輸入電話"
:class="{ 'is-invalid': errors['電話']}"
></v-field>
<error-message name="電話" class="invalid-feedback"></error-message>
</div>
<div class="mb-3">
<label for="region" class="form-label">地區</label>
<v-field
id="region"
name="地區"
class="form-control"
:class="{ 'is-invalid': errors['地區']}"
placeholder="請輸入地區"
rules="required"
v-model="user.region"
as="select"
>
<option value="">請選擇地區</option>
<option value="台北市">台北市</option>
<option value="高雄市">高雄市</option>
</v-field>
<error-message name="地區" class="invalid-feedback"></error-message>
</div>
<div class="mb-3">
<label for="address" class="form-label">地址</label>
<v-field
id="address"
name="地址"
type="text"
class="form-control"
rules="required"
v-model="user.address"
placeholder="請輸入地址"
:class="{ 'is-invalid': errors['地址']}"
></v-field>
<error-message name="地址" class="invalid-feedback"></error-message>
</div>
<button class="btn btn-primary" type="submit">送出</button>
</v-form>
</div>
// JS
<!-- 1. 加入 VeeValidation 相關資源 -->
<!-- 主套件 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/vee-validate/4.1.17/vee-validate.min.js"></script>
<!-- i18n -->
<script src="https://cdn.jsdelivr.net/npm/@vee-validate/i18n@4.1.17/dist/vee-validate-i18n.min.js"></script>
<!-- rules -->
<script src="https://cdn.jsdelivr.net/npm/@vee-validate/rules@4.1.17/dist/vee-validate-rules.min.js"></script>
<script type="module">
// 3. 定義規則
// 選擇加入特定規則,全規則可參考
// 參考: https://vee-validate.logaretm.com/v4/guide/global-validators#vee-validaterules
VeeValidate.defineRule("email", VeeValidateRules["email"]);
VeeValidate.defineRule("required", VeeValidateRules["required"]);
// 4. 加入多國語系
// 將外部資源儲存至本地
// 外部資源: https://github.com/logaretm/vee-validate/blob/vee-validate%404.1.16/packages/i18n/src/locale/zh_TW.json
// 讀取外部的資源
VeeValidateI18n.loadLocaleFromURL("./zh_TW.json");
// Activate the locale
VeeValidate.configure({
generateMessage: VeeValidateI18n.localize("zh_TW"), // 切換成中文版
validateOnInput: true, // 調整為:輸入文字時,就立即進行驗證
});
const app = Vue.createApp({
data() {
return {
// 8. 有加上資料欄位
user: {
email: "",
name: "",
address: "",
phone: "",
region: "",
},
};
},
methods: {
onSubmit() {
console.log(this.user);
},
isPhone(value) {
const phoneNumber = /^(09)[0-9]{8}$/;
return phoneNumber.test(value) ? true : "需要正確的電話號碼";
},
},
created() {
console.log(this);
},
});
// 2. 註冊元件
// 註冊全域的表單驗證元件(VForm, VField, ErrorMessage)
app.component("VForm", VeeValidate.Form);
app.component("VField", VeeValidate.Field);
app.component("ErrorMessage", VeeValidate.ErrorMessage);
app.mount("#app");
</script>
進階 API 章節延伸資源
Vue Axios
Vee Validation v4 連結