指令語法全介紹 | 操作畫面超容易
指令觀念介紹
綁定內容於畫面上 v-text
// HTML
<div id="app">
<h3>字串 v-text 與 <code v-pre>{{}}</code> (Mustache)</h3>
<p>{{ name }}在{{ position }}吃早餐</p>
<p><strong v-text="name"></strong>在<span v-text="position"></span>吃早餐</p>
<input type="text" v-model="name">
<hr>
<h3>原始字串 v-html</h3>
<div v-html="rawHtml"></div>
<p><a href="https://v3.cn.vuejs.org/api/directives.html#v-html">注意事項</a></p>
<hr>
<h3>單次綁定 v-once</h3>
<p v-once>{{ name }}在{{ position }}吃早餐</p>
<input type="text" v-model="name">
<hr>
<h3>進階技巧:表達式</h3>
<p>樣板字面值: {{ `${name}在${position}吃早餐` }}</p>
<p>反轉字串: {{ text.split('').reverse().join('') }}</p>
<p>綁定 methods: {{ say('杰倫') }}</p>
<p>JS 運算: {{ 1 + 1 }}</p>
<hr>
<h3>進階技巧:顯示資料狀態</h3>
<p>顯示目前的陣列內容 {{ products }}</p>
<hr>
<h3>顯示 <span v-pre>{{ }}</span></h3>
<p v-pre>這段文字不會被轉譯:{{ name }}在{{ position }}吃早餐</p>
</div>
// JS
const App = {
name: 'Hexschool Component',
data() {
return {
name: '小明',
position: '早餐店',
text: '小明在早餐店吃早餐',
rawHtml: '<p>小明在早餐店吃早餐</p>',
products: [
{
name: '蛋餅',
price: 30,
vegan: false
},
{
name: '飯糰',
price: 35,
vegan: false
},
{
name: '小籠包',
price: 60,
vegan: false
},
{
name: '蘿蔔糕',
price: 30,
vegan: true
}
],
selected: [
{
name: '蛋餅',
price: 30,
vegan: false
},
{
name: '飯糰',
price: 35,
vegan: false
},
]
}
},
methods: {
say(name) {
return `${name}在${this.position}吃早餐`
}
},
computed: {
total() {
const total = this.selected.reduce((curr, next) => {
return curr + next.price;
}, 0);
console.log(total);
return total;
}
},
created() {
}
};
Vue.createApp(App).mount('#app');
多筆資料渲染 v-for
// HTML
<div id="app">
<h3>呈現多筆資料於畫面上 v-for</h3>
<p>菜單</p>
<ul>
<li v-for="(item, key) in products" >
{{ key + 1 }} - {{ item.name }} / {{ item.price }}
</li>
</ul>
<h3>物件回圈</h3>
<ul>
<li v-for="(item, key) in productsObj">
{{ key }} - {{ item.name }} / {{ item.price }}元
</li>
</ul>
<h3>純數值回圈</h3>
<ul>
<li v-for="i in 5">{{ i }}</li>
</ul>
<h3>v-for 與 key</h3>
<p>菜單</p>
<ul>
<li v-for="(item, key) in products" v-bind:key="item.name">
{{ key }} - {{ item.name}} / {{ item.price }} 元
<input type="text">
</li>
</ul>
<p>說明:有相同父元素的子元素必須有獨特的 key。重複的 key 會造成渲染錯誤。</p>
<button type="button" v-on:click="reverseArray">反轉資料內容</button>
<hr>
<h3>進階技巧:在 template 標籤使用 v-for</h3>
<table class="table">
<tbody>
<template v-for="(item, i) in products" v-bind:key="item.name">
<tr>
<th rowspan="2">{{ i + 1 }}</th>
<td colspan="2">
{{ item.name }}
</td>
</tr>
<tr>
<td>
{{ item.price }}元
</td>
<td>
<!-- 三元運算子 -->
<!-- 變數 ? true : false -->
{{ item.vegan ? "素食" : "不可素食" }}
</td>
</tr>
</template>
</tbody>
</table>
<p><a href="https://cn.vuejs.org/guide/essentials/list.html#v-for-on-template">參考介紹</a></p>
<h3>補充說明:v-for 與元件</h3>
<ul>
<list-item :item="item" v-for="(item, key) in products" :key="item.name + 2"></list-item>
</ul>
</div>
// JS
const App = {
data() {
return {
name: '小明',
products: [
{
name: '蛋餅',
price: 30,
vegan: false
},
{
name: '飯糰',
price: 35,
vegan: false
},
{
name: '小籠包',
price: 60,
vegan: false
},
{
name: '蘿蔔糕',
price: 30,
vegan: true
},
],
productsObj: {
chineseOmelette: {
name: '蛋餅',
price: 30,
vegan: false
},
riceBall: {
name: '飯糰',
price: 35,
vegan: false
},
soupDumpling: {
name: '小籠包',
price: 60,
vegan: false
},
radishCake: {
name: '蘿蔔糕',
price: 30,
vegan: true
}
},
}
},
methods: {
reverseArray: function () {
this.products.reverse();
},
},
};
Vue.createApp(App)
.component('list-item', {
template: `
<li>
{{ item.name}} / {{ item.price }} 元
</li>
`,
props: ['item']
}).mount('#app');
條件判斷 v-if
// HTML
<div id="app">
<h3>v-if</h3>
<p v-if="isFull">小明 飽了</p>
<p v-else>小明 還沒吃飽</p>
<!-- {{ isFull }} -->
<button type="button" v-on:click="change('isFull')">狀態切換</button>
<hr />
<h3>v-else-if</h3>
<nav class="nav nav-pills nav-fill">
<a
class="nav-link"
href="#"
v-bind:class="{ 'active': link === '小明' }"
v-on:click="link = '小明'"
>小明</a
>
<a
class="nav-link"
href="#"
v-bind:class="{ 'active': link === '小美' }"
v-on:click="link = '小美'"
>小美</a
>
<a
class="nav-link"
href="#"
v-bind:class="{ 'active': link === '杰倫' }"
v-on:click="link = '杰倫'"
>杰倫</a
>
</nav>
<div>
<!-- {{ link }} -->
<div v-if="link === '小明'">小明吃早餐</div>
<div v-else-if="link === '小美'">小美去百貨公司</div>
<div v-else-if="link === '杰倫'">杰倫去幫助人</div>
</div>
<hr />
<h3>注意事項:v-for 與 v-if 混用</h3>
<ul>
<template v-for="(item, key) in products" v-bind:key="item.name">
<li v-if="item.price <= 35">{{ item.name}} / {{ item.price }} 元</li>
</template>
</ul>
<p>
參考說明:<a
href="https://vue3js.cn/docs/zh/guide/conditional.html#v-if-%E4%B8%8E-v-for-%E4%B8%80%E8%B5%B7%E4%BD%BF%E7%94%A8"
>https://vue3js.cn/docs/zh/guide/conditional.html#v-if-%E4%B8%8E-v-for-%E4%B8%80%E8%B5%B7%E4%BD%BF%E7%94%A8</a
>
</p>
<hr />
<h3>v-if 與 v-show</h3>
<p v-show="isFull">小明 飽了</p>
<p v-if="isFull">小明 飽了</p>
<button type="button" v-on:click="change('isFull')">狀態切換</button>
</div>
// JS
const App = {
data() {
return {
name: "小明",
isFull: true,
link: "小明",
products: [
{
name: "蛋餅",
price: 30,
vegan: false,
},
{
name: "飯糰",
price: 35,
vegan: false,
},
{
name: "小籠包",
price: 60,
vegan: false,
},
{
name: "蘿蔔糕",
price: 30,
vegan: true,
},
],
productsObj: {
chineseOmelette: {
name: "蛋餅",
price: 30,
vegan: false,
},
riceBall: {
name: "飯糰",
price: 35,
vegan: false,
},
soupDumpling: {
name: "小籠包",
price: 60,
vegan: false,
},
radishCake: {
name: "蘿蔔糕",
price: 30,
vegan: true,
},
},
};
},
methods: {
change: function (key) {
this[key] = !this[key];
},
},
};
Vue.createApp(App).mount("#app");
HTML 屬性綁定 v-bind
// HTML
<div id="app">
<h3>綁定屬性 v-bind</h3>
<p>{{ breakfastShop.name }}</p>
<img
v-bind:src="breakfastShop.imgUrl"
class="square-img"
v-bind:title="breakfastShop.name"
alt=""
/>
<h3>縮寫形式 <code>:</code></h3>
<img
:src="breakfastShop.imgUrl"
class="square-img"
:title="breakfastShop.name"
alt=""
/>
<hr />
<h3>更多屬性綁定</h3>
小明還想點餐:
<form action="">
<input type="text" value="我要吃蘿蔔糕" />
<button type="submit" :disabled="isFull">送出</button>
</form>
<button type="button" v-on:click="change('isFull')">狀態切換</button>
<hr />
<h3>搭配 v-for</h3>
<table class="table">
<tbody>
<tr v-for="item in products" :key="item.imgUrl">
<td>
<img :src="item.imgUrl" class="square-img" alt="" />
</td>
<td>{{ item.name }}</td>
<td>{{ item.price }}元</td>
<td>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
value=""
:id="item.name"
/>
<label class="form-check-label" :for="item.name"> 我要這個 </label>
</div>
</td>
</tr>
</tbody>
</table>
<hr />
<h3>表達式運用(串接)</h3>
<p>{{ imageSize }}</p>
<input type="range" min="100" max="1000" v-model="imageSize" />
<br />
<img :src="`${breakfastShop.resizeImg}&w=${imageSize}`" alt="" />
<br />
{{ `${breakfastShop.resizeImg}&w=${imageSize}` }}
<hr />
<h3>動態屬性綁定(注意大小寫)</h3>
<button
type="button"
v-on:click="dynamic = dynamic === 'disabled' ? 'readonly':'disabled'"
>
切換為 {{ dynamic }}
</button>
<br />
<input type="text" :[dynamic] :value="name" />
<hr />
<h3>預告:元件的資料傳遞亦是使用 v-bind</h3>
<ul>
<list-item
:item="item"
v-for="(item, key) in products"
:key="item.name + 2"
></list-item>
</ul>
</div>
// JS
const App = {
data() {
return {
name: "小明",
isFull: false,
link: "小明",
imageSize: 200,
dynamic: "disabled",
breakfastShop: {
name: "奇蹟早餐",
imgUrl:
"https://images.unsplash.com/photo-1600182610361-4b4d664e07b9?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=200&q=80",
resizeImg:
"https://images.unsplash.com/photo-1600182610361-4b4d664e07b9?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&q=80",
},
products: [
{
name: "蛋餅",
price: 30,
vegan: false,
imgUrl:
"https://images.unsplash.com/photo-1607278967323-8766a3a501e6?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=200&q=80",
},
{
name: "飯糰",
price: 35,
vegan: false,
imgUrl:
"https://images.unsplash.com/photo-1603245460565-5a7b42a6a6f4?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=200&q=80",
},
{
name: "小籠包",
price: 60,
vegan: false,
imgUrl:
"https://images.unsplash.com/photo-1595424265370-3e02d3e6c10c?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=200&q=80",
},
],
productsObj: {
chineseOmelette: {
name: "蛋餅",
price: 30,
vegan: false,
},
riceBall: {
name: "飯糰",
price: 35,
vegan: false,
},
soupDumpling: {
name: "小籠包",
price: 60,
vegan: false,
},
radishCake: {
name: "蘿蔔糕",
price: 30,
vegan: true,
},
},
};
},
methods: {
change: function (key) {
this[key] = !this[key];
},
},
};
Vue.createApp(App)
.component("list-item", {
template: `
<li>
{{ item.name}} / {{ item.price }} 元
</li>
`,
props: ["item"],
})
.mount("#app");
// CSS
.square-img {
width: 100px;
height: 100px;
object-fit: cover;
}
HTML 樣式綁定
// HTML
<div id="app">
<h2>切換 Class</h2>
<h3>物件寫法</h3>
<!-- key 值是對應 className,物件的值則是判斷式 -->
<div class="box" :class="{ rotate: isTransform, 'bg-danger': boxColor }"></div>
<hr>
<button class="btn btn-outline-primary" v-on:click="change('isTransform')">選轉物件</button>
<button class="btn btn-outline-primary ms-1" v-on:click="change('boxColor')">切換色彩</button>
<hr class="mt-4">
<h3>物件寫法 2</h5>
<div class="box" :class="objectClass"></div>
<hr>
<h4>陣列寫法</h4>
<button class="btn" :class="['btn-primary', 'disabled']">請操作本元件</button>
<button class="btn" :class="arrayClass">請操作本元件</button>
<button type="button"
class="btn btn-outline-primary"
v-on:click="addClass(['btn-primary', 'disabled'])">為陣列加入 Class</button>
<hr>
<h2>行內樣式</h2>
<h4>綁定行內樣式</h4>
<!-- key 會帶入 style 的屬性 -->
<!-- 值則是帶入 Style 相對應的值 -->
<div class="box" :style="{ backgroundColor: 'red' }"></div>
<div class="box" :style="styleObject"></div>
<div class="box" :style="[styleObject, styleObject2]"></div>
</div>
// JS
const App = {
data() {
return {
isTransform: true,
boxColor: false,
objectClass: {
rotate: true,
'bg-danger': true
},
// Array 操作
arrayClass: [''],
// 行內樣式
// 使用駝峰式命名
styleObject: {
backgroundColor: 'red',
borderWidth: '5px'
},
styleObject2: {
boxShadow: '3px 3px 5px rgba(0, 0, 0, 0.16)'
},
styleObject3: {
userSelect: 'none'
}
};
},
methods: {
change: function (key) {
this[key] = !this[key];
},
addClass(arr) {
this.arrayClass.push(...arr);
}
},
};
Vue.createApp(App).mount('#app');
// CSS
.box {
background-color: var(--bs-light);
border: 1px solid var(--bs-gray);
width: 80px;
height: 80px;
}
.box {
transition: all .5s;
}
.box.rotate {
transform: rotate(45deg)
}
資料雙向綁定 v-model
// HTML
<div id="app">
<h3>input</h3>
<input type="text" class="form-control" v-model="name" />
{{ name }}
<hr />
<h3>textarea</h3>
<textarea cols="30" rows="3" class="form-control" v-model="text"></textarea>
{{ text }}
<hr />
<h3>checkbox 單選框</h3>
<p>小明,你是吃飽沒?</p>
<p>{{ checkAnswer }}</p>
<p>{{ checkAnswer ? '吃飽了' : '還沒'}}</p>
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
id="check1"
v-model="checkAnswer"
/>
<label class="form-check-label" for="check1">小明回覆</label>
</div>
<hr />
<h3>checkbox 單選延伸</h3>
<p>小明,你是吃飽沒?</p>
<p>{{ checkAnswer2 }}</p>
<div class="form-check">
<input
type="checkbox"
v-model="checkAnswer2"
true-value="吃飽了"
false-value="還沒"
class="form-check-input"
id="check2"
/>
<label class="form-check-label" for="check2">小明回覆</label>
</div>
<hr />
<h3>checkbox 複選框</h3>
<p>你還要吃什麼?</p>
<p>{{ checkAnswer3.join(' ') }}</p>
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
id="check3"
value="蛋餅"
v-model="checkAnswer3"
/>
<label class="form-check-label" for="check3">蛋餅</label>
</div>
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
id="check4"
value="蘿蔔糕"
v-model="checkAnswer3"
/>
<label class="form-check-label" for="check4">蘿蔔糕</label>
</div>
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
id="check5"
value="豆漿"
v-model="checkAnswer3"
/>
<label class="form-check-label" for="check5">豆漿</label>
</div>
<hr />
<h3>radio 單選框</h3>
<p>你還要吃什麼?</p>
<p>{{ radioAnswer }}</p>
<div class="form-check">
<input
type="radio"
v-model="radioAnswer"
class="form-check-input"
id="radio1"
value="蛋餅"
/>
<label class="form-check-label" for="radio1">蛋餅</label>
</div>
<div class="form-check">
<input
type="radio"
v-model="radioAnswer"
class="form-check-input"
id="radio2"
value="蘿蔔糕"
/>
<label class="form-check-label" for="radio2">蘿蔔糕</label>
</div>
<div class="form-check">
<input
type="radio"
v-model="radioAnswer"
class="form-check-input"
id="radio3"
value="豆漿"
/>
<label class="form-check-label" for="radio3">豆漿</label>
</div>
<hr />
<h3>select 單選</h3>
<p>你還要吃什麼?</p>
<p>{{ selectAnswer }}</p>
<select class="form-select" v-model="selectAnswer">
<option value="" disabled>說吧,你要吃什麼?</option>
<!-- <option value="1234">12345</option> -->
<option :value="item.name" v-for="item in products" :key="item.name">
{{ item.name }} / {{ item.price }} 元
</option>
</select>
<hr />
<h3>select 多選</h3>
<p>你還要吃什麼?</p>
<p>{{ selectAnswer2 }}</p>
<select class="form-select" multiple v-model="selectAnswer2">
<option selected disabled value="">說吧,你要吃什麼?</option>
<option :value="item.name" v-for="item in products" :key="item.name">
{{item.name}} / {{item.price}} 元
</option>
</select>
</div>
// JS
const App = {
data() {
return {
name: "小明",
text: "一段文字敘述",
checkAnswer: false,
checkAnswer2: "",
checkAnswer3: [],
radioAnswer: "蛋餅",
selectAnswer: "",
selectAnswer2: [],
products: [
{
name: "蛋餅",
price: 30,
vegan: false,
},
{
name: "飯糰",
price: 35,
vegan: false,
},
{
name: "小籠包",
price: 60,
vegan: false,
},
{
name: "蘿蔔糕",
price: 30,
vegan: true,
},
],
productsObj: {
chineseOmelette: {
name: "蛋餅",
price: 30,
vegan: false,
},
riceBall: {
name: "飯糰",
price: 35,
vegan: false,
},
soupDumpling: {
name: "小籠包",
price: 60,
vegan: false,
},
radishCake: {
name: "蘿蔔糕",
price: 30,
vegan: true,
},
},
};
},
methods: {
reverseArray: function () {
this.products.reverse();
},
},
};
Vue.createApp(App)
.component("list-item", {
template: `
<li>
{{ item.name}} / {{ item.price }} 元
</li>
`,
props: ["item"],
})
.mount("#app");
v-model 修飾符
// HTML
<div id="app">
<h3>修飾符</h3>
<h4 class="mt-3">延遲 Lazy</h4>
{{ lazyMsg }}
<input type="text" class="form-control" v-model.lazy="lazyMsg" />
<h4 class="mt-3">純數值 Number</h4>
{{ numberMsg }}{{ typeof numberMsg }}
<input type="number" class="form-control" v-model.number="numberMsg" />
<h4 class="mt-3">修剪 Trim</h4>
這是一段{{ trimMsg }}緊黏的文字
<input type="text" class="form-control" v-model.trim="trimMsg" />
</div>
// JS
const App = {
data() {
return {
lazyMsg: "",
numberMsg: "",
trimMsg: "",
};
},
};
Vue.createApp(App).mount("#app");
事件觸發 v-on
// HTML
<div id="app">
<h3>觸發事件 與 縮寫*</h3>
<div class="box" :class="{ rotate: isTransform }"></div>
<hr />
<!-- v-on:click 縮寫寫法 @click -->
<button class="btn btn-outline-primary" @click="changeClass">選轉物件</button>
<br />
<h3>帶入參數*</h3>
<div class="box" :class="{ rotate: isTransform }"></div>
<hr />
<button class="btn btn-outline-primary" v-on:click="change('isTransform')">
選轉物件
</button>
<hr />
<h3>原生 DOM 事件*</h3>
<!-- https://developer.mozilla.org/en-US/docs/Web/Events -->
<h4>input change 事件</h4>
<input type="text" @change="onChange" />
<br /><br />
<h4>form submit 事件</h4>
<form @submit.prevent="submitForm">
<input type="text" v-model="name" />
<button>送出表單</button>
</form>
<hr />
<h3>動態事件 []</h3>
<input type="text" @[event]="dynamicEvent" />
<input type="text" v-model="event" />
<hr />
<h3>動態物件方法 {}</h3>
<!-- 此方法無法傳入參數 -->
<button class="box" @="{ mousedown: down, mouseup: up }"></button>
</div>
// JS
const App = {
data() {
return {
name: "小明",
isTransform: true,
num: 10,
event: "click",
};
},
methods: {
changeClass() {
this.isTransform = !this.isTransform;
},
change(key) {
this[key] = !this[key];
},
onChange(evt) {
console.log("change 事件");
console.log(evt);
},
submitForm() {
console.log("表單已送出", `name 為 ${this.name}`);
},
dynamicEvent() {
console.log("這是一個動態事件", this.event);
},
down() {
console.log("按下");
},
up() {
console.log("放開");
},
},
};
Vue.createApp(App).mount("#app");
// CSS
.box {
background-color: var(--bs-light);
border: 1px solid var(--bs-gray);
width: 80px;
height: 80px;
}
.box {
transition: all 0.5s;
}
.box.rotate {
transform: rotate(45deg);
}
v-on 修飾符
// HTML
<div id="app">
<h3>修飾符</h3>
<h4>按鍵修飾符*</h4>
<ul>
<li>keyAlias - 只當事件是從特定鍵觸發時才觸發。</li>
<li>
別名修飾 - .enter, .tab, .delete, .esc, .space, .up, .down, .left, .right
</li>
<li>
修飾符來實現僅在按下相應按鍵時才觸發鼠標或鍵盤事件的監聽器 - .ctrl, .alt,
.shift, .meta
</li>
</ul>
<h6 class="mt-3">別名修飾</h6>
<input
type="text"
class="form-control"
v-model="text"
@keyup.enter="trigger('enter')"
/>
<h6 class="mt-3">相應按鍵時才觸發的監聽器</h6>
<input
type="text"
class="form-control"
v-model="text"
@keyup.shift.enter="trigger('shift + Enter')"
/>
<h6 class="mt-3">特定鍵</h6>
<input
type="text"
class="form-control"
v-model="text"
@keyup.h="trigger('h')"
/>
<hr />
<h4>滑鼠修飾符</h4>
<ul>
<li>.left 只當點擊鼠標左鍵時觸發。</li>
<li>.right 只當點擊鼠標右鍵時觸發。</li>
<li>.middle 只當點擊鼠標中鍵時觸發。</li>
</ul>
<h6 class="mt-3">滑鼠修飾符</h6>
<div class="p-3 bg-primary">
<span class="box" @click.right="trigger('right button')"> </span>
</div>
<hr />
<h4>事件修飾符</h4>
<ul>
<li>.stop - 調用 event.stopPropagation()。</li>
<li><strong>.prevent - 調用 event.preventDefault()。</strong></li>
<li>.capture - 添加事件偵聽器時使用 capture 模式。</li>
<li>.self - 只當事件是從偵聽器綁定的元素本身觸發時才觸發回調。</li>
<li>.once - 只觸發一次回調。</li>
</ul>
<a href="https://www.google.com/" @click.prevent="trigger('prevent')"
>加入 Prevent</a
>
<hr />
<h6>預設情境</h6>
<a href="https://javascript.info/bubbling-and-capturing">冒泡事件參考文章</a>
<div class="p-3 bg-primary" @click="trigger('div')">
<span
class="box d-flex align-items-center justify-content-center"
@click="trigger('box')"
>
<button class="btn btn-outline-secondary" @click="trigger('button')">
按我
</button>
</span>
</div>
<h6 class="mt-3">stopPropagation (防止向外尋找)</h6>
<div class="p-3 bg-primary" @click="trigger('div')">
<span
class="box d-flex align-items-center justify-content-center"
@click.stop="trigger('box')"
>
<button class="btn btn-outline-secondary" @click="trigger('button')">
按我
</button>
</span>
</div>
<h6 class="mt-3">事件偵聽器時使用 capture 模式 (事件改為由外而內)</h6>
<div class="p-3 bg-primary" @click.capture="trigger('div')">
<span
class="box d-flex align-items-center justify-content-center"
@click.capture="trigger('box')"
>
<button
class="btn btn-outline-secondary"
@click.capture="trigger('button')"
>
按我
</button>
</span>
</div>
<h6 class="mt-3">事件偵聽器時使用 self 模式 (只會觸發自己範圍內的)</h6>
<div class="p-3 bg-primary" @click.self="trigger('div')">
<span
class="box d-flex align-items-center justify-content-center"
@click.self="trigger('box')"
>
<button class="btn btn-outline-secondary" @click.self="trigger('button')">
按我
</button>
</span>
</div>
<hr />
<h3 class="mt-3">事件偵聽器只觸發一次 once</h3>
<div class="p-3 bg-primary" @click.once="trigger('div')">
<span
class="box d-flex align-items-center justify-content-center"
@click.once="trigger('box')"
>
<button class="btn btn-outline-secondary" @click.once="trigger('button')">
按我
</button>
</span>
</div>
<hr />
</div>
// JS
const App = {
data() {
return {
text: "小明",
isTransform: true,
num: 10,
event: "click",
};
},
methods: {
trigger: function (name) {
console.log(name, "此事件被觸發了");
},
},
};
Vue.createApp(App).mount("#app");
// CSS
.box {
display: block;
background-color: var(--bs-light);
border: 1px solid var(--bs-gray);
width: 80px;
height: 80px;
}
.box {
transition: all 0.5s;
}
.box.rotate {
transform: rotate(45deg);
}
v-on DOM 事件處理技巧
// HTML
<div id="app">
<h3>原生 DOM 事件</h3>
<a href="https://www.w3schools.com/jsref/dom_obj_event.asp">參考:DOM 事件</a>
<div class="box" :class="{ rotate: isTransform }"></div>
<hr />
<!-- 當沒有參數時,預設第一個則是 dom 事件參數 -->
<button class="btn btn-outline-primary" @click="changeClass">選轉物件</button>
<button class="btn btn-outline-primary" @keyup.enter="changeClass">
按鈕事件
</button>
<!-- 當如果有參數時,則可以使用 $event -->
<button
class="btn btn-outline-primary"
@click="changeClassWithEvent('這段是自訂參數', $event)"
>
自訂參數
</button>
<br />
<br />
<h3>取得原生 input 數值</h3>
<input type="text" @change="inputEvent" />
<br /><br />
<h3>監聽按鍵事件</h3>
<input type="text" @keyup="keyboardEvent" />
</div>
// JS
const App = {
data() {
return {
text: "小明",
isTransform: true,
num: 10,
};
},
methods: {
changeClass: function (e) {
console.log("dom", e);
this.isTransform = !this.isTransform;
},
changeClassWithEvent(parameter, e) {
console.log(parameter, e);
},
inputEvent(e) {
console.log(e.target.value);
},
keyboardEvent(e) {
console.dir(e.keyCode);
if (e.keyCode === 13) {
alert("你按下了 Enter");
} else if (e.keyCode === 32) {
alert("你按下了 空白鍵");
}
},
},
};
Vue.createApp(App).mount("#app");
// CSS
.box {
display: block;
background-color: var(--bs-light);
border: 1px solid var(--bs-gray);
width: 80px;
height: 80px;
}
.box {
transition: all 0.5s;
}
.box.rotate {
transform: rotate(45deg);
}
指令章節作業 – 簡單版
- 試著拆解流程
- 先觀察解答
- 第二次製作,就完全不要看解答
指令章節作業 – 簡單版實作示範
- 先準備左邊的品項呈現
- 準備暫存資料
- 暫存資料轉為訂單內容
- 計算總金額
- 客製化選項
// HTML
<div id="app">
<div class="container gx-2">
<div class="row gx-3 bg-light py-3">
<!-- 1. 先準備左邊的品項呈現 -->
<!-- 2-2. 觸發點擊事件 -->
<!-- 2-3. v-bind:class 樣式綁定 -->
<!-- key 值是對應 className,物件的值則是判斷式 -->
<div class="col-md-4">
<div class="list-group">
<a
href="#"
class="list-group-item list-group-item-action"
v-for="item in products"
:key="item.name"
@click.prevent="selectProduct(item)"
:class="{ 'active': itemSelected.name === item.name }"
>
<h6 class="card-title mb-1">{{ item.name }}</h6>
<div class="d-flex align-items-center justify-content-between">
<p class="mb-0"><small>{{ item.engName }}</small></p>
<p class="mb-0"><small>NT$ {{ item.price }}</small></p>
</div>
</a>
</div>
</div>
<div class="col-md-8">
<div class="card mb-2">
<!-- 2-4. 條件判斷 v-if -->
<div
v-if="!itemSelected.name"
class="position-absolute text-white d-flex align-items-center justify-content-center active"
style="
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.65);
z-index: 100;
"
>
請先選擇飲品
</div>
<!-- 5. 客製化選項 -->
<!-- 5-1. 資料雙向綁定 v-model -->
<div class="card-body px-4">
<div class="mb-3">
<label for="productNum" class="form-label">數量</label>
<input
type="number"
class="form-control"
id="productNum"
v-model="itemSelected.count"
placeholder="數量"
min="0"
/>
</div>
<div class="mb-3">
<label for="productIce" class="form-label d-block">冰塊*</label>
<div
class="form-check form-check-inline"
v-for="(ice, key) in iceType"
:key="'ice' + key"
>
<input
class="form-check-input"
name="iceType"
type="radio"
:id="'ice' + key"
:value="ice"
v-model="itemSelected.ice"
/>
<label class="form-check-label" :for="'ice' + key"
>{{ ice }}</label
>
</div>
</div>
<div class="mb-3">
<label for="productSugar" class="form-label d-block">甜度*</label>
<div
class="form-check form-check-inline"
v-for="(sugar, key) in sugarType"
:key="'sugar' + key"
>
<input
class="form-check-input"
name="sugarType"
type="radio"
:id="'sugar' + key"
:value="sugar"
v-model="itemSelected.sugar"
/>
<label class="form-check-label" :for="'sugar' + key"
>{{ sugar }}</label
>
</div>
</div>
<!-- 5-6. 製作加料是相對複雜的部份 -->
<!-- 5-7. v-for -->
<div class="mb-3">
<label for="productTopping" class="form-label d-block"
>加料</label
>
<div
class="form-check form-check-inline"
v-for="(topping, key) in toppingsType"
:key="'topping' + key"
>
<!-- 5-9. 把加料的值取出來 -->
<!-- 5-10. 資料雙向綁定 v-model -->
<input
class="form-check-input"
type="checkbox"
:id="'topping' + key"
:value="topping"
v-model="itemSelected.toppings"
/>
<label class="form-check-label" :for="'topping' + key"
>{{ topping }}</label
>
</div>
</div>
<div class="mb-3">
<label for="productNotice" class="form-label">備註</label>
<textarea
class="form-control"
id="productNotice"
rows="2"
v-model="itemSelected.notice"
></textarea>
</div>
<div class="d-flex gap-2">
<!-- 5-5. 觸發取消清空 -->
<button
class="btn btn-outline-primary w-100"
@click.prevent="resetOrder"
type="button"
>
取消
</button>
<!-- 3-2. 觸發加到購物車 -->
<button
class="btn btn-primary w-100"
@click.prevent="addToOrder(itemSelected)"
type="button"
:disabled="!itemSelected.name"
>
加入
</button>
</div>
</div>
</div>
<div class="card" v-if="orderList.length > 0">
<div class="card-body">
<table class="table">
<thead>
<tr>
<th scope="col">品項</th>
<th scope="col">冰塊</th>
<th scope="col">甜度</th>
<th scope="col">加料</th>
<th scope="col">單價</th>
<th scope="col">數量</th>
<th scope="col">小計</th>
</tr>
</thead>
<tbody>
<!-- 3-4. 呈現在列表畫面上 -->
<!-- 5-11. 把加料呈現在列表畫面上,並轉成字串 -->
<!-- 5-13. 調整加上加料後的單價 -->
<tr v-for="(item, key) in orderList" :key="'order' + key">
<th scope="row">
{{ item.name }}<br />
<small
class="text-muted fw-normal"
v-if="item.notice !== ''"
>備註:{{ item.notice }}</small
>
</th>
<td>{{ item.ice }}</td>
<td>{{ item.sugar }}</td>
<td>{{ item.toppings.toString() }}</td>
<td>{{ item.price + item.toppings.length * 10 }}</td>
<td>{{ item.count }}</td>
<td class="text-end">{{ item.total }}</td>
</tr>
</tbody>
</table>
<p class="text-end">共 NT$ {{ orderTotal }} 元</p>
<button
class="btn btn-sm btn-secondary w-100"
:disabled="orderList.length === 0"
@click="resetOrder"
>
重新選擇
</button>
</div>
</div>
</div>
</div>
</div>
</div>
// JS
const App = {
data() {
return {
// 2. 準備暫存資料
itemSelected: {},
// 3-3. 購物車列表
orderList: [],
// 4-3. 訂單總價
orderTotal: 0,
iceType: ["正常冰", "少冰", "微冰", "去冰", "熱"],
sugarType: ["全糖", "七分", "半糖", "三分", "無糖"],
toppingsType: ["珍珠", "粉條", "小粉圓", "椰果", "芋頭"],
products: [
{
name: "珍珠鮮奶茶",
engName: "Pearl Milk Tea",
price: 60,
},
{
name: "鮮奶茶",
engName: "Milk Tea",
price: 50,
},
{
name: "古意冬瓜茶",
engName: "Winter Melon Drink",
price: 30,
},
{
name: "蜜香紅茶",
engName: "Black Tea",
price: 30,
},
{
name: "包種青茶",
engName: "Pouchong tea",
price: 35,
},
{
name: "檸檬烏龍",
engName: "Lemon Oolong Tea",
price: 55,
},
{
name: "薑母茶",
engName: "Ginger Tea",
price: 55,
},
{
name: "青草茶",
engName: "Herbal Tea",
price: 35,
},
{
name: "金桔檸檬",
engName: "Kumquat Lemonade",
price: 40,
},
{
name: "柳澄青茶",
engName: "Orange Mountain Tea",
price: 45,
},
],
};
},
methods: {
// 2-1. 選擇品項
selectProduct(product) {
this.itemSelected = {
// 透過解構方式避免影響到原始資料
...product,
// 3-5. 加上預設值
count: 1,
// 5-8. 加入加料項目
toppings: [],
ice: "正常冰",
sugar: "全糖",
notice: "",
};
},
// 3-1. 加到購物車
addToOrder(product) {
// 4. 計算總金額
// 4-1. 重新整理訂單所需要的內容
const order = {
// 透過解構方式
...product,
// 小計
// 5-12. 加料
total: (product.price + product.toppings.length * 10) * product.count,
};
// 4-2. product → order
this.orderList.push(order);
// 4-5. 計算當前的總價
this.countTotal();
// 5-4. 使用 resetOrder 方法加入後清空
this.resetOrder();
},
// 4-4. 計算總價
countTotal() {
// 4-6. 每次計算先清空總價
this.orderTotal = 0;
this.orderList.forEach((item) => {
this.orderTotal += item.total;
});
},
// 5-3. 清空方法
resetOrder() {
// 5-2. 加入後清空
this.itemSelected = {};
},
},
};
Vue.createApp(App).mount("#app");
指令章節作業
- 先寫出流程,一個一個拆解
- 列表
- 表單
- 加入購物車
- 請同時撰寫筆記,製作第二次,僅參考自己筆記完成
// HTML
<div id="app">
<div class="container py-5 gx-2">
<h2 class="text-center">指令章節作業</h2>
<div class="row gx-3 bg-light py-3">
<div class="col-md-4">
<div class="list-group">
<a
href="#"
class="list-group-item list-group-item-action"
v-for="item in products"
:key="item.name"
@click.prevent="selectProduct(item)"
:class="{ 'active': itemSelected.name === item.name }"
>
<h6 class="card-title mb-1">{{ item.name }}</h6>
<div class="d-flex align-items-center justify-content-between">
<p class="mb-0"><small>{{ item.engName }}</small></p>
<p class="mb-0"><small>NT$ {{ item.price }}</small></p>
</div>
</a>
</div>
</div>
<div class="col-md-8">
<div class="card mb-2">
<!-- 條件判斷 v-if -->
<div
v-if="!itemSelected.hasOwnProperty('name')"
class="position-absolute text-white d-flex align-items-center justify-content-center"
style="
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.65);
z-index: 100;
"
>
請先選擇飲品
</div>
<div class="card-body px-4">
<div class="mb-3">
<label for="productNum" class="form-label">數量</label>
<input
type="number"
class="form-control"
id="productNum"
v-model="itemSelected.count"
placeholder="數量"
min="0"
/>
</div>
<div class="mb-3">
<label for="productIce" class="form-label d-block">冰塊*</label>
<div
class="form-check form-check-inline"
v-for="(ice, key) in iceType"
:key="'ice' + key"
>
<!-- HTML 屬性綁定 :disabled -->
<input
class="form-check-input"
name="iceType"
type="radio"
:id="'ice' + key"
:value="ice"
v-model="itemSelected.ice"
:disabled="!itemSelected.hasOwnProperty('defaults') || (itemSelected.defaults.ice !== '' && itemSelected.defaults.ice !== ice)"
/>
<label class="form-check-label" :for="'ice' + key"
>{{ ice }}</label
>
</div>
</div>
<div class="mb-3">
<label for="productSugar" class="form-label d-block">甜度*</label>
<div
class="form-check form-check-inline"
v-for="(sugar, key) in sugarType"
:key="'sugar' + key"
>
<!-- HTML 屬性綁定 :disabled -->
<input
class="form-check-input"
name="sugarType"
type="radio"
:id="'sugar' + key"
:value="sugar"
v-model="itemSelected.sugar"
:disabled="!itemSelected.hasOwnProperty('defaults') || (itemSelected.defaults.sugar !== '' && itemSelected.defaults.sugar !== sugar)"
/>
<label class="form-check-label" :for="'sugar' + key"
>{{ sugar }}</label
>
</div>
</div>
<div class="mb-3">
<label for="productTopping" class="form-label d-block"
>加料</label
>
<div
class="form-check form-check-inline"
v-for="(topping, key) in toppingsType"
:key="'topping' + key"
>
<!-- HTML 屬性綁定 :disabled -->
<input
class="form-check-input"
type="checkbox"
:id="'topping' + key"
:value="topping"
v-model="itemSelected.toppings"
:disabled="!itemSelected.hasOwnProperty('defaults') || (itemSelected.defaults.toppings.includes(topping))"
/>
<label class="form-check-label" :for="'topping' + key"
>{{ topping }}</label
>
</div>
</div>
<div class="mb-3">
<label for="productNotice" class="form-label">備註</label>
<textarea
class="form-control"
id="productNotice"
rows="2"
v-model="itemSelected.notice"
></textarea>
</div>
<div class="d-flex gap-2">
<button
class="btn btn-outline-primary w-100"
@click.prevent="resetOrder"
type="button"
>
取消
</button>
<!-- HTML 屬性綁定 :disabled -->
<button
class="btn btn-primary w-100"
@click.prevent="addToOrder(itemSelected)"
:disabled="!itemSelected.hasOwnProperty('name')"
type="button"
>
加入
</button>
</div>
</div>
</div>
<!-- 條件判斷 v-if -->
<div class="card" v-if="orderList.length > 0">
<div class="card-body">
<table class="table">
<thead>
<tr>
<th scope="col">品項</th>
<th scope="col">冰塊</th>
<th scope="col">甜度</th>
<th scope="col">加料</th>
<th scope="col">單價</th>
<th scope="col">數量</th>
<th scope="col">小計</th>
</tr>
</thead>
<tbody>
<!-- 多筆資料渲染 v-for -->
<tr v-for="(item, key) in orderList" :key="'order' + key">
<th scope="row">
{{ item.name }}<br />
<!-- 條件判斷 v-if -->
<small
v-if="item.notice !== ''"
class="text-muted fw-normal"
>備註:{{ item.notice }}</small
>
</th>
<td>{{ item.ice }}</td>
<td>{{ item.sugar }}</td>
<td>{{ item.toppings.toString() }}</td>
<td>{{ item.price + item.toppings.length * 10 }}</td>
<td>{{ item.count }}</td>
<td class="text-end">{{ item.total }}</td>
</tr>
</tbody>
</table>
<p class="text-end">共 NT$ {{ orderTotal }} 元</p>
<!-- HTML 屬性綁定 :disabled -->
<!-- 事件觸發 v-on generateOrder() -->
<button
class="btn btn-sm btn-primary w-100"
:disabled="orderList.length === 0"
@click.prevent="generateOrder(orderList, orderTotal)"
>
產生訂單
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 產生訂單 -->
<!-- 條件判斷 v-if -->
<div class="bg-light p-3 mt-3" v-if="checkedOrder.orders.length > 0">
<div class="bg-white p-3 d-flex flex-column" style="min-height: 450px">
<table class="table">
<thead>
<tr>
<th scope="col">品項</th>
<th scope="col">冰塊</th>
<th scope="col">甜度</th>
<th scope="col">加料</th>
<th scope="col">單價</th>
<th scope="col">數量</th>
<th scope="col">小計</th>
</tr>
</thead>
<tbody>
<!-- 多筆資料渲染 v-for -->
<tr v-for="(item, key) in checkedOrder.orders" :key="'order' + key">
<th scope="row">
{{ item.name }}<br />
<!-- 條件判斷 v-if -->
<small v-if="item.notice !== ''" class="text-muted fw-normal"
>備註:{{ item.notice }}</small
>
</th>
<td>{{ item.ice }}</td>
<td>{{ item.sugar }}</td>
<td>{{ item.toppings.toString() }}</td>
<td>{{ item.price + item.toppings.length * 10 }}</td>
<td>{{ item.count }}</td>
<td class="text-end">{{ item.total }}</td>
</tr>
</tbody>
</table>
<p class="mt-3 mb-1">訂單成立時間:{{ checkedOrder.time }}</p>
<p class="mb-1">餐點數: {{ checkedOrder.orders.length }}</p>
<p class="mb-1">付款狀態:未付款</p>
<p class="text-end mt-auto">共 NT$ {{ checkedOrder.total }} 元</p>
</div>
</div>
</div>
// JS
const app = {
// 資料 (函式)
data() {
return {
itemSelected: {},
orderList: [],
orderTotal: 0,
// 確認訂單
checkedOrder: {
time: "",
total: 0,
orders: [],
},
iceType: ["正常冰", "少冰", "微冰", "去冰", "熱"],
sugarType: ["全糖", "七分", "半糖", "三分", "無糖"],
toppingsType: ["珍珠", "粉條", "小粉圓", "椰果", "芋頭"],
products: [
{
name: "珍珠鮮奶茶",
engName: "Pearl Milk Tea",
price: 60,
defaults: {
toppings: ["珍珠"],
sugar: "",
ice: "",
},
},
{
name: "椰果鮮奶茶",
engName: "Coconut Milk Tea",
price: 60,
defaults: {
toppings: ["椰果"],
sugar: "",
ice: "",
},
},
{
name: "鮮奶茶",
engName: "Milk Tea",
price: 50,
defaults: {
toppings: [""],
sugar: "",
ice: "",
},
},
{
name: "古意冬瓜茶 (糖固定)",
engName: "Winter Melon Drink",
price: 30,
defaults: {
toppings: [""],
sugar: "全糖",
ice: "",
},
},
{
name: "蜜香紅茶",
engName: "Black Tea",
price: 30,
defaults: {
toppings: [""],
sugar: "",
ice: "",
},
},
{
name: "包種青茶",
engName: "Pouchong tea",
price: 35,
defaults: {
toppings: [""],
sugar: "",
ice: "",
},
},
{
name: "檸檬烏龍",
engName: "Lemon Oolong Tea",
price: 55,
defaults: {
toppings: [""],
sugar: "",
ice: "",
},
},
{
name: "薑母茶 (熱飲)",
engName: "Ginger Tea",
price: 55,
defaults: {
toppings: [""],
sugar: "",
ice: "熱",
},
},
{
name: "青草茶",
engName: "Herbal Tea",
price: 35,
defaults: {
toppings: [""],
sugar: "",
ice: "",
},
},
{
name: "金桔檸檬",
engName: "Kumquat Lemonade",
price: 40,
defaults: {
toppings: [""],
sugar: "",
ice: "",
},
},
{
name: "柳澄青茶",
engName: "Orange Mountain Tea",
price: 45,
defaults: {
toppings: [""],
sugar: "",
ice: "",
},
},
],
};
},
// 生命週期 (函式)
created() {
console.log(this);
},
// 方法 (物件)
methods: {
// 選擇飲品
selectProduct(product) {
this.itemSelected = {
...product,
count: 1,
toppings: [],
// 三元運算子
ice: product.defaults.ice !== "" ? product.defaults.ice : "正常冰",
sugar:
product.defaults.sugar !== "" ? product.defaults.sugar : "全糖",
notice: "",
};
},
// 加入品項
addToOrder(product) {
const order = {
...product,
total: (product.price + product.toppings.length * 10) * product.count,
};
this.orderList.push(order);
this.countTotal();
this.resetOrder();
},
// 計算總價
countTotal() {
// reduce 將陣列化為單一值
this.orderTotal = this.orderList.reduce(
(acc, obj) => acc + obj.total,
0
);
},
resetOrder() {
this.itemSelected = {};
},
// 產生訂單
generateOrder(orders, total) {
const date = new Date().toLocaleString();
this.checkedOrder.time = date;
this.checkedOrder.orders = orders;
this.checkedOrder.total = total;
this.orderList = [];
this.resetOrder();
},
},
};
Vue.createApp(app).mount("#app");