# 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 個字的時候,則樣式會變成紅框紅字。