# 1-4 Vue.js 的黑魔法: 指令
接下來的幾篇內容,會開始解說 Vue.js 模板裡的黑魔法 —— 「指令」(directive)。
為什麼稱它做黑魔法呢? 相信跟著此系列看到這裡的讀者,或多或少都發現 Vue.js 提供了一些特殊的語法,
沒錯,就是那些 v-
開頭的屬性們,像是最早出現的 v-model
、後來用來處理事件處理的 v-on
等等。
先前沒有特別解說的疑惑,從這一篇開始詳細來說明。
這些帶有 v-
開頭的屬性們,在 Vue.js 裡面就被稱為 「指令」(directive) ,
當與指令搭配運算式 (expression) 的值被改變時,對應的標籤 (包括節點、元件等) 也會隨之影響。 換句話說,透過指令的作用與狀態的變化,我們也就可以透過改變狀態 (資料) 來操作整個網頁系統了。
# 屬性綁定 - v-bind
我發現有不少朋友在剛開始學習 Vue.js 的時候,很可能都做過類似的事,就是覺得既然可以利用 {{ }}
將資料輸出至網頁模版上,那麼網頁標籤內的「屬性」是不是也能這樣呢:
const vm = Vue.createApp({
data () {
return {
customId: 'item-id-1'
}
}
}).mount('#app');
2
3
4
5
6
7
<div id="app">
<p id="{{ customId }}">...</p>
</div>
2
3
像上面程式的執行結果,標籤 <p>
的 id
並不會如我們所預期地出現 item-id-1
,
因為像 {{ }}
的模板語法並沒有辦法被套用在 HTML 標籤的「屬性」上,
如果我們需要由 Vue.js 來控制標籤的「屬性」,則可以使用 v-bind
的指令。
v-bind
指令的用法很簡單,我們只要再標籤上面加上 v-bind:屬性名稱
,
以剛剛的範例來說,我們希望由 Vue 的實體來輸出 id
,就可以這樣做:
<p v-bind:id="customId">...</p>
實際在瀏覽器渲染時會變成:
<p id="item-id-1">...</p>
再來個例子,如果說我們今天希望控制某個按鈕是否為 disabled
的狀態,就可以這樣做:
const vm = Vue.createApp({
data () {
return {
isBtnDisabled: true
}
},
}).mount('#app');
2
3
4
5
6
7
<button v-bind:disabled="isBtnDisabled">click me!</button>
如果這個時候 isBtnDisabled
的內容是 true
,那麼這個按鈕就會被掛上 disabled
的屬性無法使用。
如果為 false
,這個按鈕就可以正常使用。
像這樣,常見的標籤屬性如 id
、 圖片的 src
,或是連結的 href
等 DOM 的屬性,都可以透過 v-bind
指令來控制它的內容。
小提醒
v-bind
指令除了完整的 v-bind:屬性名稱
寫法外,也可以直接簡寫成 :屬性名稱
,如:
<img v-bind:src="imageSrcUrl">
<img :src="imageSrcUrl">
2
3
上面兩種寫法,在瀏覽器渲染後的結果會是一樣的。
簡單說,有 :
的是 Vue 實體 v-bind
綁定後的屬性,沒有的則是純文字屬性。
像這樣的用法我們在後續還會常常見到它們。
# 表單綁定 - v-model
在一個與高互動性的網頁中,表單類型的元素是不可或缺的。
常見的表單元素像是 <input>
、<textarea>
以及 <select>
等,在 Vue.js 可透過 v-model
來進行資料的「雙向」綁定, v-model
會根據不同的表單類別來更新元素的內容。
小提醒
由於 <div>
標籤並非表單元素,所以即使加上 contenteditable
屬性仍無法透過 v-model
指令來處理與資料的雙向綁定。
# input
文字框
首先是最常見的 input
文字框,當我們在 input
加上 v-model="message"
之後,此時這個輸入框便會自動被綁定 input
事件:
<div id="app">
<input v-model="message" placeholder="edit me">
<p>Message is: {{ message }}</p>
</div>
2
3
4
const vm = Vue.createApp({
data () {
return {
message: 'Hello'
}
}
});
vm.mount('#app');
2
3
4
5
6
7
8
9
當輸入框內的文字被更改的時候,就會同步更新至 data
的 message
裡。
# textarea
文字方塊
textarea
的使用方式與 input
完全一樣:
<p><span>Multiline message is:</span> {{ message }}</p>
<textarea v-model="message" placeholder="add multiple lines"></textarea>
2
3
這裏要特別注意的是, 若在 <textarea>
裡面使用大括號 {{ }}
模板語法而不是 v-model
時,只會控制內容的顯示,此時更新的時候並不會寫回 data
內,需要使用 v-model
指令來與 data
內的狀態綁定。
# radio
/ checkbox
選擇框
input
標籤提供了兩種選擇框,分別是 radio
的單選框,以及 checkbox
的複選框。
先看 radio
的單選框,使用的方式很簡單,我們只需要在對應的 input
加上 v-model
,並且透過 value
指定它的值:
<div id="app">
<!-- 要注意下面兩組 input 對應的都是 v-model="picked" -->
<div>
<input type="radio" id="one" value="1" v-model="picked">
<label for="one">One</label>
</div>
<div>
<input type="radio" id="two" value="2" v-model="picked">
<label for="two">Two</label>
</div>
<span>Picked: {{ picked }}</span>
</div>
2
3
4
5
6
7
8
9
10
11
12
13
const vm = Vue.createApp({
data () {
return {
picked: 1
}
}
}).mount('#app');
2
3
4
5
6
7
因為 data
裡的 picked
預設為 1
,所以執行時畫面上 <input type="radio" id="one" value="1">
會預設被選中。
再來是 checkbox
的複選框,checkbox
這個元素很有趣,你可以將它當作多選的選項來看待,
但是當它只有一個的時候,又可以將它看做 true
或 false
( on 或 off ) 的選項。
先看複選時的情境,用法跟前面 radio
完全一樣, 因為是複選的關係,唯一的差別只在 data
內的狀態必須是「陣列」:
<div id="app">
<!-- 注意下面 input 對應的都是 v-model="checkedNames" -->
<input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
<label for="jack">Jack</label>
<input type="checkbox" id="john" value="John" v-model="checkedNames">
<label for="john">John</label>
<input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
<label for="mike">Mike</label>
<input type="checkbox" id="mary" value="Mary" v-model="checkedNames">
<label for="mary">Mary</label>
<br>
<p>Checked names: {{ checkedNames }}</p>
</div>
2
3
4
5
6
7
8
9
10
11
12
13
// 因為是複選的關係,這裡的 checkedNames 是陣列
const vm = Vue.createApp({
data () {
return {
checkedNames: []
}
}
}).mount('#app');
2
3
4
5
6
7
8
換句話說,如果我們要控制表單的「全選」或「全部取消」狀態,只需要控制 data
裡的 checkedNames
陣列內容即可。
另外, checkbox
也可用在只有一個選項的情境,像是:
<div id="app">
<input type="checkbox" id="checkbox" v-model="isChecked">
<label for="checkbox">Status: {{ isChecked }}</label>
</div>
2
3
4
const vm = Vue.createApp({
data () {
return {
isChecked: false
}
}
}).mount('#app');
2
3
4
5
6
7
此時, data
內的選項,會變成 true
或 false
,當值為 true
時,對應的 checkbox
會被勾起。
# select
下拉列表選單
select
元素是很常見的下拉式選單,它用來與 option
做搭配,如下:
<div id="app">
<!-- 注意,v-model 要放在 select 而不是 option -->
<select v-model="selected">
<option disabled value="">請選擇</option>
<option>台北市</option>
<option>新北市</option>
<option>基隆市</option>
</select>
<p>Selected: {{ selected || '未選擇' }}</p>
</div>
2
3
4
5
6
7
8
9
10
11
const vm = Vue.createApp({
data () {
return {
selected: ''
}
}
}).mount('#app');
2
3
4
5
6
7
要小心的是,v-model
必須使用在 <select>
標籤,不可套用在 <option>
標籤中。
若是 <select>
標籤中 v-model
的值無法對應到任何一個選項時,這個 select
標籤會預設為未選中的狀態,
在 iOS 系統中,會讓使用者無法選擇第一個選項,因為此時 iOS 不會觸發 change
事件。
所以為了解決這個問題,我們可以在第一個 option
的空值選項加入 disabled
屬性來排除此問題。
注意
使用了 v-model
指令的表單元素會自動忽略原有的 value
、 checked
與 selected
屬性,實際的值將以 data
內的狀態為主。
v-model
表面上看似神奇,實際上 Vue.js 是將表單元素的事件監聽與實體內容更新的動作在背後處理掉了,可說是語法糖的一種。
# v-model
與修飾子
前面說過, v-model
在 <input>
的 text
文字輸入框對應的是 input
事件。
換句話說,當使用者每敲一次鍵盤就會觸發一次 input
事件,若是我們不希望這個文字輸入框的內容更新次數這麼頻繁,可以怎麼做呢?
以傳統的 JavaScript 做法來說,我們會將 input
事件改為監聽 change
事件,但 v-model
都幫我們在背後把事件處理掉了,我們該怎麼修改呢?
別擔心,Vue.js 針對此類需求提供了「修飾子」 (Modifiers) 的增強功能來協助我們。
為了增強指令 (directive) 的功能,修飾子會以 .XXX
的形式附加在指令後面,而與 v-model
搭配的有下面三種:
# .lazy
<input v-model.lazy="msg">
像這樣在 v-model
後面加上 .lazy
,這個 input
輸入框就會從原本的 input
事件改為監聽 change
事件,
換句話說,就會在使用者離開輸入框焦點時才會更新 data
內容。
# .number
在 input
輸入框當中,即便我們輸入的都是數字,但實際上由 DOM API 取出的時候都會是以「字串」的形式出現。
假設我們今天想要做到兩個輸入框內容的數字相加,最簡單的方式可以這樣寫:
<input v-model="num1"> + <input v-model="num2"> = {{ sum }}
const vm = Vue.createApp({
data () {
return {
num1: 0,
num2: 0,
}
},
computed: {
sum () {
return this.num1 + this.num2;
}
}
}).mount('#app');
2
3
4
5
6
7
8
9
10
11
12
13
當使用者分別為 num1
與 num2
兩個輸入框填入 1
與 2
後, sum
仍會出現 12
的結果。
過去我們在處理這樣問題的時候,會在 computed
的 sum
裡面針對 num1
與 num2
做數字轉型。
然而,Vue.js 針對這類數字轉型的問題也提供了 .number
的修飾子,也就是說,我們只需要在原本的 v-model
後面加上 .number
<input v-model.number="num1"> + <input v-model.number="num2"> = {{ sum }}
如此一來,當 v-model
在更新狀態的時候,會在背後試圖將對應的資料轉型為數字的格式,
即便 sum
維持 return this.num1 + this.num2
的程式碼,回傳的也會是兩個數字相加的結果了。
# .trim
如果我們想要針對某個輸入框自動過濾前後的空白字元時,就可以在 v-model
後面加上 .trim
修飾子:
<input v-model.trim="msg">
此時更新後的實體狀態前後的空白字元就會自動被過濾掉了。
v-model
的修飾子就介紹到這,未來在介紹「事件」的時候,我們還會看到更多其他的修飾子。
# 模板綁定 - v-text
/ v-html
/ v-once
/ v-pre
# v-text
先前我們曾提過, Vue.js 是採用兩組大括號 {{ }}
的 Mustache 語法作為模板,除了這種方式之外,我們也可以透過指令來處理模板:
const vm = Vue.createApp({
data () {
return {
text: 'HELLO'
}
}
}).mount('#app');
2
3
4
5
6
7
<div v-text="text"></div>
像這樣我們透過 v-text
指令來指定對應 <div>
標籤的內容,此時畫面渲染的結果會與
<div>{{ text }}</div>
一樣都是 HELLO
。
不同的是,若我們在 <div>
標籤內加入文字,如:
<!-- 只會出現 HELLO -->
<div v-text="text">World!</div>
<!-- HELLO World! -->
<div>{{ text }} World!</div>
2
3
4
5
以 v-text
指令渲染的結果會無視標籤內原本的內容,也就是只會出現 HELLO
,而透過模板語法的結果則是 HELLO World!
。
# v-html
除了 v-text
之外,還有個類似的指令,叫 v-html
。
v-html
與 v-text
的差別,就像 JavaScript DOM API 的 innerText
與 innerHTML
一樣。
即使 data
裡的內容是 HTML 格式的字串,用 v-text
以及兩組大括號 {{ }}
的模板輸出的結果,會是原本的 HTML 字串而不會變成 HTML 來渲染:
const vm = Vue.createApp({
data () {
return {
text: '<h1>HELLO</h1>'
}
}
}).mount('#app');
2
3
4
5
6
7
<div v-text="text"></div>
顯示的結果會是 <h1>HELLO</h1>
的字串。
而改用 v-html
的情況:
<div v-html="text"></div>
則會出現解析後的 HELLO
h1 大標題。
# v-once
v-once
的作用,在於只會渲染指定的節點一次,往後就不再更新。
如:
const vm = Vue.createApp({
data () {
return {
text: 'HELLO'
}
}
}).mount('#app');
2
3
4
5
6
7
<input v-model="text">
<-- 此處可以比較有無 v-once 的差異 -->
<div>{{ text }}</div>
<div v-once>{{ text }}</div>
2
3
4
5
像這樣,我們為 text
狀態加上了 input
與 v-model
作為雙向綁定,
但即便我們更新輸入框內的文字,加上了 v-once
的 div
依然只會顯示最初的 HELLO
字樣,並不會隨之更新。
# v-pre
前面提到,Vue.js 採用兩組大括號 {{ }}
的 Mustache 語法作為模板,
若我們就是想在網頁上呈現兩組大括號,而不是將它當作模板時,就會需要 v-pre
這個指令了:
const vm = Vue.createApp({
data () {
return {
text: 'HELLO'
}
}
}).mount('#app');
2
3
4
5
6
7
<!-- 加上 v-pre 後不會解析模板內容 -->
<div v-pre>{{ text }}</div>
<!-- 將 {{ text }} 轉換為 HELLO -->
<div>{{ text }}</div>
2
3
4
5
像這樣,上面有加上 v-pre
指令的標籤,會顯示原始的 {{ text }}
的字樣,而下方 <div>
則是會出現 HELLO
的結果。
# 樣式綁定
本篇的最後來聊聊 Vue.js 與 CSS 樣式的綁定。
說到網頁的樣式,我們大多會透過 HTML 標籤的 class
或 style
屬性來處理,
前面講到的 v-bind
除了可以用來控制一般屬性外,也可以用來控制標籤的 class
與樣式 style
的內容。
假設我們想要一個功能,當使用者在文字輸入框輸入超過十個字元的內容時,加入紅色樣式來作為警告提示,我們可以這樣做:
首先我們在 CSS 定義 .error
的樣式:
.error {
border: red solid 1px;
color: red;
}
2
3
4
接著我們在 Vue 實體中定義 message
狀態來與 input
綁定:
const vm = Vue.createApp({
data () {
return {
message: ''
}
}
}).mount('#app');
2
3
4
5
6
7
<div id="app">
<input
type="text"
v-model.trim="message"
v-bind:class="{ 'error': message.length > 10 }">
</div>
2
3
4
5
6
最後,我們就可以透過 v-bind:class
或 :class
來設定條件,當 message
的內容長度超過了 10 個字時,
就會自動加上 error
的 class
。
如果想要一次控制多個 class
,則可以透過逗號分隔:
<!-- v-bind:class 也可簡寫為 :class -->
<div v-bind:class="{ active: isActive, 'text-danger': hasError }"> ... </div>
2
當 isActive
與 hasError
的狀態更新時,對應的 class
也會依照它們的 truthy
或 falsy
狀態來進行更新。
小提醒
在 JavaScript 裡, truthy
或 falsy
指的是在邏輯判斷式,轉型為 true
的值,我們稱為 truthy value
,而轉型後為 false
的值,則稱為 falsy value
。
不熟悉 JavaScript truthy
或 falsy
規則的朋友可參考拙作:重新認識 JavaScript: Day 08 Boolean 的真假判斷 (opens new window) 的介紹。
而除了 class
之外,我們也可以直接操作 style
的屬性,以前面的範例改寫:
<div id="app">
<input
type="text"
v-model.trim="message"
placeholder="請勿超過十個字"
:style="msgStyle">
</div>
2
3
4
5
6
7
const vm = Vue.createApp({
data () {
return {
message: ''
}
},
computed: {
isVaild: function() {
return this.message.length <= 10;
},
msgStyle: function() {
return {
'border': this.isVaild ? '' : 'red solid 1px',
'color': this.isVaild ? '' : 'red'
};
}
}
}).mount('#app');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
像這樣,我們將驗證的邏輯透過 computed
屬性來包裝,樣式由 msgStyle
來處理。
當輸入文字不超過 10 個字時,輸入框維持預設樣式,超過 10 個字的時候,則樣式會變成紅框紅字。